| /* |
| * Copyright (C) 2017 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 static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; |
| import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Notification; |
| import android.content.Context; |
| import android.os.AsyncTask; |
| import android.os.CancellationSignal; |
| import android.service.notification.StatusBarNotification; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.view.View; |
| import android.widget.RemoteViews; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.widget.ImageMessageConsumer; |
| import com.android.systemui.Dependency; |
| import com.android.systemui.statusbar.InflationTask; |
| import com.android.systemui.statusbar.SmartReplyController; |
| import com.android.systemui.statusbar.notification.InflationException; |
| import com.android.systemui.statusbar.notification.MediaNotificationProcessor; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; |
| import com.android.systemui.statusbar.phone.StatusBar; |
| import com.android.systemui.statusbar.policy.HeadsUpManager; |
| import com.android.systemui.statusbar.policy.InflatedSmartReplies; |
| import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; |
| import com.android.systemui.statusbar.policy.SmartReplyConstants; |
| import com.android.systemui.util.Assert; |
| |
| import java.util.HashMap; |
| |
| /** |
| * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by |
| * asynchronously building the content's {@link RemoteViews} and applying it to the row. |
| */ |
| public class NotificationContentInflater implements NotificationRowContentBinder { |
| |
| public static final String TAG = "NotifContentInflater"; |
| |
| private RemoteViews.OnClickHandler mRemoteViewClickHandler; |
| private boolean mInflateSynchronously = false; |
| private final ArrayMap<Integer, RemoteViews> mCachedContentViews = new ArrayMap<>(); |
| |
| @Override |
| public void bindContent( |
| NotificationEntry entry, |
| ExpandableNotificationRow row, |
| @InflationFlag int contentToBind, |
| BindParams bindParams, |
| boolean forceInflate, |
| @Nullable InflationCallback callback) { |
| if (row.isRemoved()) { |
| // We don't want to reinflate anything for removed notifications. Otherwise views might |
| // be readded to the stack, leading to leaks. This may happen with low-priority groups |
| // where the removal of already removed children can lead to a reinflation. |
| return; |
| } |
| |
| StatusBarNotification sbn = row.getEntry().getSbn(); |
| |
| // To check if the notification has inline image and preload inline image if necessary. |
| row.getImageResolver().preloadImages(sbn.getNotification()); |
| |
| if (forceInflate) { |
| mCachedContentViews.clear(); |
| } |
| |
| AsyncInflationTask task = new AsyncInflationTask( |
| sbn, |
| mInflateSynchronously, |
| contentToBind, |
| mCachedContentViews, |
| row, |
| bindParams.isLowPriority, |
| bindParams.isChildInGroup, |
| bindParams.usesIncreasedHeight, |
| bindParams.usesIncreasedHeadsUpHeight, |
| callback, |
| mRemoteViewClickHandler); |
| if (mInflateSynchronously) { |
| task.onPostExecute(task.doInBackground()); |
| } else { |
| task.execute(); |
| } |
| } |
| |
| @VisibleForTesting |
| InflationProgress inflateNotificationViews( |
| NotificationEntry entry, |
| ExpandableNotificationRow row, |
| BindParams bindParams, |
| boolean inflateSynchronously, |
| @InflationFlag int reInflateFlags, |
| Notification.Builder builder, |
| Context packageContext) { |
| InflationProgress result = createRemoteViews(reInflateFlags, |
| builder, |
| bindParams.isLowPriority, |
| bindParams.isChildInGroup, |
| bindParams.usesIncreasedHeight, |
| bindParams.usesIncreasedHeadsUpHeight, |
| packageContext); |
| result = inflateSmartReplyViews(result, reInflateFlags, entry, |
| row.getContext(), packageContext, row.getHeadsUpManager(), |
| row.getExistingSmartRepliesAndActions()); |
| apply( |
| inflateSynchronously, |
| result, |
| reInflateFlags, |
| mCachedContentViews, |
| row, |
| mRemoteViewClickHandler, |
| null); |
| return result; |
| } |
| |
| @Override |
| public void cancelBind( |
| @NonNull NotificationEntry entry, |
| @NonNull ExpandableNotificationRow row) { |
| entry.abortTask(); |
| } |
| |
| @Override |
| public void unbindContent( |
| @NonNull NotificationEntry entry, |
| @NonNull ExpandableNotificationRow row, |
| @InflationFlag int contentToUnbind) { |
| int curFlag = 1; |
| while (contentToUnbind != 0) { |
| if ((contentToUnbind & curFlag) != 0) { |
| freeNotificationView(row, curFlag); |
| } |
| contentToUnbind &= ~curFlag; |
| curFlag = curFlag << 1; |
| } |
| } |
| |
| /** |
| * Set click handler for notification remote views |
| * |
| * @param remoteViewClickHandler click handler for remote views |
| */ |
| public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) { |
| mRemoteViewClickHandler = remoteViewClickHandler; |
| } |
| |
| /** |
| * Frees the content view associated with the inflation flag. Will only succeed if the |
| * view is safe to remove. |
| * |
| * @param inflateFlag the flag corresponding to the content view which should be freed |
| */ |
| private void freeNotificationView(ExpandableNotificationRow row, |
| @InflationFlag int inflateFlag) { |
| switch (inflateFlag) { |
| case FLAG_CONTENT_VIEW_HEADS_UP: |
| if (row.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_HEADSUP)) { |
| row.getPrivateLayout().setHeadsUpChild(null); |
| mCachedContentViews.remove(FLAG_CONTENT_VIEW_HEADS_UP); |
| row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); |
| } |
| break; |
| case FLAG_CONTENT_VIEW_PUBLIC: |
| if (row.getPublicLayout().isContentViewInactive(VISIBLE_TYPE_CONTRACTED)) { |
| row.getPublicLayout().setContractedChild(null); |
| mCachedContentViews.remove(FLAG_CONTENT_VIEW_PUBLIC); |
| } |
| break; |
| case FLAG_CONTENT_VIEW_CONTRACTED: |
| case FLAG_CONTENT_VIEW_EXPANDED: |
| default: |
| break; |
| } |
| } |
| |
| private static InflationProgress inflateSmartReplyViews(InflationProgress result, |
| @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, |
| Context packageContext, HeadsUpManager headsUpManager, |
| SmartRepliesAndActions previousSmartRepliesAndActions) { |
| SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class); |
| SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class); |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) { |
| result.expandedInflatedSmartReplies = |
| InflatedSmartReplies.inflate( |
| context, packageContext, entry, smartReplyConstants, |
| smartReplyController, headsUpManager, previousSmartRepliesAndActions); |
| } |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) { |
| result.headsUpInflatedSmartReplies = |
| InflatedSmartReplies.inflate( |
| context, packageContext, entry, smartReplyConstants, |
| smartReplyController, headsUpManager, previousSmartRepliesAndActions); |
| } |
| return result; |
| } |
| |
| private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, |
| Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, |
| boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, |
| Context packageContext) { |
| InflationProgress result = new InflationProgress(); |
| isLowPriority = isLowPriority && !isChildInGroup; |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { |
| result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { |
| result.newExpandedView = createExpandedView(builder, isLowPriority); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { |
| result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { |
| result.newPublicView = builder.makePublicContentView(isLowPriority); |
| } |
| |
| result.packageContext = packageContext; |
| result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); |
| result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( |
| true /* showingPublic */); |
| return result; |
| } |
| |
| public static CancellationSignal apply( |
| boolean inflateSynchronously, |
| InflationProgress result, |
| @InflationFlag int reInflateFlags, |
| ArrayMap<Integer, RemoteViews> cachedContentViews, |
| ExpandableNotificationRow row, |
| RemoteViews.OnClickHandler remoteViewClickHandler, |
| @Nullable InflationCallback callback) { |
| NotificationContentView privateLayout = row.getPrivateLayout(); |
| NotificationContentView publicLayout = row.getPublicLayout(); |
| final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); |
| |
| int flag = FLAG_CONTENT_VIEW_CONTRACTED; |
| if ((reInflateFlags & flag) != 0) { |
| boolean isNewView = |
| !canReapplyRemoteView(result.newContentView, |
| cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED)); |
| ApplyCallback applyCallback = new ApplyCallback() { |
| @Override |
| public void setResultView(View v) { |
| result.inflatedContentView = v; |
| } |
| |
| @Override |
| public RemoteViews getRemoteView() { |
| return result.newContentView; |
| } |
| }; |
| applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews, |
| row, isNewView, remoteViewClickHandler, callback, privateLayout, |
| privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( |
| NotificationContentView.VISIBLE_TYPE_CONTRACTED), |
| runningInflations, applyCallback); |
| } |
| |
| flag = FLAG_CONTENT_VIEW_EXPANDED; |
| if ((reInflateFlags & flag) != 0) { |
| if (result.newExpandedView != null) { |
| boolean isNewView = |
| !canReapplyRemoteView(result.newExpandedView, |
| cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED)); |
| ApplyCallback applyCallback = new ApplyCallback() { |
| @Override |
| public void setResultView(View v) { |
| result.inflatedExpandedView = v; |
| } |
| |
| @Override |
| public RemoteViews getRemoteView() { |
| return result.newExpandedView; |
| } |
| }; |
| applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, |
| cachedContentViews, row, isNewView, remoteViewClickHandler, |
| callback, privateLayout, privateLayout.getExpandedChild(), |
| privateLayout.getVisibleWrapper( |
| NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations, |
| applyCallback); |
| } |
| } |
| |
| flag = FLAG_CONTENT_VIEW_HEADS_UP; |
| if ((reInflateFlags & flag) != 0) { |
| if (result.newHeadsUpView != null) { |
| boolean isNewView = |
| !canReapplyRemoteView(result.newHeadsUpView, |
| cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP)); |
| ApplyCallback applyCallback = new ApplyCallback() { |
| @Override |
| public void setResultView(View v) { |
| result.inflatedHeadsUpView = v; |
| } |
| |
| @Override |
| public RemoteViews getRemoteView() { |
| return result.newHeadsUpView; |
| } |
| }; |
| applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, |
| cachedContentViews, row, isNewView, remoteViewClickHandler, |
| callback, privateLayout, privateLayout.getHeadsUpChild(), |
| privateLayout.getVisibleWrapper( |
| VISIBLE_TYPE_HEADSUP), runningInflations, |
| applyCallback); |
| } |
| } |
| |
| flag = FLAG_CONTENT_VIEW_PUBLIC; |
| if ((reInflateFlags & flag) != 0) { |
| boolean isNewView = |
| !canReapplyRemoteView(result.newPublicView, |
| cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC)); |
| ApplyCallback applyCallback = new ApplyCallback() { |
| @Override |
| public void setResultView(View v) { |
| result.inflatedPublicView = v; |
| } |
| |
| @Override |
| public RemoteViews getRemoteView() { |
| return result.newPublicView; |
| } |
| }; |
| applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews, |
| row, isNewView, remoteViewClickHandler, callback, |
| publicLayout, publicLayout.getContractedChild(), |
| publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), |
| runningInflations, applyCallback); |
| } |
| |
| // Let's try to finish, maybe nobody is even inflating anything |
| finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, callback, row); |
| CancellationSignal cancellationSignal = new CancellationSignal(); |
| cancellationSignal.setOnCancelListener( |
| () -> runningInflations.values().forEach(CancellationSignal::cancel)); |
| return cancellationSignal; |
| } |
| |
| @VisibleForTesting |
| static void applyRemoteView( |
| boolean inflateSynchronously, |
| final InflationProgress result, |
| final @InflationFlag int reInflateFlags, |
| @InflationFlag int inflationId, |
| final ArrayMap<Integer, RemoteViews> cachedContentViews, |
| final ExpandableNotificationRow row, |
| boolean isNewView, |
| RemoteViews.OnClickHandler remoteViewClickHandler, |
| @Nullable final InflationCallback callback, |
| NotificationContentView parentLayout, |
| View existingView, |
| NotificationViewWrapper existingWrapper, |
| final HashMap<Integer, CancellationSignal> runningInflations, |
| ApplyCallback applyCallback) { |
| RemoteViews newContentView = applyCallback.getRemoteView(); |
| if (inflateSynchronously) { |
| try { |
| if (isNewView) { |
| View v = newContentView.apply( |
| result.packageContext, |
| parentLayout, |
| remoteViewClickHandler); |
| v.setIsRootNamespace(true); |
| applyCallback.setResultView(v); |
| } else { |
| newContentView.reapply( |
| result.packageContext, |
| existingView, |
| remoteViewClickHandler); |
| existingWrapper.onReinflated(); |
| } |
| } catch (Exception e) { |
| handleInflationError(runningInflations, e, row.getEntry().getSbn(), callback); |
| // Add a running inflation to make sure we don't trigger callbacks. |
| // Safe to do because only happens in tests. |
| runningInflations.put(inflationId, new CancellationSignal()); |
| } |
| return; |
| } |
| 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) { |
| if (isNewView) { |
| v.setIsRootNamespace(true); |
| applyCallback.setResultView(v); |
| } else if (existingWrapper != null) { |
| existingWrapper.onReinflated(); |
| } |
| runningInflations.remove(inflationId); |
| finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, |
| callback, row); |
| } |
| |
| @Override |
| public void onError(Exception e) { |
| // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could |
| // actually also be a system issue, so let's try on the UI thread again to be safe. |
| try { |
| View newView = existingView; |
| if (isNewView) { |
| newView = newContentView.apply( |
| result.packageContext, |
| parentLayout, |
| remoteViewClickHandler); |
| } else { |
| newContentView.reapply( |
| result.packageContext, |
| existingView, |
| remoteViewClickHandler); |
| } |
| Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", |
| e); |
| onViewApplied(newView); |
| } catch (Exception anotherException) { |
| runningInflations.remove(inflationId); |
| handleInflationError(runningInflations, e, row.getEntry().getSbn(), |
| callback); |
| } |
| } |
| }; |
| CancellationSignal cancellationSignal; |
| if (isNewView) { |
| cancellationSignal = newContentView.applyAsync( |
| result.packageContext, |
| parentLayout, |
| null, |
| listener, |
| remoteViewClickHandler); |
| } else { |
| cancellationSignal = newContentView.reapplyAsync( |
| result.packageContext, |
| existingView, |
| null, |
| listener, |
| remoteViewClickHandler); |
| } |
| runningInflations.put(inflationId, cancellationSignal); |
| } |
| |
| private static void handleInflationError( |
| HashMap<Integer, CancellationSignal> runningInflations, Exception e, |
| StatusBarNotification notification, @Nullable InflationCallback callback) { |
| Assert.isMainThread(); |
| runningInflations.values().forEach(CancellationSignal::cancel); |
| if (callback != null) { |
| callback.handleInflationException(notification, e); |
| } |
| } |
| |
| /** |
| * Finish the inflation of the views |
| * |
| * @return true if the inflation was finished |
| */ |
| private static boolean finishIfDone(InflationProgress result, |
| @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, |
| HashMap<Integer, CancellationSignal> runningInflations, |
| @Nullable InflationCallback endListener, ExpandableNotificationRow row) { |
| Assert.isMainThread(); |
| NotificationEntry entry = row.getEntry(); |
| NotificationContentView privateLayout = row.getPrivateLayout(); |
| NotificationContentView publicLayout = row.getPublicLayout(); |
| if (runningInflations.isEmpty()) { |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { |
| if (result.inflatedContentView != null) { |
| // New view case |
| privateLayout.setContractedChild(result.inflatedContentView); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView); |
| } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED) != null) { |
| // Reinflation case. Only update if it's still cached (i.e. view has not been |
| // freed while inflating). |
| cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView); |
| } |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { |
| if (result.inflatedExpandedView != null) { |
| privateLayout.setExpandedChild(result.inflatedExpandedView); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView); |
| } else if (result.newExpandedView == null) { |
| privateLayout.setExpandedChild(null); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, null); |
| } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED) != null) { |
| cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView); |
| } |
| if (result.newExpandedView != null) { |
| privateLayout.setExpandedInflatedSmartReplies( |
| result.expandedInflatedSmartReplies); |
| } else { |
| privateLayout.setExpandedInflatedSmartReplies(null); |
| } |
| row.setExpandable(result.newExpandedView != null); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { |
| if (result.inflatedHeadsUpView != null) { |
| privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView); |
| } else if (result.newHeadsUpView == null) { |
| privateLayout.setHeadsUpChild(null); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, null); |
| } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP) != null) { |
| cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView); |
| } |
| if (result.newHeadsUpView != null) { |
| privateLayout.setHeadsUpInflatedSmartReplies( |
| result.headsUpInflatedSmartReplies); |
| } else { |
| privateLayout.setHeadsUpInflatedSmartReplies(null); |
| } |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { |
| if (result.inflatedPublicView != null) { |
| publicLayout.setContractedChild(result.inflatedPublicView); |
| cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView); |
| } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC) != null) { |
| cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView); |
| } |
| } |
| |
| entry.headsUpStatusBarText = result.headsUpStatusBarText; |
| entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; |
| if (endListener != null) { |
| endListener.onAsyncInflationFinished(row.getEntry(), reInflateFlags); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private static RemoteViews createExpandedView(Notification.Builder builder, |
| boolean isLowPriority) { |
| RemoteViews bigContentView = builder.createBigContentView(); |
| if (bigContentView != null) { |
| return bigContentView; |
| } |
| if (isLowPriority) { |
| RemoteViews contentView = builder.createContentView(); |
| Notification.Builder.makeHeaderExpanded(contentView); |
| return contentView; |
| } |
| return null; |
| } |
| |
| private static RemoteViews createContentView(Notification.Builder builder, |
| boolean isLowPriority, boolean useLarge) { |
| if (isLowPriority) { |
| return builder.makeLowPriorityContentView(false /* useRegularSubtext */); |
| } |
| return builder.createContentView(useLarge); |
| } |
| |
| /** |
| * @param newView The new view that will be applied |
| * @param oldView The old view that was applied to the existing view before |
| * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. |
| */ |
| @VisibleForTesting |
| static boolean canReapplyRemoteView(final RemoteViews newView, |
| final RemoteViews oldView) { |
| return (newView == null && oldView == null) || |
| (newView != null && oldView != null |
| && oldView.getPackage() != null |
| && newView.getPackage() != null |
| && newView.getPackage().equals(oldView.getPackage()) |
| && newView.getLayoutId() == oldView.getLayoutId() |
| && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)); |
| } |
| |
| /** |
| * Sets whether to perform inflation on the same thread as the caller. This method should only |
| * be used in tests, not in production. |
| */ |
| @VisibleForTesting |
| public void setInflateSynchronously(boolean inflateSynchronously) { |
| mInflateSynchronously = inflateSynchronously; |
| } |
| |
| public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> |
| implements InflationCallback, InflationTask { |
| |
| private final StatusBarNotification mSbn; |
| private final Context mContext; |
| private final boolean mInflateSynchronously; |
| private final boolean mIsLowPriority; |
| private final boolean mIsChildInGroup; |
| private final boolean mUsesIncreasedHeight; |
| private final InflationCallback mCallback; |
| private final boolean mUsesIncreasedHeadsUpHeight; |
| private @InflationFlag int mReInflateFlags; |
| private final ArrayMap<Integer, RemoteViews> mCachedContentViews; |
| private ExpandableNotificationRow mRow; |
| private Exception mError; |
| private RemoteViews.OnClickHandler mRemoteViewClickHandler; |
| private CancellationSignal mCancellationSignal; |
| |
| private AsyncInflationTask( |
| StatusBarNotification notification, |
| boolean inflateSynchronously, |
| @InflationFlag int reInflateFlags, |
| ArrayMap<Integer, RemoteViews> cachedContentViews, |
| ExpandableNotificationRow row, |
| boolean isLowPriority, |
| boolean isChildInGroup, |
| boolean usesIncreasedHeight, |
| boolean usesIncreasedHeadsUpHeight, |
| InflationCallback callback, |
| RemoteViews.OnClickHandler remoteViewClickHandler) { |
| mRow = row; |
| mSbn = notification; |
| mInflateSynchronously = inflateSynchronously; |
| mReInflateFlags = reInflateFlags; |
| mCachedContentViews = cachedContentViews; |
| mContext = mRow.getContext(); |
| mIsLowPriority = isLowPriority; |
| mIsChildInGroup = isChildInGroup; |
| mUsesIncreasedHeight = usesIncreasedHeight; |
| mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; |
| mRemoteViewClickHandler = remoteViewClickHandler; |
| mCallback = callback; |
| NotificationEntry entry = row.getEntry(); |
| entry.setInflationTask(this); |
| } |
| |
| @VisibleForTesting |
| @InflationFlag |
| public int getReInflateFlags() { |
| return mReInflateFlags; |
| } |
| |
| @Override |
| protected InflationProgress doInBackground(Void... params) { |
| try { |
| final Notification.Builder recoveredBuilder |
| = Notification.Builder.recoverBuilder(mContext, |
| mSbn.getNotification()); |
| |
| Context packageContext = mSbn.getPackageContext(mContext); |
| Notification notification = mSbn.getNotification(); |
| if (notification.isMediaNotification()) { |
| MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, |
| packageContext); |
| processor.processNotification(notification, recoveredBuilder); |
| } |
| InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, |
| recoveredBuilder, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, |
| mUsesIncreasedHeadsUpHeight, packageContext); |
| return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(), |
| mRow.getContext(), packageContext, mRow.getHeadsUpManager(), |
| mRow.getExistingSmartRepliesAndActions()); |
| } catch (Exception e) { |
| mError = e; |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(InflationProgress result) { |
| if (mError == null) { |
| mCancellationSignal = apply(mInflateSynchronously, result, mReInflateFlags, |
| mCachedContentViews, mRow, mRemoteViewClickHandler, this); |
| } else { |
| handleError(mError); |
| } |
| } |
| |
| private void handleError(Exception e) { |
| mRow.getEntry().onInflationTaskFinished(); |
| StatusBarNotification sbn = mRow.getEntry().getSbn(); |
| final String ident = sbn.getPackageName() + "/0x" |
| + Integer.toHexString(sbn.getId()); |
| Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); |
| if (mCallback != null) { |
| mCallback.handleInflationException(sbn, |
| new InflationException("Couldn't inflate contentViews" + e)); |
| } |
| } |
| |
| @Override |
| public void abort() { |
| cancel(true /* mayInterruptIfRunning */); |
| if (mCancellationSignal != null) { |
| mCancellationSignal.cancel(); |
| } |
| } |
| |
| @Override |
| public void supersedeTask(InflationTask task) { |
| if (task instanceof AsyncInflationTask) { |
| // We want to inflate all flags of the previous task as well |
| mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags; |
| } |
| } |
| |
| @Override |
| public void handleInflationException(StatusBarNotification notification, Exception e) { |
| handleError(e); |
| } |
| |
| @Override |
| public void onAsyncInflationFinished(NotificationEntry entry, |
| @InflationFlag int inflatedFlags) { |
| mRow.getEntry().onInflationTaskFinished(); |
| mRow.onNotificationUpdated(); |
| if (mCallback != null) { |
| mCallback.onAsyncInflationFinished(mRow.getEntry(), inflatedFlags); |
| } |
| |
| // Notify the resolver that the inflation task has finished, |
| // try to purge unnecessary cached entries. |
| mRow.getImageResolver().purgeCache(); |
| } |
| } |
| |
| @VisibleForTesting |
| static class InflationProgress { |
| private RemoteViews newContentView; |
| private RemoteViews newHeadsUpView; |
| private RemoteViews newExpandedView; |
| private RemoteViews newPublicView; |
| |
| @VisibleForTesting |
| Context packageContext; |
| |
| private View inflatedContentView; |
| private View inflatedHeadsUpView; |
| private View inflatedExpandedView; |
| private View inflatedPublicView; |
| private CharSequence headsUpStatusBarText; |
| private CharSequence headsUpStatusBarTextPublic; |
| |
| private InflatedSmartReplies expandedInflatedSmartReplies; |
| private InflatedSmartReplies headsUpInflatedSmartReplies; |
| } |
| |
| @VisibleForTesting |
| abstract static class ApplyCallback { |
| public abstract void setResultView(View v); |
| public abstract RemoteViews getRemoteView(); |
| } |
| } |