| /* |
| * 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.internal.annotations.VisibleForTesting.Visibility.PACKAGE; |
| import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; |
| import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; |
| 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.content.ContextWrapper; |
| import android.content.pm.ApplicationInfo; |
| import android.os.AsyncTask; |
| import android.os.CancellationSignal; |
| import android.service.notification.StatusBarNotification; |
| 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.dagger.qualifiers.Background; |
| import com.android.systemui.media.MediaDataManagerKt; |
| import com.android.systemui.media.MediaFeatureFlag; |
| import com.android.systemui.statusbar.InflationTask; |
| import com.android.systemui.statusbar.NotificationRemoteInputManager; |
| import com.android.systemui.statusbar.SmartReplyController; |
| import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; |
| 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; |
| import java.util.concurrent.Executor; |
| |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| |
| import dagger.Lazy; |
| |
| /** |
| * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by |
| * asynchronously building the content's {@link RemoteViews} and applying it to the row. |
| */ |
| @Singleton |
| @VisibleForTesting(visibility = PACKAGE) |
| public class NotificationContentInflater implements NotificationRowContentBinder { |
| |
| public static final String TAG = "NotifContentInflater"; |
| |
| private boolean mInflateSynchronously = false; |
| private final boolean mIsMediaInQS; |
| private final NotificationRemoteInputManager mRemoteInputManager; |
| private final NotifRemoteViewCache mRemoteViewCache; |
| private final Lazy<SmartReplyConstants> mSmartReplyConstants; |
| private final Lazy<SmartReplyController> mSmartReplyController; |
| private final ConversationNotificationProcessor mConversationProcessor; |
| private final Executor mBgExecutor; |
| |
| @Inject |
| NotificationContentInflater( |
| NotifRemoteViewCache remoteViewCache, |
| NotificationRemoteInputManager remoteInputManager, |
| Lazy<SmartReplyConstants> smartReplyConstants, |
| Lazy<SmartReplyController> smartReplyController, |
| ConversationNotificationProcessor conversationProcessor, |
| MediaFeatureFlag mediaFeatureFlag, |
| @Background Executor bgExecutor) { |
| mRemoteViewCache = remoteViewCache; |
| mRemoteInputManager = remoteInputManager; |
| mSmartReplyConstants = smartReplyConstants; |
| mSmartReplyController = smartReplyController; |
| mConversationProcessor = conversationProcessor; |
| mIsMediaInQS = mediaFeatureFlag.getEnabled(); |
| mBgExecutor = bgExecutor; |
| } |
| |
| @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 = entry.getSbn(); |
| |
| // To check if the notification has inline image and preload inline image if necessary. |
| row.getImageResolver().preloadImages(sbn.getNotification()); |
| |
| if (forceInflate) { |
| mRemoteViewCache.clearCache(entry); |
| } |
| |
| // Cancel any pending frees on any view we're trying to bind since we should be bound after. |
| cancelContentViewFrees(row, contentToBind); |
| |
| AsyncInflationTask task = new AsyncInflationTask( |
| mBgExecutor, |
| mInflateSynchronously, |
| contentToBind, |
| mRemoteViewCache, |
| entry, |
| mSmartReplyConstants.get(), |
| mSmartReplyController.get(), |
| mConversationProcessor, |
| row, |
| bindParams.isLowPriority, |
| bindParams.usesIncreasedHeight, |
| bindParams.usesIncreasedHeadsUpHeight, |
| callback, |
| mRemoteInputManager.getRemoteViewsOnClickHandler(), |
| mIsMediaInQS); |
| if (mInflateSynchronously) { |
| task.onPostExecute(task.doInBackground()); |
| } else { |
| task.executeOnExecutor(mBgExecutor); |
| } |
| } |
| |
| @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.usesIncreasedHeight, |
| bindParams.usesIncreasedHeadsUpHeight, |
| packageContext); |
| result = inflateSmartReplyViews(result, reInflateFlags, entry, |
| row.getContext(), packageContext, row.getHeadsUpManager(), |
| mSmartReplyConstants.get(), mSmartReplyController.get(), |
| row.getExistingSmartRepliesAndActions()); |
| |
| apply( |
| mBgExecutor, |
| inflateSynchronously, |
| result, |
| reInflateFlags, |
| mRemoteViewCache, |
| entry, |
| row, |
| mRemoteInputManager.getRemoteViewsOnClickHandler(), |
| 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(entry, row, curFlag); |
| } |
| contentToUnbind &= ~curFlag; |
| curFlag = curFlag << 1; |
| } |
| } |
| |
| /** |
| * Frees the content view associated with the inflation flag as soon as the view is not showing. |
| * |
| * @param inflateFlag the flag corresponding to the content view which should be freed |
| */ |
| private void freeNotificationView( |
| NotificationEntry entry, |
| ExpandableNotificationRow row, |
| @InflationFlag int inflateFlag) { |
| switch (inflateFlag) { |
| case FLAG_CONTENT_VIEW_CONTRACTED: |
| row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { |
| row.getPrivateLayout().setContractedChild(null); |
| mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED); |
| }); |
| break; |
| case FLAG_CONTENT_VIEW_EXPANDED: |
| row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> { |
| row.getPrivateLayout().setExpandedChild(null); |
| mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); |
| }); |
| break; |
| case FLAG_CONTENT_VIEW_HEADS_UP: |
| row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> { |
| row.getPrivateLayout().setHeadsUpChild(null); |
| mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); |
| row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); |
| }); |
| break; |
| case FLAG_CONTENT_VIEW_PUBLIC: |
| row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { |
| row.getPublicLayout().setContractedChild(null); |
| mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); |
| }); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Cancel any pending content view frees from {@link #freeNotificationView} for the provided |
| * content views. |
| * |
| * @param row top level notification row containing the content views |
| * @param contentViews content views to cancel pending frees on |
| */ |
| private void cancelContentViewFrees( |
| ExpandableNotificationRow row, |
| @InflationFlag int contentViews) { |
| if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { |
| row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); |
| } |
| if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) { |
| row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED); |
| } |
| if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { |
| row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP); |
| } |
| if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { |
| row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); |
| } |
| } |
| |
| private static InflationProgress inflateSmartReplyViews(InflationProgress result, |
| @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, |
| Context packageContext, HeadsUpManager headsUpManager, |
| SmartReplyConstants smartReplyConstants, SmartReplyController smartReplyController, |
| SmartRepliesAndActions previousSmartRepliesAndActions) { |
| 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 usesIncreasedHeight, |
| boolean usesIncreasedHeadsUpHeight, Context packageContext) { |
| InflationProgress result = new InflationProgress(); |
| |
| 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; |
| } |
| |
| private static CancellationSignal apply( |
| Executor bgExecutor, |
| boolean inflateSynchronously, |
| InflationProgress result, |
| @InflationFlag int reInflateFlags, |
| NotifRemoteViewCache remoteViewCache, |
| NotificationEntry entry, |
| 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, |
| remoteViewCache.getCachedView(entry, 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(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, |
| remoteViewCache, entry, 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, |
| remoteViewCache.getCachedView(entry, 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(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, |
| remoteViewCache, entry, 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, |
| remoteViewCache.getCachedView(entry, 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(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, |
| remoteViewCache, entry, 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, |
| remoteViewCache.getCachedView(entry, 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(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, |
| remoteViewCache, entry, 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, remoteViewCache, runningInflations, callback, entry, |
| row); |
| CancellationSignal cancellationSignal = new CancellationSignal(); |
| cancellationSignal.setOnCancelListener( |
| () -> runningInflations.values().forEach(CancellationSignal::cancel)); |
| return cancellationSignal; |
| } |
| |
| @VisibleForTesting |
| static void applyRemoteView( |
| Executor bgExecutor, |
| boolean inflateSynchronously, |
| final InflationProgress result, |
| final @InflationFlag int reInflateFlags, |
| @InflationFlag int inflationId, |
| final NotifRemoteViewCache remoteViewCache, |
| final NotificationEntry entry, |
| 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(), 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, remoteViewCache, runningInflations, |
| callback, entry, 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(), |
| callback); |
| } |
| } |
| }; |
| CancellationSignal cancellationSignal; |
| if (isNewView) { |
| cancellationSignal = newContentView.applyAsync( |
| result.packageContext, |
| parentLayout, |
| bgExecutor, |
| listener, |
| remoteViewClickHandler); |
| } else { |
| cancellationSignal = newContentView.reapplyAsync( |
| result.packageContext, |
| existingView, |
| bgExecutor, |
| listener, |
| remoteViewClickHandler); |
| } |
| runningInflations.put(inflationId, cancellationSignal); |
| } |
| |
| private static void handleInflationError( |
| HashMap<Integer, CancellationSignal> runningInflations, Exception e, |
| NotificationEntry 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, NotifRemoteViewCache remoteViewCache, |
| HashMap<Integer, CancellationSignal> runningInflations, |
| @Nullable InflationCallback endListener, NotificationEntry entry, |
| ExpandableNotificationRow row) { |
| Assert.isMainThread(); |
| 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); |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, |
| result.newContentView); |
| } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { |
| // Reinflation case. Only update if it's still cached (i.e. view has not been |
| // freed while inflating). |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, |
| result.newContentView); |
| } |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { |
| if (result.inflatedExpandedView != null) { |
| privateLayout.setExpandedChild(result.inflatedExpandedView); |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, |
| result.newExpandedView); |
| } else if (result.newExpandedView == null) { |
| privateLayout.setExpandedChild(null); |
| remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); |
| } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { |
| remoteViewCache.putCachedView(entry, 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); |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, |
| result.newHeadsUpView); |
| } else if (result.newHeadsUpView == null) { |
| privateLayout.setHeadsUpChild(null); |
| remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); |
| } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { |
| remoteViewCache.putCachedView(entry, 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); |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, |
| result.newPublicView); |
| } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { |
| remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, |
| result.newPublicView); |
| } |
| } |
| |
| entry.headsUpStatusBarText = result.headsUpStatusBarText; |
| entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; |
| if (endListener != null) { |
| endListener.onAsyncInflationFinished(entry); |
| } |
| 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 NotificationEntry mEntry; |
| private final Context mContext; |
| private final boolean mInflateSynchronously; |
| private final boolean mIsLowPriority; |
| private final boolean mUsesIncreasedHeight; |
| private final InflationCallback mCallback; |
| private final boolean mUsesIncreasedHeadsUpHeight; |
| private final @InflationFlag int mReInflateFlags; |
| private final NotifRemoteViewCache mRemoteViewCache; |
| private final SmartReplyConstants mSmartReplyConstants; |
| private final SmartReplyController mSmartReplyController; |
| private final Executor mBgExecutor; |
| private ExpandableNotificationRow mRow; |
| private Exception mError; |
| private RemoteViews.OnClickHandler mRemoteViewClickHandler; |
| private CancellationSignal mCancellationSignal; |
| private final ConversationNotificationProcessor mConversationProcessor; |
| private final boolean mIsMediaInQS; |
| |
| private AsyncInflationTask( |
| Executor bgExecutor, |
| boolean inflateSynchronously, |
| @InflationFlag int reInflateFlags, |
| NotifRemoteViewCache cache, |
| NotificationEntry entry, |
| SmartReplyConstants smartReplyConstants, |
| SmartReplyController smartReplyController, |
| ConversationNotificationProcessor conversationProcessor, |
| ExpandableNotificationRow row, |
| boolean isLowPriority, |
| boolean usesIncreasedHeight, |
| boolean usesIncreasedHeadsUpHeight, |
| InflationCallback callback, |
| RemoteViews.OnClickHandler remoteViewClickHandler, |
| boolean isMediaFlagEnabled) { |
| mEntry = entry; |
| mRow = row; |
| mSmartReplyConstants = smartReplyConstants; |
| mSmartReplyController = smartReplyController; |
| mBgExecutor = bgExecutor; |
| mInflateSynchronously = inflateSynchronously; |
| mReInflateFlags = reInflateFlags; |
| mRemoteViewCache = cache; |
| mContext = mRow.getContext(); |
| mIsLowPriority = isLowPriority; |
| mUsesIncreasedHeight = usesIncreasedHeight; |
| mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; |
| mRemoteViewClickHandler = remoteViewClickHandler; |
| mCallback = callback; |
| mConversationProcessor = conversationProcessor; |
| mIsMediaInQS = isMediaFlagEnabled; |
| entry.setInflationTask(this); |
| } |
| |
| @VisibleForTesting |
| @InflationFlag |
| public int getReInflateFlags() { |
| return mReInflateFlags; |
| } |
| |
| @Override |
| protected InflationProgress doInBackground(Void... params) { |
| try { |
| final StatusBarNotification sbn = mEntry.getSbn(); |
| final Notification.Builder recoveredBuilder |
| = Notification.Builder.recoverBuilder(mContext, |
| sbn.getNotification()); |
| |
| Context packageContext = sbn.getPackageContext(mContext); |
| if (recoveredBuilder.usesTemplate()) { |
| // For all of our templates, we want it to be RTL |
| packageContext = new RtlEnabledContext(packageContext); |
| } |
| Notification notification = sbn.getNotification(); |
| if (notification.isMediaNotification() && !(mIsMediaInQS |
| && MediaDataManagerKt.isMediaNotification(sbn))) { |
| MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, |
| packageContext); |
| processor.processNotification(notification, recoveredBuilder); |
| } |
| if (mEntry.getRanking().isConversation()) { |
| mConversationProcessor.processNotification(mEntry, recoveredBuilder); |
| } |
| InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, |
| recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, |
| mUsesIncreasedHeadsUpHeight, packageContext); |
| return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mEntry, |
| mRow.getContext(), packageContext, mRow.getHeadsUpManager(), |
| mSmartReplyConstants, mSmartReplyController, |
| mRow.getExistingSmartRepliesAndActions()); |
| } catch (Exception e) { |
| mError = e; |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(InflationProgress result) { |
| if (mError == null) { |
| mCancellationSignal = apply( |
| mBgExecutor, |
| mInflateSynchronously, |
| result, |
| mReInflateFlags, |
| mRemoteViewCache, |
| mEntry, |
| mRow, |
| mRemoteViewClickHandler, |
| this); |
| } else { |
| handleError(mError); |
| } |
| } |
| |
| private void handleError(Exception e) { |
| mEntry.onInflationTaskFinished(); |
| StatusBarNotification sbn = mEntry.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(mRow.getEntry(), |
| new InflationException("Couldn't inflate contentViews" + e)); |
| } |
| } |
| |
| @Override |
| public void abort() { |
| cancel(true /* mayInterruptIfRunning */); |
| if (mCancellationSignal != null) { |
| mCancellationSignal.cancel(); |
| } |
| } |
| |
| @Override |
| public void handleInflationException(NotificationEntry entry, Exception e) { |
| handleError(e); |
| } |
| |
| @Override |
| public void onAsyncInflationFinished(NotificationEntry entry) { |
| mEntry.onInflationTaskFinished(); |
| mRow.onNotificationUpdated(); |
| if (mCallback != null) { |
| mCallback.onAsyncInflationFinished(mEntry); |
| } |
| |
| // Notify the resolver that the inflation task has finished, |
| // try to purge unnecessary cached entries. |
| mRow.getImageResolver().purgeCache(); |
| } |
| |
| private class RtlEnabledContext extends ContextWrapper { |
| private RtlEnabledContext(Context packageContext) { |
| super(packageContext); |
| } |
| |
| @Override |
| public ApplicationInfo getApplicationInfo() { |
| ApplicationInfo applicationInfo = super.getApplicationInfo(); |
| applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; |
| return applicationInfo; |
| } |
| } |
| } |
| |
| @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(); |
| } |
| } |