| /* |
| * 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_AMBIENT; |
| 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.IntDef; |
| 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.statusbar.InflationTask; |
| 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.util.Assert; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.HashMap; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadFactory; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * A utility that inflates the right kind of contentView based on the state |
| */ |
| public class NotificationInflater { |
| |
| public static final String TAG = "NotificationInflater"; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(flag = true, |
| prefix = {"FLAG_CONTENT_VIEW_"}, |
| value = { |
| FLAG_CONTENT_VIEW_CONTRACTED, |
| FLAG_CONTENT_VIEW_EXPANDED, |
| FLAG_CONTENT_VIEW_HEADS_UP, |
| FLAG_CONTENT_VIEW_AMBIENT, |
| FLAG_CONTENT_VIEW_PUBLIC, |
| FLAG_CONTENT_VIEW_ALL}) |
| public @interface InflationFlag {} |
| /** |
| * The default, contracted view. Seen when the shade is pulled down and in the lock screen |
| * if there is no worry about content sensitivity. |
| */ |
| public static final int FLAG_CONTENT_VIEW_CONTRACTED = 1; |
| |
| /** |
| * The expanded view. Seen when the user expands a notification. |
| */ |
| public static final int FLAG_CONTENT_VIEW_EXPANDED = 1 << 1; |
| |
| /** |
| * The heads up view. Seen when a high priority notification peeks in from the top. |
| */ |
| public static final int FLAG_CONTENT_VIEW_HEADS_UP = 1 << 2; |
| |
| /** |
| * The ambient view. Seen when a high priority notification is received and the phone |
| * is dozing. |
| */ |
| public static final int FLAG_CONTENT_VIEW_AMBIENT = 1 << 3; |
| |
| /** |
| * The public view. This is a version of the contracted view that hides sensitive |
| * information and is used on the lock screen if we determine that the notification's |
| * content should be hidden. |
| */ |
| public static final int FLAG_CONTENT_VIEW_PUBLIC = 1 << 4; |
| |
| public static final int FLAG_CONTENT_VIEW_ALL = ~0; |
| |
| /** |
| * Content views that must be inflated at all times. |
| */ |
| @InflationFlag |
| private static final int REQUIRED_INFLATION_FLAGS = |
| FLAG_CONTENT_VIEW_CONTRACTED |
| | FLAG_CONTENT_VIEW_EXPANDED; |
| |
| /** |
| * The set of content views to inflate. |
| */ |
| @InflationFlag |
| private int mInflationFlags = REQUIRED_INFLATION_FLAGS; |
| |
| static final InflationExecutor EXECUTOR = new InflationExecutor(); |
| |
| private final ExpandableNotificationRow mRow; |
| private boolean mIsLowPriority; |
| private boolean mUsesIncreasedHeight; |
| private boolean mUsesIncreasedHeadsUpHeight; |
| private RemoteViews.OnClickHandler mRemoteViewClickHandler; |
| private boolean mIsChildInGroup; |
| private InflationCallback mCallback; |
| private boolean mRedactAmbient; |
| private final ArrayMap<Integer, RemoteViews> mCachedContentViews = new ArrayMap<>(); |
| |
| public NotificationInflater(ExpandableNotificationRow row) { |
| mRow = row; |
| } |
| |
| public void setIsLowPriority(boolean isLowPriority) { |
| mIsLowPriority = isLowPriority; |
| } |
| |
| /** |
| * Set whether the notification is a child in a group |
| * |
| * @return whether the view was re-inflated |
| */ |
| public void setIsChildInGroup(boolean childInGroup) { |
| if (childInGroup != mIsChildInGroup) { |
| mIsChildInGroup = childInGroup; |
| if (mIsLowPriority) { |
| int flags = FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED; |
| inflateNotificationViews(flags); |
| } |
| } |
| } |
| |
| public void setUsesIncreasedHeight(boolean usesIncreasedHeight) { |
| mUsesIncreasedHeight = usesIncreasedHeight; |
| } |
| |
| public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) { |
| mUsesIncreasedHeadsUpHeight = usesIncreasedHeight; |
| } |
| |
| public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) { |
| mRemoteViewClickHandler = remoteViewClickHandler; |
| } |
| |
| /** |
| * Update whether or not the notification is redacted on the lock screen. If the notification |
| * is now redacted, we should inflate the public contracted view and public ambient view to |
| * now show on the lock screen. |
| * |
| * @param needsRedaction true if the notification should now be redacted on the lock screen |
| */ |
| public void updateNeedsRedaction(boolean needsRedaction) { |
| mRedactAmbient = needsRedaction; |
| if (mRow.getEntry() == null) { |
| return; |
| } |
| int flags = FLAG_CONTENT_VIEW_AMBIENT; |
| if (needsRedaction) { |
| flags |= FLAG_CONTENT_VIEW_PUBLIC; |
| } |
| inflateNotificationViews(flags); |
| } |
| |
| /** |
| * Set whether or not a particular content view is needed and whether or not it should be |
| * inflated. These flags will be used when we inflate or reinflate. |
| * |
| * @param flag the {@link InflationFlag} corresponding to the view that should/should not be |
| * inflated |
| * @param shouldInflate true if the view should be inflated, false otherwise |
| */ |
| public void updateInflationFlag(@InflationFlag int flag, boolean shouldInflate) { |
| if (shouldInflate) { |
| mInflationFlags |= flag; |
| } else if ((REQUIRED_INFLATION_FLAGS & flag) == 0) { |
| mInflationFlags &= ~flag; |
| } |
| } |
| |
| /** |
| * Convenience method for setting multiple flags at once. |
| * |
| * @param flags a set of {@link InflationFlag} corresponding to content views that should be |
| * inflated |
| */ |
| @VisibleForTesting |
| public void addInflationFlags(@InflationFlag int flags) { |
| mInflationFlags |= flags; |
| } |
| |
| /** |
| * Whether or not the view corresponding to the flag is set to be inflated currently. |
| * |
| * @param flag the {@link InflationFlag} corresponding to the view |
| * @return true if the flag is set and view will be inflated, false o/w |
| */ |
| public boolean isInflationFlagSet(@InflationFlag int flag) { |
| return ((mInflationFlags & flag) != 0); |
| } |
| |
| /** |
| * Inflate views for set flags on a background thread. This is asynchronous and will |
| * notify the callback once it's finished. |
| */ |
| public void inflateNotificationViews() { |
| inflateNotificationViews(mInflationFlags); |
| } |
| |
| /** |
| * Inflate all views for the specified flags on a background thread. This is asynchronous and |
| * will notify the callback once it's finished. If the content view is already inflated, this |
| * will reinflate it. |
| * |
| * @param reInflateFlags flags which views should be inflated. Should be a subset of |
| * {@link NotificationInflater#mInflationFlags} as only those will be |
| * inflated/reinflated. |
| */ |
| private void inflateNotificationViews(@InflationFlag int reInflateFlags) { |
| if (mRow.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; |
| } |
| // Only inflate the ones that are set. |
| reInflateFlags &= mInflationFlags; |
| StatusBarNotification sbn = mRow.getEntry().notification; |
| |
| // To check if the notification has inline image and preload inline image if necessary. |
| mRow.getImageResolver().preloadImages(sbn.getNotification()); |
| |
| AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mCachedContentViews, |
| mRow, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, |
| mUsesIncreasedHeadsUpHeight, mRedactAmbient, mCallback, mRemoteViewClickHandler); |
| if (mCallback != null && mCallback.doInflateSynchronous()) { |
| task.onPostExecute(task.doInBackground()); |
| } else { |
| task.execute(); |
| } |
| } |
| |
| @VisibleForTesting |
| InflationProgress inflateNotificationViews(@InflationFlag int reInflateFlags, |
| Notification.Builder builder, Context packageContext) { |
| InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority, |
| mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, |
| mRedactAmbient, packageContext); |
| apply(result, reInflateFlags, mCachedContentViews, mRow, mRedactAmbient, |
| mRemoteViewClickHandler, null); |
| return result; |
| } |
| |
| /** |
| * 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 |
| */ |
| public void freeNotificationView(@InflationFlag int inflateFlag) { |
| if ((mInflationFlags & inflateFlag) != 0) { |
| // The view should still be inflated. |
| return; |
| } |
| switch (inflateFlag) { |
| case FLAG_CONTENT_VIEW_HEADS_UP: |
| if (mRow.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_HEADSUP)) { |
| mRow.getPrivateLayout().setHeadsUpChild(null); |
| mCachedContentViews.remove(FLAG_CONTENT_VIEW_HEADS_UP); |
| } |
| break; |
| case FLAG_CONTENT_VIEW_AMBIENT: |
| boolean privateSafeToRemove = mRow.getPrivateLayout().isContentViewInactive( |
| VISIBLE_TYPE_AMBIENT); |
| boolean publicSafeToRemove = mRow.getPublicLayout().isContentViewInactive( |
| VISIBLE_TYPE_AMBIENT); |
| if (privateSafeToRemove) { |
| mRow.getPrivateLayout().setAmbientChild(null); |
| } |
| if (publicSafeToRemove) { |
| mRow.getPublicLayout().setAmbientChild(null); |
| } |
| if (privateSafeToRemove && publicSafeToRemove) { |
| mCachedContentViews.remove(FLAG_CONTENT_VIEW_AMBIENT); |
| } |
| break; |
| case FLAG_CONTENT_VIEW_PUBLIC: |
| if (mRow.getPublicLayout().isContentViewInactive(VISIBLE_TYPE_CONTRACTED)) { |
| mRow.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 createRemoteViews(@InflationFlag int reInflateFlags, |
| Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, |
| boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, |
| 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(); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) { |
| result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification() |
| : builder.makeAmbientNotification(); |
| } |
| result.packageContext = packageContext; |
| result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); |
| result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( |
| true /* showingPublic */); |
| return result; |
| } |
| |
| public static CancellationSignal apply(InflationProgress result, |
| @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, |
| ExpandableNotificationRow row, boolean redactAmbient, |
| 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(result, reInflateFlags, flag, cachedContentViews, row, redactAmbient, |
| 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(result, reInflateFlags, flag, cachedContentViews, row, |
| redactAmbient, 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(result, reInflateFlags, flag, cachedContentViews, row, |
| redactAmbient, 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(result, reInflateFlags, flag, cachedContentViews, row, |
| redactAmbient, isNewView, remoteViewClickHandler, callback, |
| publicLayout, publicLayout.getContractedChild(), |
| publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), |
| runningInflations, applyCallback); |
| } |
| |
| flag = FLAG_CONTENT_VIEW_AMBIENT; |
| if ((reInflateFlags & flag) != 0) { |
| NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout; |
| boolean isNewView = (!canReapplyAmbient(row, redactAmbient) |
| || !canReapplyRemoteView(result.newAmbientView, |
| cachedContentViews.get(FLAG_CONTENT_VIEW_AMBIENT))); |
| ApplyCallback applyCallback = new ApplyCallback() { |
| @Override |
| public void setResultView(View v) { |
| result.inflatedAmbientView = v; |
| } |
| |
| @Override |
| public RemoteViews getRemoteView() { |
| return result.newAmbientView; |
| } |
| }; |
| applyRemoteView(result, reInflateFlags, flag, cachedContentViews, row, |
| redactAmbient, isNewView, remoteViewClickHandler, callback, |
| newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper( |
| NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations, |
| applyCallback); |
| } |
| |
| // Let's try to finish, maybe nobody is even inflating anything |
| finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, callback, row, |
| redactAmbient); |
| CancellationSignal cancellationSignal = new CancellationSignal(); |
| cancellationSignal.setOnCancelListener( |
| () -> runningInflations.values().forEach(CancellationSignal::cancel)); |
| return cancellationSignal; |
| } |
| |
| @VisibleForTesting |
| static void applyRemoteView(final InflationProgress result, |
| final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, |
| final ArrayMap<Integer, RemoteViews> cachedContentViews, |
| final ExpandableNotificationRow row, final boolean redactAmbient, 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 (callback != null && callback.doInflateSynchronous()) { |
| 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.getStatusBarNotification(), 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, redactAmbient); |
| } |
| |
| @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.getStatusBarNotification(), |
| callback); |
| } |
| } |
| }; |
| CancellationSignal cancellationSignal; |
| if (isNewView) { |
| cancellationSignal = newContentView.applyAsync( |
| result.packageContext, |
| parentLayout, |
| EXECUTOR, |
| listener, |
| remoteViewClickHandler); |
| } else { |
| cancellationSignal = newContentView.reapplyAsync( |
| result.packageContext, |
| existingView, |
| EXECUTOR, |
| 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, |
| boolean redactAmbient) { |
| 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) { |
| privateLayout.setContractedChild(result.inflatedContentView); |
| } |
| cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { |
| if (result.inflatedExpandedView != null) { |
| privateLayout.setExpandedChild(result.inflatedExpandedView); |
| } else if (result.newExpandedView == null) { |
| privateLayout.setExpandedChild(null); |
| } |
| cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView); |
| row.setExpandable(result.newExpandedView != null); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { |
| if (result.inflatedHeadsUpView != null) { |
| privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); |
| } else if (result.newHeadsUpView == null) { |
| privateLayout.setHeadsUpChild(null); |
| } |
| cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { |
| if (result.inflatedPublicView != null) { |
| publicLayout.setContractedChild(result.inflatedPublicView); |
| } |
| cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView); |
| } |
| |
| if ((reInflateFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) { |
| if (result.inflatedAmbientView != null) { |
| NotificationContentView newParent = redactAmbient |
| ? publicLayout : privateLayout; |
| NotificationContentView otherParent = !redactAmbient |
| ? publicLayout : privateLayout; |
| newParent.setAmbientChild(result.inflatedAmbientView); |
| otherParent.setAmbientChild(null); |
| } |
| cachedContentViews.put(FLAG_CONTENT_VIEW_AMBIENT, result.newAmbientView); |
| } |
| 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)); |
| } |
| |
| public void setInflationCallback(InflationCallback callback) { |
| mCallback = callback; |
| } |
| |
| public interface InflationCallback { |
| void handleInflationException(StatusBarNotification notification, Exception e); |
| |
| /** |
| * Callback for after the content views finish inflating. |
| * |
| * @param entry the entry with the content views set |
| * @param inflatedFlags the flags associated with the content views that were inflated |
| */ |
| void onAsyncInflationFinished(NotificationEntry entry, |
| @InflationFlag int inflatedFlags); |
| |
| /** |
| * Used to disable async-ness for tests. Should only be used for tests. |
| */ |
| default boolean doInflateSynchronous() { |
| return false; |
| } |
| } |
| |
| public void clearCachesAndReInflate() { |
| mCachedContentViews.clear(); |
| inflateNotificationViews(); |
| } |
| |
| private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) { |
| NotificationContentView ambientView = redactAmbient ? row.getPublicLayout() |
| : row.getPrivateLayout(); |
| return ambientView.getAmbientChild() != null; |
| } |
| |
| public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> |
| implements InflationCallback, InflationTask { |
| |
| private final StatusBarNotification mSbn; |
| private final Context mContext; |
| private final boolean mIsLowPriority; |
| private final boolean mIsChildInGroup; |
| private final boolean mUsesIncreasedHeight; |
| private final InflationCallback mCallback; |
| private final boolean mUsesIncreasedHeadsUpHeight; |
| private final boolean mRedactAmbient; |
| 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, |
| @InflationFlag int reInflateFlags, |
| ArrayMap<Integer, RemoteViews> cachedContentViews, ExpandableNotificationRow row, |
| boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, |
| boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, |
| InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler) { |
| mRow = row; |
| mSbn = notification; |
| mReInflateFlags = reInflateFlags; |
| mCachedContentViews = cachedContentViews; |
| mContext = mRow.getContext(); |
| mIsLowPriority = isLowPriority; |
| mIsChildInGroup = isChildInGroup; |
| mUsesIncreasedHeight = usesIncreasedHeight; |
| mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; |
| mRedactAmbient = redactAmbient; |
| 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); |
| } |
| return createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, |
| mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, |
| mRedactAmbient, packageContext); |
| } catch (Exception e) { |
| mError = e; |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(InflationProgress result) { |
| if (mError == null) { |
| mCancellationSignal = apply(result, mReInflateFlags, mCachedContentViews, mRow, |
| mRedactAmbient, mRemoteViewClickHandler, this); |
| } else { |
| handleError(mError); |
| } |
| } |
| |
| private void handleError(Exception e) { |
| mRow.getEntry().onInflationTaskFinished(); |
| StatusBarNotification sbn = mRow.getStatusBarNotification(); |
| final String ident = sbn.getPackageName() + "/0x" |
| + Integer.toHexString(sbn.getId()); |
| Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); |
| 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(); |
| mCallback.onAsyncInflationFinished(mRow.getEntry(), inflatedFlags); |
| |
| // Notify the resolver that the inflation task has finished, |
| // try to purge unnecessary cached entries. |
| mRow.getImageResolver().purgeCache(); |
| } |
| |
| @Override |
| public boolean doInflateSynchronous() { |
| return mCallback != null && mCallback.doInflateSynchronous(); |
| } |
| } |
| |
| @VisibleForTesting |
| static class InflationProgress { |
| private RemoteViews newContentView; |
| private RemoteViews newHeadsUpView; |
| private RemoteViews newExpandedView; |
| private RemoteViews newAmbientView; |
| private RemoteViews newPublicView; |
| |
| @VisibleForTesting |
| Context packageContext; |
| |
| private View inflatedContentView; |
| private View inflatedHeadsUpView; |
| private View inflatedExpandedView; |
| private View inflatedAmbientView; |
| private View inflatedPublicView; |
| private CharSequence headsUpStatusBarText; |
| private CharSequence headsUpStatusBarTextPublic; |
| } |
| |
| @VisibleForTesting |
| abstract static class ApplyCallback { |
| public abstract void setResultView(View v); |
| public abstract RemoteViews getRemoteView(); |
| } |
| |
| /** |
| * A custom executor that allows more tasks to be queued. Default values are copied from |
| * AsyncTask |
| */ |
| private static class InflationExecutor implements Executor { |
| private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); |
| // We want at least 2 threads and at most 4 threads in the core pool, |
| // preferring to have 1 less than the CPU count to avoid saturating |
| // the CPU with background work |
| private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); |
| private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; |
| private static final int KEEP_ALIVE_SECONDS = 30; |
| |
| private static final ThreadFactory sThreadFactory = new ThreadFactory() { |
| private final AtomicInteger mCount = new AtomicInteger(1); |
| |
| public Thread newThread(Runnable r) { |
| return new Thread(r, "InflaterThread #" + mCount.getAndIncrement()); |
| } |
| }; |
| |
| private final ThreadPoolExecutor mExecutor; |
| |
| private InflationExecutor() { |
| mExecutor = new ThreadPoolExecutor( |
| CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, |
| new LinkedBlockingQueue<>(), sThreadFactory); |
| mExecutor.allowCoreThreadTimeOut(true); |
| } |
| |
| @Override |
| public void execute(Runnable runnable) { |
| mExecutor.execute(runnable); |
| } |
| } |
| } |