/* * 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.content.pm.PackageManager; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Build; import android.os.CancellationSignal; import android.os.Trace; import android.os.UserHandle; 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.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; import com.android.systemui.statusbar.policy.SmartReplyStateInflater; import com.android.systemui.util.Assert; import java.util.HashMap; import java.util.concurrent.Executor; import javax.inject.Inject; /** * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by * asynchronously building the content's {@link RemoteViews} and applying it to the row. */ @SysUISingleton @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 ConversationNotificationProcessor mConversationProcessor; private final Executor mBgExecutor; private final SmartReplyStateInflater mSmartReplyStateInflater; @Inject NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor, SmartReplyStateInflater smartRepliesInflater) { mRemoteViewCache = remoteViewCache; mRemoteInputManager = remoteInputManager; mConversationProcessor = conversationProcessor; mIsMediaInQS = mediaFeatureFlag.getEnabled(); mBgExecutor = bgExecutor; mSmartReplyStateInflater = smartRepliesInflater; } @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, mConversationProcessor, row, bindParams.isLowPriority, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), mIsMediaInQS, mSmartReplyStateInflater); 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, SmartReplyStateInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, bindParams.isLowPriority, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, packageContext); result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, row.getExistingSmartReplyState(), smartRepliesInflater); 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, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater) { boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0 && result.newContentView != null; boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null; boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null; if (inflateContracted || inflateExpanded || inflateHeadsUp) { result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry); } if (inflateExpanded) { result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( context, packageContext, entry, previousSmartReplyState, result.inflatedSmartReplyState); } if (inflateHeadsUp) { result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( context, packageContext, entry, previousSmartReplyState, result.inflatedSmartReplyState); } 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.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback) { NotificationContentView privateLayout = row.getPrivateLayout(); NotificationContentView publicLayout = row.getPublicLayout(); final HashMap 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.InteractionHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap runningInflations, ApplyCallback applyCallback) { RemoteViews newContentView = applyCallback.getRemoteView(); if (inflateSynchronously) { try { if (isNewView) { View v = newContentView.apply( result.packageContext, parentLayout, remoteViewClickHandler); validateView(v, entry, row.getResources()); v.setIsRootNamespace(true); applyCallback.setResultView(v); } else { newContentView.reapply( result.packageContext, existingView, remoteViewClickHandler); validateView(existingView, entry, row.getResources()); 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) { String invalidReason = isValidView(v, entry, row.getResources()); if (invalidReason != null) { handleInflationError(runningInflations, new InflationException(invalidReason), row.getEntry(), callback); runningInflations.remove(inflationId); return; } 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); } /** * Checks if the given View is a valid notification View. * * @return null == valid, non-null == invalid, String represents reason for rejection. */ @VisibleForTesting @Nullable static String isValidView(View view, NotificationEntry entry, Resources resources) { if (!satisfiesMinHeightRequirement(view, entry, resources)) { return "inflated notification does not meet minimum height requirement"; } return null; } private static boolean satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources) { if (!requiresHeightCheck(entry)) { return true; } Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement"); int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); int referenceWidth = resources.getDimensionPixelSize( R.dimen.notification_validation_reference_width); int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY); view.measure(widthSpec, heightSpec); int minHeight = resources.getDimensionPixelSize( R.dimen.notification_validation_minimum_allowed_height); boolean result = view.getMeasuredHeight() >= minHeight; Trace.endSection(); return result; } private static boolean requiresHeightCheck(NotificationEntry entry) { // Undecorated custom views are disallowed from S onwards if (entry.targetSdk >= Build.VERSION_CODES.S) { return false; } // No need to check if the app isn't using any custom views Notification notification = entry.getSbn().getNotification(); if (notification.contentView == null && notification.bigContentView == null && notification.headsUpContentView == null) { return false; } return true; } private static void validateView(View view, NotificationEntry entry, Resources resources) throws InflationException { String invalidReason = isValidView(view, entry, resources); if (invalidReason != null) { throw new InflationException(invalidReason); } } private static void handleInflationError( HashMap 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 runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, ExpandableNotificationRow row) { Assert.isMainThread(); NotificationContentView privateLayout = row.getPrivateLayout(); NotificationContentView publicLayout = row.getPublicLayout(); if (runningInflations.isEmpty()) { boolean setRepliesAndActions = true; 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); } setRepliesAndActions = true; } 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); setRepliesAndActions = true; } 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); } setRepliesAndActions = true; } if (setRepliesAndActions) { privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); } 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 implements InflationCallback, InflationTask { private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L; 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 Executor mBgExecutor; private ExpandableNotificationRow mRow; private Exception mError; private RemoteViews.InteractionHandler mRemoteViewClickHandler; private CancellationSignal mCancellationSignal; private final ConversationNotificationProcessor mConversationProcessor; private final boolean mIsMediaInQS; private final SmartReplyStateInflater mSmartRepliesInflater; private AsyncInflationTask( Executor bgExecutor, boolean inflateSynchronously, @InflationFlag int reInflateFlags, NotifRemoteViewCache cache, NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, SmartReplyStateInflater smartRepliesInflater) { mEntry = entry; mRow = row; mBgExecutor = bgExecutor; mInflateSynchronously = inflateSynchronously; mReInflateFlags = reInflateFlags; mRemoteViewCache = cache; mSmartRepliesInflater = smartRepliesInflater; 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; } void updateApplicationInfo(StatusBarNotification sbn) { String packageName = sbn.getPackageName(); int userId = UserHandle.getUserId(sbn.getUid()); final ApplicationInfo appInfo; try { // This method has an internal cache, so we don't need to add our own caching here. appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); } catch (PackageManager.NameNotFoundException e) { return; } Notification.addFieldsFromContext(appInfo, sbn.getNotification()); } @Override protected InflationProgress doInBackground(Void... params) { try { final StatusBarNotification sbn = mEntry.getSbn(); // Ensure the ApplicationInfo is updated before a builder is recovered. updateApplicationInfo(sbn); 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); } if (mEntry.getRanking().isConversation()) { mConversationProcessor.processNotification(mEntry, recoveredBuilder); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext); InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); InflationProgress result = inflateSmartReplyViews( inflationProgress, mReInflateFlags, mEntry, mContext, packageContext, previousSmartReplyState, mSmartRepliesInflater); // wait for image resolver to finish preloading mRow.getImageResolver().waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS); return result; } 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(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e); if (mCallback != null) { mCallback.handleInflationException(mRow.getEntry(), new InflationException("Couldn't inflate contentViews" + e)); } // Cancel any image loading tasks, not useful any more mRow.getImageResolver().cancelRunningTasks(); } @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(); // Cancel any image loading tasks that have not completed at this point mRow.getImageResolver().cancelRunningTasks(); } private static 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 InflatedSmartReplyState inflatedSmartReplyState; private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; } @VisibleForTesting abstract static class ApplyCallback { public abstract void setResultView(View v); public abstract RemoteViews getRemoteView(); } }