diff options
11 files changed, 414 insertions, 11 deletions
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 3b916d16b2b4..2dec4e87e662 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -3423,6 +3423,12 @@ public class RemoteViews implements Parcelable, Filter { * @hide */ public interface OnViewAppliedListener { + /** + * Callback when the RemoteView has finished inflating, + * but no actions have been applied yet. + */ + default void onViewInflated(View v) {}; + void onViewApplied(View v); void onError(Exception e); @@ -3519,6 +3525,10 @@ public class RemoteViews implements Parcelable, Filter { @Override protected void onPostExecute(ViewTree viewTree) { if (mError == null) { + if (mListener != null) { + mListener.onViewInflated(viewTree.mRoot); + } + try { if (mActions != null) { OnClickHandler handler = mHandler == null diff --git a/core/java/com/android/internal/widget/ImageMessageConsumer.java b/core/java/com/android/internal/widget/ImageMessageConsumer.java new file mode 100644 index 000000000000..01613dcdf3fa --- /dev/null +++ b/core/java/com/android/internal/widget/ImageMessageConsumer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +/** + * An interface for the class who will use the {@link ImageResolver} to resolve images. + */ +public interface ImageMessageConsumer { + /** + * Set the custom {@link ImageResolver} other than {@link LocalImageResolver}. + * @param resolver An image resolver that has custom implementation. + */ + void setImageResolver(ImageResolver resolver); +} diff --git a/core/java/com/android/internal/widget/ImageResolver.java b/core/java/com/android/internal/widget/ImageResolver.java new file mode 100644 index 000000000000..45885257ad8d --- /dev/null +++ b/core/java/com/android/internal/widget/ImageResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.graphics.drawable.Drawable; +import android.net.Uri; + +/** + * An interface for image resolvers that have custom implementations like cache mechanisms. + */ +public interface ImageResolver { + /** + * Load an image from specified uri. + * @param uri Uri of the target image. + * @return Target image in Drawable. + */ + Drawable loadImage(Uri uri); +} diff --git a/core/java/com/android/internal/widget/LocalImageResolver.java b/core/java/com/android/internal/widget/LocalImageResolver.java index 71d3bb5d6b5c..2302de2cd058 100644 --- a/core/java/com/android/internal/widget/LocalImageResolver.java +++ b/core/java/com/android/internal/widget/LocalImageResolver.java @@ -17,7 +17,6 @@ package com.android.internal.widget; import android.annotation.Nullable; -import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; diff --git a/core/java/com/android/internal/widget/MessagingImageMessage.java b/core/java/com/android/internal/widget/MessagingImageMessage.java index 607a3a9ab542..64650a7ebc2f 100644 --- a/core/java/com/android/internal/widget/MessagingImageMessage.java +++ b/core/java/com/android/internal/widget/MessagingImageMessage.java @@ -25,6 +25,7 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.util.Pools; @@ -57,6 +58,7 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage private int mActualWidth; private int mActualHeight; private boolean mIsIsolated; + private ImageResolver mImageResolver; public MessagingImageMessage(@NonNull Context context) { this(context, null); @@ -96,11 +98,16 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage MessagingMessage.super.setMessage(message); Drawable drawable; try { - drawable = LocalImageResolver.resolveImage(message.getDataUri(), getContext()); + Uri uri = message.getDataUri(); + drawable = mImageResolver != null ? mImageResolver.loadImage(uri) : + LocalImageResolver.resolveImage(uri, getContext()); } catch (IOException | SecurityException e) { e.printStackTrace(); return false; } + if (drawable == null) { + return false; + } int intrinsicHeight = drawable.getIntrinsicHeight(); if (intrinsicHeight == 0) { Log.w(TAG, "Drawable with 0 intrinsic height was returned"); @@ -114,7 +121,7 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage } static MessagingMessage createMessage(MessagingLayout layout, - Notification.MessagingStyle.Message m) { + Notification.MessagingStyle.Message m, ImageResolver resolver) { MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout(); MessagingImageMessage createdMessage = sInstancePool.acquire(); if (createdMessage == null) { @@ -125,6 +132,7 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage false); createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); } + createdMessage.setImageResolver(resolver); boolean created = createdMessage.setMessage(m); if (!created) { createdMessage.recycle(); @@ -133,6 +141,10 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage return createdMessage; } + private void setImageResolver(ImageResolver resolver) { + mImageResolver = resolver; + } + @Override protected void onDraw(Canvas canvas) { canvas.save(); diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 0f2e9c52add0..07d0d7d91997 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -57,7 +57,7 @@ import java.util.regex.Pattern; * messages and adapts the layout accordingly. */ @RemoteViews.RemoteView -public class MessagingLayout extends FrameLayout { +public class MessagingLayout extends FrameLayout implements ImageMessageConsumer { private static final float COLOR_SHIFT_AMOUNT = 60; /** @@ -95,6 +95,7 @@ public class MessagingLayout extends FrameLayout { private Person mUser; private CharSequence mNameReplacement; private boolean mDisplayImagesAtEnd; + private ImageResolver mImageResolver; public MessagingLayout(@NonNull Context context) { super(context); @@ -167,6 +168,11 @@ public class MessagingLayout extends FrameLayout { bind(newMessages, newHistoricMessages, showSpinner); } + @Override + public void setImageResolver(ImageResolver resolver) { + mImageResolver = resolver; + } + private void addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, CharSequence[] remoteInputHistory) { @@ -463,12 +469,12 @@ public class MessagingLayout extends FrameLayout { */ private List<MessagingMessage> createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic) { - List<MessagingMessage> result = new ArrayList<>();; + List<MessagingMessage> result = new ArrayList<>(); for (int i = 0; i < newMessages.size(); i++) { Notification.MessagingStyle.Message m = newMessages.get(i); MessagingMessage message = findAndRemoveMatchingMessage(m); if (message == null) { - message = MessagingMessage.createMessage(this, m); + message = MessagingMessage.createMessage(this, m, mImageResolver); } message.setIsHistoric(historic); result.add(message); diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java index 74d0aae3634b..c32d3705bba7 100644 --- a/core/java/com/android/internal/widget/MessagingMessage.java +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -33,9 +33,9 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { String IMAGE_MIME_TYPE_PREFIX = "image/"; static MessagingMessage createMessage(MessagingLayout layout, - Notification.MessagingStyle.Message m) { + Notification.MessagingStyle.Message m, ImageResolver resolver) { if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) { - return MessagingImageMessage.createMessage(layout, m); + return MessagingImageMessage.createMessage(layout, m, resolver); } else { return MessagingTextMessage.createMessage(layout, m); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 442d57880578..694c574cbf53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -328,6 +328,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private float mTranslationWhenRemoved; private boolean mWasChildInGroupWhenRemoved; private int mNotificationColorAmbient; + private NotificationInlineImageResolver mImageResolver; private SystemNotificationAsyncTask mSystemNotificationAsyncTask = new SystemNotificationAsyncTask(); @@ -1621,6 +1622,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mFalsingManager = FalsingManager.getInstance(context); mNotificationInflater = new NotificationInflater(this); mMenuRow = new NotificationMenuRow(mContext); + mImageResolver = new NotificationInlineImageResolver(context, + new NotificationInlineImageCache()); initDimens(); } @@ -1657,6 +1660,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView res.getBoolean(R.bool.config_showGroupNotificationBgWhenExpanded); } + NotificationInlineImageResolver getImageResolver() { + return mImageResolver; + } + /** * Resets this view so it can be re-used for an updated notification. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java index ef343fac5afa..9908049984d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java @@ -33,6 +33,7 @@ import android.view.View; import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.ImageMessageConsumer; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.MediaNotificationProcessor; @@ -114,7 +115,7 @@ public class NotificationInflater { @InflationFlag private int mInflationFlags = REQUIRED_INFLATION_FLAGS; - private static final InflationExecutor EXECUTOR = new InflationExecutor(); + static final InflationExecutor EXECUTOR = new InflationExecutor(); private final ExpandableNotificationRow mRow; private boolean mIsLowPriority; @@ -244,6 +245,10 @@ public class NotificationInflater { // Only inflate the ones that are set. reInflateFlags &= mInflationFlags; StatusBarNotification sbn = mRow.getEntry().notification; + + // To check if the notification has inline image and preload inline image if necessary. + mRow.getImageResolver().preloadImages(sbn.getNotification()); + AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mCachedContentViews, mRow, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, mCallback, mRemoteViewClickHandler); @@ -520,8 +525,14 @@ public class NotificationInflater { } return; } - RemoteViews.OnViewAppliedListener listener - = new RemoteViews.OnViewAppliedListener() { + RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() { + + @Override + public void onViewInflated(View v) { + if (v instanceof ImageMessageConsumer) { + ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver()); + } + } @Override public void onViewApplied(View v) { @@ -851,6 +862,10 @@ public class NotificationInflater { mRow.getEntry().onInflationTaskFinished(); mRow.onNotificationUpdated(); mCallback.onAsyncInflationFinished(mRow.getEntry(), inflatedFlags); + + // Notify the resolver that the inflation task has finished, + // try to purge unnecessary cached entries. + mRow.getImageResolver().purgeCache(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java new file mode 100644 index 000000000000..8c8bad2ab196 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +/** + * A cache for inline images of image messages. + */ +public class NotificationInlineImageCache implements NotificationInlineImageResolver.ImageCache { + private static final String TAG = NotificationInlineImageCache.class.getSimpleName(); + + private NotificationInlineImageResolver mResolver; + private final ConcurrentHashMap<Uri, PreloadImageTask> mCache; + + public NotificationInlineImageCache() { + mCache = new ConcurrentHashMap<>(); + } + + @Override + public void setImageResolver(NotificationInlineImageResolver resolver) { + mResolver = resolver; + } + + @Override + public boolean hasEntry(Uri uri) { + return mCache.containsKey(uri); + } + + @Override + public void preload(Uri uri) { + PreloadImageTask newTask = new PreloadImageTask(mResolver); + newTask.executeOnExecutor(NotificationInflater.EXECUTOR, uri); + mCache.put(uri, newTask); + } + + @Override + public Drawable get(Uri uri) { + Drawable result = null; + try { + result = mCache.get(uri).get(); + } catch (InterruptedException | ExecutionException ex) { + Log.d(TAG, "get: Failed get image from " + uri); + } + return result; + } + + @Override + public void purge() { + Set<Uri> wantedSet = mResolver.getWantedUriSet(); + mCache.entrySet().removeIf(entry -> !wantedSet.contains(entry.getKey())); + } + + private static class PreloadImageTask extends AsyncTask<Uri, Void, Drawable> { + private final NotificationInlineImageResolver mResolver; + + PreloadImageTask(NotificationInlineImageResolver resolver) { + mResolver = resolver; + } + + @Override + protected Drawable doInBackground(Uri... uris) { + Drawable drawable = null; + Uri target = uris[0]; + + try { + drawable = mResolver.resolveImage(target); + } catch (IOException ex) { + Log.d(TAG, "PreloadImageTask: Resolve failed from " + target); + } + + return drawable; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java new file mode 100644 index 000000000000..588246f3d2c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import android.app.ActivityManager; +import android.app.Notification; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; + +import com.android.internal.widget.ImageResolver; +import com.android.internal.widget.LocalImageResolver; +import com.android.internal.widget.MessagingMessage; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Custom resolver with built-in image cache for image messages. + */ +public class NotificationInlineImageResolver implements ImageResolver { + private static final String TAG = NotificationInlineImageResolver.class.getSimpleName(); + + private final Context mContext; + private final ImageCache mImageCache; + private Set<Uri> mWantedUriSet; + + /** + * Constructor. + * @param context Context. + * @param imageCache The implementation of internal cache. + */ + public NotificationInlineImageResolver(Context context, ImageCache imageCache) { + mContext = context.getApplicationContext(); + mImageCache = imageCache; + + if (mImageCache != null) { + mImageCache.setImageResolver(this); + } + } + + /** + * Check if this resolver has its internal cache implementation. + * @return True if has its internal cache, false otherwise. + */ + public boolean hasCache() { + return mImageCache != null && !ActivityManager.isLowRamDeviceStatic(); + } + + /** + * To resolve image from specified uri directly. + * @param uri Uri of the image. + * @return Drawable of the image. + * @throws IOException Throws if failed at resolving the image. + */ + Drawable resolveImage(Uri uri) throws IOException { + return LocalImageResolver.resolveImage(uri, mContext); + } + + @Override + public Drawable loadImage(Uri uri) { + Drawable result = null; + try { + result = hasCache() ? mImageCache.get(uri) : resolveImage(uri); + } catch (IOException ex) { + Log.d(TAG, "loadImage: Can't load image from " + uri); + } + return result; + } + + /** + * Resolve the message list from specified notification and + * refresh internal cache according to the result. + * @param notification The Notification to be resolved. + */ + public void preloadImages(Notification notification) { + if (!hasCache()) { + return; + } + + retrieveWantedUriSet(notification); + Set<Uri> wantedSet = getWantedUriSet(); + wantedSet.forEach(uri -> { + if (!mImageCache.hasEntry(uri)) { + // The uri is not in the cache, we need trigger a loading task for it. + mImageCache.preload(uri); + } + }); + } + + /** + * Try to purge unnecessary cache entries. + */ + public void purgeCache() { + if (!hasCache()) { + return; + } + mImageCache.purge(); + } + + private void retrieveWantedUriSet(Notification notification) { + Parcelable[] messages; + Parcelable[] historicMessages; + List<Notification.MessagingStyle.Message> messageList; + List<Notification.MessagingStyle.Message> historicList; + Set<Uri> result = new HashSet<>(); + + Bundle extras = notification.extras; + if (extras == null) { + return; + } + + messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); + messageList = messages == null ? null : + Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); + if (messageList != null) { + for (Notification.MessagingStyle.Message message : messageList) { + if (MessagingMessage.hasImage(message)) { + result.add(message.getDataUri()); + } + } + } + + historicMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); + historicList = historicMessages == null ? null : + Notification.MessagingStyle.Message.getMessagesFromBundleArray(historicMessages); + if (historicList != null) { + for (Notification.MessagingStyle.Message historic : historicList) { + if (MessagingMessage.hasImage(historic)) { + result.add(historic.getDataUri()); + } + } + } + + mWantedUriSet = result; + } + + Set<Uri> getWantedUriSet() { + return mWantedUriSet; + } + + /** + * A interface for internal cache implementation of this resolver. + */ + interface ImageCache { + /** + * Load the image from cache first then resolve from uri if missed the cache. + * @param uri The uri of the image. + * @return Drawable of the image. + */ + Drawable get(Uri uri); + + /** + * Set the image resolver that actually resolves image from specified uri. + * @param resolver The resolver implementation that resolves image from specified uri. + */ + void setImageResolver(NotificationInlineImageResolver resolver); + + /** + * Check if the uri is in the cache no matter it is loading or loaded. + * @param uri The uri to check. + * @return True if it is already in the cache; false otherwise. + */ + boolean hasEntry(Uri uri); + + /** + * Start a new loading task for the target uri. + * @param uri The target to load. + */ + void preload(Uri uri); + + /** + * Purge unnecessary entries in the cache. + */ + void purge(); + } + +} |