1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification; 18 19 import android.annotation.Nullable; 20 import android.app.Notification; 21 import android.content.Context; 22 import android.os.AsyncTask; 23 import android.os.CancellationSignal; 24 import android.service.notification.StatusBarNotification; 25 import android.util.Log; 26 import android.view.View; 27 import android.widget.RemoteViews; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.systemui.R; 31 import com.android.systemui.statusbar.InflationTask; 32 import com.android.systemui.statusbar.ExpandableNotificationRow; 33 import com.android.systemui.statusbar.NotificationContentView; 34 import com.android.systemui.statusbar.NotificationData; 35 import com.android.systemui.statusbar.phone.StatusBar; 36 import com.android.systemui.util.Assert; 37 38 import java.util.HashMap; 39 import java.util.concurrent.Executor; 40 import java.util.concurrent.LinkedBlockingQueue; 41 import java.util.concurrent.ThreadFactory; 42 import java.util.concurrent.ThreadPoolExecutor; 43 import java.util.concurrent.TimeUnit; 44 import java.util.concurrent.atomic.AtomicInteger; 45 46 /** 47 * A utility that inflates the right kind of contentView based on the state 48 */ 49 public class NotificationInflater { 50 51 public static final String TAG = "NotificationInflater"; 52 @VisibleForTesting 53 static final int FLAG_REINFLATE_ALL = ~0; 54 private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0; 55 @VisibleForTesting 56 static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1; 57 private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2; 58 private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3; 59 private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4; 60 private static final InflationExecutor EXECUTOR = new InflationExecutor(); 61 62 private final ExpandableNotificationRow mRow; 63 private boolean mIsLowPriority; 64 private boolean mUsesIncreasedHeight; 65 private boolean mUsesIncreasedHeadsUpHeight; 66 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 67 private boolean mIsChildInGroup; 68 private InflationCallback mCallback; 69 private boolean mRedactAmbient; 70 NotificationInflater(ExpandableNotificationRow row)71 public NotificationInflater(ExpandableNotificationRow row) { 72 mRow = row; 73 } 74 setIsLowPriority(boolean isLowPriority)75 public void setIsLowPriority(boolean isLowPriority) { 76 mIsLowPriority = isLowPriority; 77 } 78 79 /** 80 * Set whether the notification is a child in a group 81 * 82 * @return whether the view was re-inflated 83 */ setIsChildInGroup(boolean childInGroup)84 public void setIsChildInGroup(boolean childInGroup) { 85 if (childInGroup != mIsChildInGroup) { 86 mIsChildInGroup = childInGroup; 87 if (mIsLowPriority) { 88 int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW; 89 inflateNotificationViews(flags); 90 } 91 } ; 92 } 93 setUsesIncreasedHeight(boolean usesIncreasedHeight)94 public void setUsesIncreasedHeight(boolean usesIncreasedHeight) { 95 mUsesIncreasedHeight = usesIncreasedHeight; 96 } 97 setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight)98 public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) { 99 mUsesIncreasedHeadsUpHeight = usesIncreasedHeight; 100 } 101 setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler)102 public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) { 103 mRemoteViewClickHandler = remoteViewClickHandler; 104 } 105 setRedactAmbient(boolean redactAmbient)106 public void setRedactAmbient(boolean redactAmbient) { 107 if (mRedactAmbient != redactAmbient) { 108 mRedactAmbient = redactAmbient; 109 if (mRow.getEntry() == null) { 110 return; 111 } 112 inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW); 113 } 114 } 115 116 /** 117 * Inflate all views of this notification on a background thread. This is asynchronous and will 118 * notify the callback once it's finished. 119 */ inflateNotificationViews()120 public void inflateNotificationViews() { 121 inflateNotificationViews(FLAG_REINFLATE_ALL); 122 } 123 124 /** 125 * Reinflate all views for the specified flags on a background thread. This is asynchronous and 126 * will notify the callback once it's finished. 127 * 128 * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL} 129 * to reinflate all of views. 130 */ 131 @VisibleForTesting inflateNotificationViews(int reInflateFlags)132 void inflateNotificationViews(int reInflateFlags) { 133 if (mRow.isRemoved()) { 134 // We don't want to reinflate anything for removed notifications. Otherwise views might 135 // be readded to the stack, leading to leaks. This may happen with low-priority groups 136 // where the removal of already removed children can lead to a reinflation. 137 return; 138 } 139 StatusBarNotification sbn = mRow.getEntry().notification; 140 AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow, 141 mIsLowPriority, 142 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, 143 mCallback, mRemoteViewClickHandler); 144 if (mCallback != null && mCallback.doInflateSynchronous()) { 145 task.onPostExecute(task.doInBackground()); 146 } else { 147 task.execute(); 148 } 149 } 150 151 @VisibleForTesting inflateNotificationViews(int reInflateFlags, Notification.Builder builder, Context packageContext)152 InflationProgress inflateNotificationViews(int reInflateFlags, 153 Notification.Builder builder, Context packageContext) { 154 InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority, 155 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, 156 mRedactAmbient, packageContext); 157 apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null); 158 return result; 159 } 160 createRemoteViews(int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, Context packageContext)161 private static InflationProgress createRemoteViews(int reInflateFlags, 162 Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, 163 boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, 164 Context packageContext) { 165 InflationProgress result = new InflationProgress(); 166 isLowPriority = isLowPriority && !isChildInGroup; 167 if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { 168 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); 169 } 170 171 if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { 172 result.newExpandedView = createExpandedView(builder, isLowPriority); 173 } 174 175 if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { 176 result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); 177 } 178 179 if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { 180 result.newPublicView = builder.makePublicContentView(); 181 } 182 183 if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { 184 result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification() 185 : builder.makeAmbientNotification(); 186 } 187 result.packageContext = packageContext; 188 result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); 189 result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( 190 true /* showingPublic */); 191 return result; 192 } 193 apply(InflationProgress result, int reInflateFlags, ExpandableNotificationRow row, boolean redactAmbient, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable InflationCallback callback)194 public static CancellationSignal apply(InflationProgress result, int reInflateFlags, 195 ExpandableNotificationRow row, boolean redactAmbient, 196 RemoteViews.OnClickHandler remoteViewClickHandler, 197 @Nullable InflationCallback callback) { 198 NotificationData.Entry entry = row.getEntry(); 199 NotificationContentView privateLayout = row.getPrivateLayout(); 200 NotificationContentView publicLayout = row.getPublicLayout(); 201 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 202 203 int flag = FLAG_REINFLATE_CONTENT_VIEW; 204 if ((reInflateFlags & flag) != 0) { 205 boolean isNewView = !canReapplyRemoteView(result.newContentView, entry.cachedContentView); 206 ApplyCallback applyCallback = new ApplyCallback() { 207 @Override 208 public void setResultView(View v) { 209 result.inflatedContentView = v; 210 } 211 212 @Override 213 public RemoteViews getRemoteView() { 214 return result.newContentView; 215 } 216 }; 217 applyRemoteView(result, reInflateFlags, flag, row, redactAmbient, 218 isNewView, remoteViewClickHandler, callback, entry, privateLayout, 219 privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( 220 NotificationContentView.VISIBLE_TYPE_CONTRACTED), 221 runningInflations, applyCallback); 222 } 223 224 flag = FLAG_REINFLATE_EXPANDED_VIEW; 225 if ((reInflateFlags & flag) != 0) { 226 if (result.newExpandedView != null) { 227 boolean isNewView = !canReapplyRemoteView(result.newExpandedView, 228 entry.cachedBigContentView); 229 ApplyCallback applyCallback = new ApplyCallback() { 230 @Override 231 public void setResultView(View v) { 232 result.inflatedExpandedView = v; 233 } 234 235 @Override 236 public RemoteViews getRemoteView() { 237 return result.newExpandedView; 238 } 239 }; 240 applyRemoteView(result, reInflateFlags, flag, row, 241 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 242 privateLayout, privateLayout.getExpandedChild(), 243 privateLayout.getVisibleWrapper( 244 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations, 245 applyCallback); 246 } 247 } 248 249 flag = FLAG_REINFLATE_HEADS_UP_VIEW; 250 if ((reInflateFlags & flag) != 0) { 251 if (result.newHeadsUpView != null) { 252 boolean isNewView = !canReapplyRemoteView(result.newHeadsUpView, 253 entry.cachedHeadsUpContentView); 254 ApplyCallback applyCallback = new ApplyCallback() { 255 @Override 256 public void setResultView(View v) { 257 result.inflatedHeadsUpView = v; 258 } 259 260 @Override 261 public RemoteViews getRemoteView() { 262 return result.newHeadsUpView; 263 } 264 }; 265 applyRemoteView(result, reInflateFlags, flag, row, 266 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 267 privateLayout, privateLayout.getHeadsUpChild(), 268 privateLayout.getVisibleWrapper( 269 NotificationContentView.VISIBLE_TYPE_HEADSUP), runningInflations, 270 applyCallback); 271 } 272 } 273 274 flag = FLAG_REINFLATE_PUBLIC_VIEW; 275 if ((reInflateFlags & flag) != 0) { 276 boolean isNewView = !canReapplyRemoteView(result.newPublicView, 277 entry.cachedPublicContentView); 278 ApplyCallback applyCallback = new ApplyCallback() { 279 @Override 280 public void setResultView(View v) { 281 result.inflatedPublicView = v; 282 } 283 284 @Override 285 public RemoteViews getRemoteView() { 286 return result.newPublicView; 287 } 288 }; 289 applyRemoteView(result, reInflateFlags, flag, row, 290 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 291 publicLayout, publicLayout.getContractedChild(), 292 publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), 293 runningInflations, applyCallback); 294 } 295 296 flag = FLAG_REINFLATE_AMBIENT_VIEW; 297 if ((reInflateFlags & flag) != 0) { 298 NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout; 299 boolean isNewView = !canReapplyAmbient(row, redactAmbient) || 300 !canReapplyRemoteView(result.newAmbientView, entry.cachedAmbientContentView); 301 ApplyCallback applyCallback = new ApplyCallback() { 302 @Override 303 public void setResultView(View v) { 304 result.inflatedAmbientView = v; 305 } 306 307 @Override 308 public RemoteViews getRemoteView() { 309 return result.newAmbientView; 310 } 311 }; 312 applyRemoteView(result, reInflateFlags, flag, row, 313 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 314 newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper( 315 NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations, 316 applyCallback); 317 } 318 319 // Let's try to finish, maybe nobody is even inflating anything 320 finishIfDone(result, reInflateFlags, runningInflations, callback, row, 321 redactAmbient); 322 CancellationSignal cancellationSignal = new CancellationSignal(); 323 cancellationSignal.setOnCancelListener( 324 () -> runningInflations.values().forEach(CancellationSignal::cancel)); 325 return cancellationSignal; 326 } 327 328 @VisibleForTesting applyRemoteView(final InflationProgress result, final int reInflateFlags, int inflationId, final ExpandableNotificationRow row, final boolean redactAmbient, boolean isNewView, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationData.Entry entry, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)329 static void applyRemoteView(final InflationProgress result, 330 final int reInflateFlags, int inflationId, 331 final ExpandableNotificationRow row, 332 final boolean redactAmbient, boolean isNewView, 333 RemoteViews.OnClickHandler remoteViewClickHandler, 334 @Nullable final InflationCallback callback, NotificationData.Entry entry, 335 NotificationContentView parentLayout, View existingView, 336 NotificationViewWrapper existingWrapper, 337 final HashMap<Integer, CancellationSignal> runningInflations, 338 ApplyCallback applyCallback) { 339 RemoteViews newContentView = applyCallback.getRemoteView(); 340 if (callback != null && callback.doInflateSynchronous()) { 341 try { 342 if (isNewView) { 343 View v = newContentView.apply( 344 result.packageContext, 345 parentLayout, 346 remoteViewClickHandler); 347 v.setIsRootNamespace(true); 348 applyCallback.setResultView(v); 349 } else { 350 newContentView.reapply( 351 result.packageContext, 352 existingView, 353 remoteViewClickHandler); 354 existingWrapper.onReinflated(); 355 } 356 } catch (Exception e) { 357 handleInflationError(runningInflations, e, entry.notification, callback); 358 // Add a running inflation to make sure we don't trigger callbacks. 359 // Safe to do because only happens in tests. 360 runningInflations.put(inflationId, new CancellationSignal()); 361 } 362 return; 363 } 364 RemoteViews.OnViewAppliedListener listener 365 = new RemoteViews.OnViewAppliedListener() { 366 367 @Override 368 public void onViewApplied(View v) { 369 if (isNewView) { 370 v.setIsRootNamespace(true); 371 applyCallback.setResultView(v); 372 } else if (existingWrapper != null) { 373 existingWrapper.onReinflated(); 374 } 375 runningInflations.remove(inflationId); 376 finishIfDone(result, reInflateFlags, runningInflations, callback, row, 377 redactAmbient); 378 } 379 380 @Override 381 public void onError(Exception e) { 382 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could 383 // actually also be a system issue, so let's try on the UI thread again to be safe. 384 try { 385 View newView = existingView; 386 if (isNewView) { 387 newView = newContentView.apply( 388 result.packageContext, 389 parentLayout, 390 remoteViewClickHandler); 391 } else { 392 newContentView.reapply( 393 result.packageContext, 394 existingView, 395 remoteViewClickHandler); 396 } 397 Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", 398 e); 399 onViewApplied(newView); 400 } catch (Exception anotherException) { 401 runningInflations.remove(inflationId); 402 handleInflationError(runningInflations, e, entry.notification, callback); 403 } 404 } 405 }; 406 CancellationSignal cancellationSignal; 407 if (isNewView) { 408 cancellationSignal = newContentView.applyAsync( 409 result.packageContext, 410 parentLayout, 411 EXECUTOR, 412 listener, 413 remoteViewClickHandler); 414 } else { 415 cancellationSignal = newContentView.reapplyAsync( 416 result.packageContext, 417 existingView, 418 EXECUTOR, 419 listener, 420 remoteViewClickHandler); 421 } 422 runningInflations.put(inflationId, cancellationSignal); 423 } 424 handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, Exception e, StatusBarNotification notification, @Nullable InflationCallback callback)425 private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, 426 Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) { 427 Assert.isMainThread(); 428 runningInflations.values().forEach(CancellationSignal::cancel); 429 if (callback != null) { 430 callback.handleInflationException(notification, e); 431 } 432 } 433 434 /** 435 * Finish the inflation of the views 436 * 437 * @return true if the inflation was finished 438 */ finishIfDone(InflationProgress result, int reInflateFlags, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, ExpandableNotificationRow row, boolean redactAmbient)439 private static boolean finishIfDone(InflationProgress result, int reInflateFlags, 440 HashMap<Integer, CancellationSignal> runningInflations, 441 @Nullable InflationCallback endListener, ExpandableNotificationRow row, 442 boolean redactAmbient) { 443 Assert.isMainThread(); 444 NotificationData.Entry entry = row.getEntry(); 445 NotificationContentView privateLayout = row.getPrivateLayout(); 446 NotificationContentView publicLayout = row.getPublicLayout(); 447 if (runningInflations.isEmpty()) { 448 if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { 449 if (result.inflatedContentView != null) { 450 privateLayout.setContractedChild(result.inflatedContentView); 451 } 452 entry.cachedContentView = result.newContentView; 453 } 454 455 if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { 456 if (result.inflatedExpandedView != null) { 457 privateLayout.setExpandedChild(result.inflatedExpandedView); 458 } else if (result.newExpandedView == null) { 459 privateLayout.setExpandedChild(null); 460 } 461 entry.cachedBigContentView = result.newExpandedView; 462 row.setExpandable(result.newExpandedView != null); 463 } 464 465 if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { 466 if (result.inflatedHeadsUpView != null) { 467 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 468 } else if (result.newHeadsUpView == null) { 469 privateLayout.setHeadsUpChild(null); 470 } 471 entry.cachedHeadsUpContentView = result.newHeadsUpView; 472 } 473 474 if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { 475 if (result.inflatedPublicView != null) { 476 publicLayout.setContractedChild(result.inflatedPublicView); 477 } 478 entry.cachedPublicContentView = result.newPublicView; 479 } 480 481 if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { 482 if (result.inflatedAmbientView != null) { 483 NotificationContentView newParent = redactAmbient 484 ? publicLayout : privateLayout; 485 NotificationContentView otherParent = !redactAmbient 486 ? publicLayout : privateLayout; 487 newParent.setAmbientChild(result.inflatedAmbientView); 488 otherParent.setAmbientChild(null); 489 } 490 entry.cachedAmbientContentView = result.newAmbientView; 491 } 492 entry.headsUpStatusBarText = result.headsUpStatusBarText; 493 entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; 494 if (endListener != null) { 495 endListener.onAsyncInflationFinished(row.getEntry()); 496 } 497 return true; 498 } 499 return false; 500 } 501 createExpandedView(Notification.Builder builder, boolean isLowPriority)502 private static RemoteViews createExpandedView(Notification.Builder builder, 503 boolean isLowPriority) { 504 RemoteViews bigContentView = builder.createBigContentView(); 505 if (bigContentView != null) { 506 return bigContentView; 507 } 508 if (isLowPriority) { 509 RemoteViews contentView = builder.createContentView(); 510 Notification.Builder.makeHeaderExpanded(contentView); 511 return contentView; 512 } 513 return null; 514 } 515 createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)516 private static RemoteViews createContentView(Notification.Builder builder, 517 boolean isLowPriority, boolean useLarge) { 518 if (isLowPriority) { 519 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 520 } 521 return builder.createContentView(useLarge); 522 } 523 524 /** 525 * @param newView The new view that will be applied 526 * @param oldView The old view that was applied to the existing view before 527 * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. 528 */ 529 @VisibleForTesting canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)530 static boolean canReapplyRemoteView(final RemoteViews newView, 531 final RemoteViews oldView) { 532 return (newView == null && oldView == null) || 533 (newView != null && oldView != null 534 && oldView.getPackage() != null 535 && newView.getPackage() != null 536 && newView.getPackage().equals(oldView.getPackage()) 537 && newView.getLayoutId() == oldView.getLayoutId() 538 && !oldView.isReapplyDisallowed()); 539 } 540 setInflationCallback(InflationCallback callback)541 public void setInflationCallback(InflationCallback callback) { 542 mCallback = callback; 543 } 544 545 public interface InflationCallback { handleInflationException(StatusBarNotification notification, Exception e)546 void handleInflationException(StatusBarNotification notification, Exception e); onAsyncInflationFinished(NotificationData.Entry entry)547 void onAsyncInflationFinished(NotificationData.Entry entry); 548 549 /** 550 * Used to disable async-ness for tests. Should only be used for tests. 551 */ doInflateSynchronous()552 default boolean doInflateSynchronous() { 553 return false; 554 } 555 } 556 onDensityOrFontScaleChanged()557 public void onDensityOrFontScaleChanged() { 558 NotificationData.Entry entry = mRow.getEntry(); 559 entry.cachedAmbientContentView = null; 560 entry.cachedBigContentView = null; 561 entry.cachedContentView = null; 562 entry.cachedHeadsUpContentView = null; 563 entry.cachedPublicContentView = null; 564 inflateNotificationViews(); 565 } 566 canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient)567 private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) { 568 NotificationContentView ambientView = redactAmbient ? row.getPublicLayout() 569 : row.getPrivateLayout(); ; 570 return ambientView.getAmbientChild() != null; 571 } 572 573 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 574 implements InflationCallback, InflationTask { 575 576 private final StatusBarNotification mSbn; 577 private final Context mContext; 578 private final boolean mIsLowPriority; 579 private final boolean mIsChildInGroup; 580 private final boolean mUsesIncreasedHeight; 581 private final InflationCallback mCallback; 582 private final boolean mUsesIncreasedHeadsUpHeight; 583 private final boolean mRedactAmbient; 584 private int mReInflateFlags; 585 private ExpandableNotificationRow mRow; 586 private Exception mError; 587 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 588 private CancellationSignal mCancellationSignal; 589 AsyncInflationTask(StatusBarNotification notification, int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler)590 private AsyncInflationTask(StatusBarNotification notification, 591 int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, 592 boolean isChildInGroup, boolean usesIncreasedHeight, 593 boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, 594 InflationCallback callback, 595 RemoteViews.OnClickHandler remoteViewClickHandler) { 596 mRow = row; 597 mSbn = notification; 598 mReInflateFlags = reInflateFlags; 599 mContext = mRow.getContext(); 600 mIsLowPriority = isLowPriority; 601 mIsChildInGroup = isChildInGroup; 602 mUsesIncreasedHeight = usesIncreasedHeight; 603 mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; 604 mRedactAmbient = redactAmbient; 605 mRemoteViewClickHandler = remoteViewClickHandler; 606 mCallback = callback; 607 NotificationData.Entry entry = row.getEntry(); 608 entry.setInflationTask(this); 609 } 610 611 @VisibleForTesting getReInflateFlags()612 public int getReInflateFlags() { 613 return mReInflateFlags; 614 } 615 616 @Override doInBackground(Void... params)617 protected InflationProgress doInBackground(Void... params) { 618 try { 619 final Notification.Builder recoveredBuilder 620 = Notification.Builder.recoverBuilder(mContext, 621 mSbn.getNotification()); 622 Context packageContext = mSbn.getPackageContext(mContext); 623 Notification notification = mSbn.getNotification(); 624 if (notification.isMediaNotification()) { 625 MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, 626 packageContext); 627 processor.processNotification(notification, recoveredBuilder); 628 } 629 return createRemoteViews(mReInflateFlags, 630 recoveredBuilder, mIsLowPriority, mIsChildInGroup, 631 mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, 632 packageContext); 633 } catch (Exception e) { 634 mError = e; 635 return null; 636 } 637 } 638 639 @Override onPostExecute(InflationProgress result)640 protected void onPostExecute(InflationProgress result) { 641 if (mError == null) { 642 mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient, 643 mRemoteViewClickHandler, this); 644 } else { 645 handleError(mError); 646 } 647 } 648 handleError(Exception e)649 private void handleError(Exception e) { 650 mRow.getEntry().onInflationTaskFinished(); 651 StatusBarNotification sbn = mRow.getStatusBarNotification(); 652 final String ident = sbn.getPackageName() + "/0x" 653 + Integer.toHexString(sbn.getId()); 654 Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); 655 mCallback.handleInflationException(sbn, 656 new InflationException("Couldn't inflate contentViews" + e)); 657 } 658 659 @Override abort()660 public void abort() { 661 cancel(true /* mayInterruptIfRunning */); 662 if (mCancellationSignal != null) { 663 mCancellationSignal.cancel(); 664 } 665 } 666 667 @Override supersedeTask(InflationTask task)668 public void supersedeTask(InflationTask task) { 669 if (task instanceof AsyncInflationTask) { 670 // We want to inflate all flags of the previous task as well 671 mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags; 672 } 673 } 674 675 @Override handleInflationException(StatusBarNotification notification, Exception e)676 public void handleInflationException(StatusBarNotification notification, Exception e) { 677 handleError(e); 678 } 679 680 @Override onAsyncInflationFinished(NotificationData.Entry entry)681 public void onAsyncInflationFinished(NotificationData.Entry entry) { 682 mRow.getEntry().onInflationTaskFinished(); 683 mRow.onNotificationUpdated(); 684 mCallback.onAsyncInflationFinished(mRow.getEntry()); 685 } 686 687 @Override doInflateSynchronous()688 public boolean doInflateSynchronous() { 689 return mCallback != null && mCallback.doInflateSynchronous(); 690 } 691 } 692 693 @VisibleForTesting 694 static class InflationProgress { 695 private RemoteViews newContentView; 696 private RemoteViews newHeadsUpView; 697 private RemoteViews newExpandedView; 698 private RemoteViews newAmbientView; 699 private RemoteViews newPublicView; 700 701 @VisibleForTesting 702 Context packageContext; 703 704 private View inflatedContentView; 705 private View inflatedHeadsUpView; 706 private View inflatedExpandedView; 707 private View inflatedAmbientView; 708 private View inflatedPublicView; 709 private CharSequence headsUpStatusBarText; 710 private CharSequence headsUpStatusBarTextPublic; 711 } 712 713 @VisibleForTesting 714 abstract static class ApplyCallback { setResultView(View v)715 public abstract void setResultView(View v); getRemoteView()716 public abstract RemoteViews getRemoteView(); 717 } 718 719 /** 720 * A custom executor that allows more tasks to be queued. Default values are copied from 721 * AsyncTask 722 */ 723 private static class InflationExecutor implements Executor { 724 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 725 // We want at least 2 threads and at most 4 threads in the core pool, 726 // preferring to have 1 less than the CPU count to avoid saturating 727 // the CPU with background work 728 private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); 729 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; 730 private static final int KEEP_ALIVE_SECONDS = 30; 731 732 private static final ThreadFactory sThreadFactory = new ThreadFactory() { 733 private final AtomicInteger mCount = new AtomicInteger(1); 734 735 public Thread newThread(Runnable r) { 736 return new Thread(r, "InflaterThread #" + mCount.getAndIncrement()); 737 } 738 }; 739 740 private final ThreadPoolExecutor mExecutor; 741 InflationExecutor()742 private InflationExecutor() { 743 mExecutor = new ThreadPoolExecutor( 744 CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, 745 new LinkedBlockingQueue<>(), sThreadFactory); 746 mExecutor.allowCoreThreadTimeOut(true); 747 } 748 749 @Override execute(Runnable runnable)750 public void execute(Runnable runnable) { 751 mExecutor.execute(runnable); 752 } 753 } 754 } 755