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.row; 18 19 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 20 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; 21 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; 22 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.Notification; 27 import android.content.Context; 28 import android.content.ContextWrapper; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.res.Resources; 32 import android.os.AsyncTask; 33 import android.os.Build; 34 import android.os.CancellationSignal; 35 import android.os.Trace; 36 import android.os.UserHandle; 37 import android.service.notification.StatusBarNotification; 38 import android.util.Log; 39 import android.view.View; 40 import android.widget.RemoteViews; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.widget.ImageMessageConsumer; 44 import com.android.systemui.R; 45 import com.android.systemui.dagger.SysUISingleton; 46 import com.android.systemui.dagger.qualifiers.Background; 47 import com.android.systemui.media.controls.util.MediaFeatureFlag; 48 import com.android.systemui.statusbar.InflationTask; 49 import com.android.systemui.statusbar.NotificationRemoteInputManager; 50 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; 51 import com.android.systemui.statusbar.notification.InflationException; 52 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 53 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 54 import com.android.systemui.statusbar.phone.CentralSurfaces; 55 import com.android.systemui.statusbar.policy.InflatedSmartReplyState; 56 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; 57 import com.android.systemui.statusbar.policy.SmartReplyStateInflater; 58 import com.android.systemui.util.Assert; 59 60 import java.util.HashMap; 61 import java.util.concurrent.Executor; 62 63 import javax.inject.Inject; 64 65 /** 66 * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by 67 * asynchronously building the content's {@link RemoteViews} and applying it to the row. 68 */ 69 @SysUISingleton 70 @VisibleForTesting(visibility = PACKAGE) 71 public class NotificationContentInflater implements NotificationRowContentBinder { 72 73 public static final String TAG = "NotifContentInflater"; 74 75 private boolean mInflateSynchronously = false; 76 private final boolean mIsMediaInQS; 77 private final NotificationRemoteInputManager mRemoteInputManager; 78 private final NotifRemoteViewCache mRemoteViewCache; 79 private final ConversationNotificationProcessor mConversationProcessor; 80 private final Executor mBgExecutor; 81 private final SmartReplyStateInflater mSmartReplyStateInflater; 82 private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; 83 private final NotificationContentInflaterLogger mLogger; 84 85 @Inject NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor, SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory notifLayoutInflaterFactory, NotificationContentInflaterLogger logger)86 NotificationContentInflater( 87 NotifRemoteViewCache remoteViewCache, 88 NotificationRemoteInputManager remoteInputManager, 89 ConversationNotificationProcessor conversationProcessor, 90 MediaFeatureFlag mediaFeatureFlag, 91 @Background Executor bgExecutor, 92 SmartReplyStateInflater smartRepliesInflater, 93 NotifLayoutInflaterFactory notifLayoutInflaterFactory, 94 NotificationContentInflaterLogger logger) { 95 mRemoteViewCache = remoteViewCache; 96 mRemoteInputManager = remoteInputManager; 97 mConversationProcessor = conversationProcessor; 98 mIsMediaInQS = mediaFeatureFlag.getEnabled(); 99 mBgExecutor = bgExecutor; 100 mSmartReplyStateInflater = smartRepliesInflater; 101 mNotifLayoutInflaterFactory = notifLayoutInflaterFactory; 102 mLogger = logger; 103 } 104 105 @Override bindContent( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int contentToBind, BindParams bindParams, boolean forceInflate, @Nullable InflationCallback callback)106 public void bindContent( 107 NotificationEntry entry, 108 ExpandableNotificationRow row, 109 @InflationFlag int contentToBind, 110 BindParams bindParams, 111 boolean forceInflate, 112 @Nullable InflationCallback callback) { 113 if (row.isRemoved()) { 114 // We don't want to reinflate anything for removed notifications. Otherwise views might 115 // be readded to the stack, leading to leaks. This may happen with low-priority groups 116 // where the removal of already removed children can lead to a reinflation. 117 mLogger.logNotBindingRowWasRemoved(entry); 118 return; 119 } 120 121 mLogger.logBinding(entry, contentToBind); 122 123 StatusBarNotification sbn = entry.getSbn(); 124 125 // To check if the notification has inline image and preload inline image if necessary. 126 row.getImageResolver().preloadImages(sbn.getNotification()); 127 128 if (forceInflate) { 129 mRemoteViewCache.clearCache(entry); 130 } 131 132 // Cancel any pending frees on any view we're trying to bind since we should be bound after. 133 cancelContentViewFrees(row, contentToBind); 134 135 AsyncInflationTask task = new AsyncInflationTask( 136 mBgExecutor, 137 mInflateSynchronously, 138 contentToBind, 139 mRemoteViewCache, 140 entry, 141 mConversationProcessor, 142 row, 143 bindParams.isLowPriority, 144 bindParams.usesIncreasedHeight, 145 bindParams.usesIncreasedHeadsUpHeight, 146 callback, 147 mRemoteInputManager.getRemoteViewsOnClickHandler(), 148 mIsMediaInQS, 149 mSmartReplyStateInflater, 150 mNotifLayoutInflaterFactory, 151 mLogger); 152 if (mInflateSynchronously) { 153 task.onPostExecute(task.doInBackground()); 154 } else { 155 task.executeOnExecutor(mBgExecutor); 156 } 157 } 158 159 @VisibleForTesting inflateNotificationViews( NotificationEntry entry, ExpandableNotificationRow row, BindParams bindParams, boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext, SmartReplyStateInflater smartRepliesInflater)160 InflationProgress inflateNotificationViews( 161 NotificationEntry entry, 162 ExpandableNotificationRow row, 163 BindParams bindParams, 164 boolean inflateSynchronously, 165 @InflationFlag int reInflateFlags, 166 Notification.Builder builder, 167 Context packageContext, 168 SmartReplyStateInflater smartRepliesInflater) { 169 InflationProgress result = createRemoteViews(reInflateFlags, 170 builder, 171 bindParams.isLowPriority, 172 bindParams.usesIncreasedHeight, 173 bindParams.usesIncreasedHeadsUpHeight, 174 packageContext, 175 row, 176 mNotifLayoutInflaterFactory, 177 mLogger); 178 179 result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), 180 packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); 181 182 apply( 183 mBgExecutor, 184 inflateSynchronously, 185 result, 186 reInflateFlags, 187 mRemoteViewCache, 188 entry, 189 row, 190 mRemoteInputManager.getRemoteViewsOnClickHandler(), 191 null /* callback */, 192 mLogger); 193 return result; 194 } 195 196 @Override cancelBind( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)197 public boolean cancelBind( 198 @NonNull NotificationEntry entry, 199 @NonNull ExpandableNotificationRow row) { 200 final boolean abortedTask = entry.abortTask(); 201 if (abortedTask) { 202 mLogger.logCancelBindAbortedTask(entry); 203 } 204 return abortedTask; 205 } 206 207 @Override unbindContent( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @InflationFlag int contentToUnbind)208 public void unbindContent( 209 @NonNull NotificationEntry entry, 210 @NonNull ExpandableNotificationRow row, 211 @InflationFlag int contentToUnbind) { 212 mLogger.logUnbinding(entry, contentToUnbind); 213 int curFlag = 1; 214 while (contentToUnbind != 0) { 215 if ((contentToUnbind & curFlag) != 0) { 216 freeNotificationView(entry, row, curFlag); 217 } 218 contentToUnbind &= ~curFlag; 219 curFlag = curFlag << 1; 220 } 221 } 222 223 /** 224 * Frees the content view associated with the inflation flag as soon as the view is not showing. 225 * 226 * @param inflateFlag the flag corresponding to the content view which should be freed 227 */ freeNotificationView( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag)228 private void freeNotificationView( 229 NotificationEntry entry, 230 ExpandableNotificationRow row, 231 @InflationFlag int inflateFlag) { 232 switch (inflateFlag) { 233 case FLAG_CONTENT_VIEW_CONTRACTED: 234 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 235 row.getPrivateLayout().setContractedChild(null); 236 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED); 237 }); 238 break; 239 case FLAG_CONTENT_VIEW_EXPANDED: 240 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> { 241 row.getPrivateLayout().setExpandedChild(null); 242 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 243 }); 244 break; 245 case FLAG_CONTENT_VIEW_HEADS_UP: 246 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> { 247 row.getPrivateLayout().setHeadsUpChild(null); 248 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 249 row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); 250 }); 251 break; 252 case FLAG_CONTENT_VIEW_PUBLIC: 253 row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 254 row.getPublicLayout().setContractedChild(null); 255 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); 256 }); 257 break; 258 default: 259 break; 260 } 261 } 262 263 /** 264 * Cancel any pending content view frees from {@link #freeNotificationView} for the provided 265 * content views. 266 * 267 * @param row top level notification row containing the content views 268 * @param contentViews content views to cancel pending frees on 269 */ cancelContentViewFrees( ExpandableNotificationRow row, @InflationFlag int contentViews)270 private void cancelContentViewFrees( 271 ExpandableNotificationRow row, 272 @InflationFlag int contentViews) { 273 if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 274 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 275 } 276 if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 277 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED); 278 } 279 if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 280 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP); 281 } 282 if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 283 row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 284 } 285 } 286 inflateSmartReplyViews( InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater, NotificationContentInflaterLogger logger)287 private static InflationProgress inflateSmartReplyViews( 288 InflationProgress result, 289 @InflationFlag int reInflateFlags, 290 NotificationEntry entry, 291 Context context, 292 Context packageContext, 293 InflatedSmartReplyState previousSmartReplyState, 294 SmartReplyStateInflater inflater, 295 NotificationContentInflaterLogger logger) { 296 boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0 297 && result.newContentView != null; 298 boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 299 && result.newExpandedView != null; 300 boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 301 && result.newHeadsUpView != null; 302 if (inflateContracted || inflateExpanded || inflateHeadsUp) { 303 logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state"); 304 result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry); 305 } 306 if (inflateExpanded) { 307 logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state"); 308 result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 309 context, packageContext, entry, previousSmartReplyState, 310 result.inflatedSmartReplyState); 311 } 312 if (inflateHeadsUp) { 313 logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state"); 314 result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 315 context, packageContext, entry, previousSmartReplyState, 316 result.inflatedSmartReplyState); 317 } 318 return result; 319 } 320 createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory notifLayoutInflaterFactory, NotificationContentInflaterLogger logger)321 private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, 322 Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, 323 boolean usesIncreasedHeadsUpHeight, Context packageContext, 324 ExpandableNotificationRow row, 325 NotifLayoutInflaterFactory notifLayoutInflaterFactory, 326 NotificationContentInflaterLogger logger) { 327 InflationProgress result = new InflationProgress(); 328 final NotificationEntry entryForLogging = row.getEntry(); 329 330 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 331 logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); 332 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); 333 } 334 335 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 336 logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view"); 337 result.newExpandedView = createExpandedView(builder, isLowPriority); 338 } 339 340 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 341 logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view"); 342 result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); 343 } 344 345 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 346 logger.logAsyncTaskProgress(entryForLogging, "creating public remote view"); 347 result.newPublicView = builder.makePublicContentView(isLowPriority); 348 } 349 setNotifsViewsInflaterFactory(result, notifLayoutInflaterFactory); 350 result.packageContext = packageContext; 351 result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); 352 result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( 353 true /* showingPublic */); 354 return result; 355 } 356 setNotifsViewsInflaterFactory(InflationProgress result, NotifLayoutInflaterFactory notifLayoutInflaterFactory)357 private static void setNotifsViewsInflaterFactory(InflationProgress result, 358 NotifLayoutInflaterFactory notifLayoutInflaterFactory) { 359 setRemoteViewsInflaterFactory(result.newContentView, notifLayoutInflaterFactory); 360 setRemoteViewsInflaterFactory(result.newExpandedView, 361 notifLayoutInflaterFactory); 362 setRemoteViewsInflaterFactory(result.newHeadsUpView, notifLayoutInflaterFactory); 363 setRemoteViewsInflaterFactory(result.newPublicView, notifLayoutInflaterFactory); 364 } 365 setRemoteViewsInflaterFactory(RemoteViews remoteViews, NotifLayoutInflaterFactory notifLayoutInflaterFactory)366 private static void setRemoteViewsInflaterFactory(RemoteViews remoteViews, 367 NotifLayoutInflaterFactory notifLayoutInflaterFactory) { 368 if (remoteViews != null) { 369 remoteViews.setLayoutInflaterFactory(notifLayoutInflaterFactory); 370 } 371 } 372 apply( Executor bgExecutor, boolean inflateSynchronously, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, NotificationEntry entry, ExpandableNotificationRow row, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback, NotificationContentInflaterLogger logger)373 private static CancellationSignal apply( 374 Executor bgExecutor, 375 boolean inflateSynchronously, 376 InflationProgress result, 377 @InflationFlag int reInflateFlags, 378 NotifRemoteViewCache remoteViewCache, 379 NotificationEntry entry, 380 ExpandableNotificationRow row, 381 RemoteViews.InteractionHandler remoteViewClickHandler, 382 @Nullable InflationCallback callback, 383 NotificationContentInflaterLogger logger) { 384 NotificationContentView privateLayout = row.getPrivateLayout(); 385 NotificationContentView publicLayout = row.getPublicLayout(); 386 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 387 388 int flag = FLAG_CONTENT_VIEW_CONTRACTED; 389 if ((reInflateFlags & flag) != 0) { 390 boolean isNewView = 391 !canReapplyRemoteView(result.newContentView, 392 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)); 393 ApplyCallback applyCallback = new ApplyCallback() { 394 @Override 395 public void setResultView(View v) { 396 logger.logAsyncTaskProgress(entry, "contracted view applied"); 397 result.inflatedContentView = v; 398 } 399 @Override 400 public RemoteViews getRemoteView() { 401 return result.newContentView; 402 } 403 }; 404 logger.logAsyncTaskProgress(entry, "applying contracted view"); 405 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 406 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 407 privateLayout, privateLayout.getContractedChild(), 408 privateLayout.getVisibleWrapper( 409 NotificationContentView.VISIBLE_TYPE_CONTRACTED), 410 runningInflations, applyCallback, logger); 411 } 412 413 flag = FLAG_CONTENT_VIEW_EXPANDED; 414 if ((reInflateFlags & flag) != 0) { 415 if (result.newExpandedView != null) { 416 boolean isNewView = 417 !canReapplyRemoteView(result.newExpandedView, 418 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)); 419 ApplyCallback applyCallback = new ApplyCallback() { 420 @Override 421 public void setResultView(View v) { 422 logger.logAsyncTaskProgress(entry, "expanded view applied"); 423 result.inflatedExpandedView = v; 424 } 425 426 @Override 427 public RemoteViews getRemoteView() { 428 return result.newExpandedView; 429 } 430 }; 431 logger.logAsyncTaskProgress(entry, "applying expanded view"); 432 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 433 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 434 callback, privateLayout, privateLayout.getExpandedChild(), 435 privateLayout.getVisibleWrapper( 436 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations, 437 applyCallback, logger); 438 } 439 } 440 441 flag = FLAG_CONTENT_VIEW_HEADS_UP; 442 if ((reInflateFlags & flag) != 0) { 443 if (result.newHeadsUpView != null) { 444 boolean isNewView = 445 !canReapplyRemoteView(result.newHeadsUpView, 446 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)); 447 ApplyCallback applyCallback = new ApplyCallback() { 448 @Override 449 public void setResultView(View v) { 450 logger.logAsyncTaskProgress(entry, "heads up view applied"); 451 result.inflatedHeadsUpView = v; 452 } 453 454 @Override 455 public RemoteViews getRemoteView() { 456 return result.newHeadsUpView; 457 } 458 }; 459 logger.logAsyncTaskProgress(entry, "applying heads up view"); 460 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 461 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 462 callback, privateLayout, privateLayout.getHeadsUpChild(), 463 privateLayout.getVisibleWrapper( 464 VISIBLE_TYPE_HEADSUP), runningInflations, 465 applyCallback, logger); 466 } 467 } 468 469 flag = FLAG_CONTENT_VIEW_PUBLIC; 470 if ((reInflateFlags & flag) != 0) { 471 boolean isNewView = 472 !canReapplyRemoteView(result.newPublicView, 473 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)); 474 ApplyCallback applyCallback = new ApplyCallback() { 475 @Override 476 public void setResultView(View v) { 477 logger.logAsyncTaskProgress(entry, "public view applied"); 478 result.inflatedPublicView = v; 479 } 480 481 @Override 482 public RemoteViews getRemoteView() { 483 return result.newPublicView; 484 } 485 }; 486 logger.logAsyncTaskProgress(entry, "applying public view"); 487 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 488 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 489 publicLayout, publicLayout.getContractedChild(), 490 publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), 491 runningInflations, applyCallback, logger); 492 } 493 494 // Let's try to finish, maybe nobody is even inflating anything 495 finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry, 496 row, logger); 497 CancellationSignal cancellationSignal = new CancellationSignal(); 498 cancellationSignal.setOnCancelListener( 499 () -> { 500 logger.logAsyncTaskProgress(entry, "apply cancelled"); 501 runningInflations.values().forEach(CancellationSignal::cancel); 502 }); 503 504 return cancellationSignal; 505 } 506 507 @VisibleForTesting 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<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback, NotificationContentInflaterLogger logger)508 static void applyRemoteView( 509 Executor bgExecutor, 510 boolean inflateSynchronously, 511 final InflationProgress result, 512 final @InflationFlag int reInflateFlags, 513 @InflationFlag int inflationId, 514 final NotifRemoteViewCache remoteViewCache, 515 final NotificationEntry entry, 516 final ExpandableNotificationRow row, 517 boolean isNewView, 518 RemoteViews.InteractionHandler remoteViewClickHandler, 519 @Nullable final InflationCallback callback, 520 NotificationContentView parentLayout, 521 View existingView, 522 NotificationViewWrapper existingWrapper, 523 final HashMap<Integer, CancellationSignal> runningInflations, 524 ApplyCallback applyCallback, 525 NotificationContentInflaterLogger logger) { 526 RemoteViews newContentView = applyCallback.getRemoteView(); 527 if (inflateSynchronously) { 528 try { 529 if (isNewView) { 530 View v = newContentView.apply( 531 result.packageContext, 532 parentLayout, 533 remoteViewClickHandler); 534 validateView(v, entry, row.getResources()); 535 applyCallback.setResultView(v); 536 } else { 537 newContentView.reapply( 538 result.packageContext, 539 existingView, 540 remoteViewClickHandler); 541 validateView(existingView, entry, row.getResources()); 542 existingWrapper.onReinflated(); 543 } 544 } catch (Exception e) { 545 handleInflationError(runningInflations, e, row.getEntry(), callback, logger, 546 "applying view synchronously"); 547 // Add a running inflation to make sure we don't trigger callbacks. 548 // Safe to do because only happens in tests. 549 runningInflations.put(inflationId, new CancellationSignal()); 550 } 551 return; 552 } 553 RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() { 554 555 @Override 556 public void onViewInflated(View v) { 557 if (v instanceof ImageMessageConsumer) { 558 ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver()); 559 } 560 } 561 562 @Override 563 public void onViewApplied(View v) { 564 String invalidReason = isValidView(v, entry, row.getResources()); 565 if (invalidReason != null) { 566 handleInflationError(runningInflations, new InflationException(invalidReason), 567 row.getEntry(), callback, logger, "applied invalid view"); 568 runningInflations.remove(inflationId); 569 return; 570 } 571 if (isNewView) { 572 applyCallback.setResultView(v); 573 } else if (existingWrapper != null) { 574 existingWrapper.onReinflated(); 575 } 576 runningInflations.remove(inflationId); 577 finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, 578 callback, entry, row, logger); 579 } 580 581 @Override 582 public void onError(Exception e) { 583 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could 584 // actually also be a system issue, so let's try on the UI thread again to be safe. 585 try { 586 View newView = existingView; 587 if (isNewView) { 588 newView = newContentView.apply( 589 result.packageContext, 590 parentLayout, 591 remoteViewClickHandler); 592 } else { 593 newContentView.reapply( 594 result.packageContext, 595 existingView, 596 remoteViewClickHandler); 597 } 598 Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", 599 e); 600 onViewApplied(newView); 601 } catch (Exception anotherException) { 602 runningInflations.remove(inflationId); 603 handleInflationError(runningInflations, e, row.getEntry(), 604 callback, logger, "applying view"); 605 } 606 } 607 }; 608 CancellationSignal cancellationSignal; 609 if (isNewView) { 610 cancellationSignal = newContentView.applyAsync( 611 result.packageContext, 612 parentLayout, 613 bgExecutor, 614 listener, 615 remoteViewClickHandler); 616 } else { 617 cancellationSignal = newContentView.reapplyAsync( 618 result.packageContext, 619 existingView, 620 bgExecutor, 621 listener, 622 remoteViewClickHandler); 623 } 624 runningInflations.put(inflationId, cancellationSignal); 625 } 626 627 /** 628 * Checks if the given View is a valid notification View. 629 * 630 * @return null == valid, non-null == invalid, String represents reason for rejection. 631 */ 632 @VisibleForTesting 633 @Nullable isValidView(View view, NotificationEntry entry, Resources resources)634 static String isValidView(View view, 635 NotificationEntry entry, 636 Resources resources) { 637 if (!satisfiesMinHeightRequirement(view, entry, resources)) { 638 return "inflated notification does not meet minimum height requirement"; 639 } 640 return null; 641 } 642 satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources)643 private static boolean satisfiesMinHeightRequirement(View view, 644 NotificationEntry entry, 645 Resources resources) { 646 if (!requiresHeightCheck(entry)) { 647 return true; 648 } 649 Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement"); 650 int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 651 int referenceWidth = resources.getDimensionPixelSize( 652 R.dimen.notification_validation_reference_width); 653 int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY); 654 view.measure(widthSpec, heightSpec); 655 int minHeight = resources.getDimensionPixelSize( 656 R.dimen.notification_validation_minimum_allowed_height); 657 boolean result = view.getMeasuredHeight() >= minHeight; 658 Trace.endSection(); 659 return result; 660 } 661 requiresHeightCheck(NotificationEntry entry)662 private static boolean requiresHeightCheck(NotificationEntry entry) { 663 // Undecorated custom views are disallowed from S onwards 664 if (entry.targetSdk >= Build.VERSION_CODES.S) { 665 return false; 666 } 667 // No need to check if the app isn't using any custom views 668 Notification notification = entry.getSbn().getNotification(); 669 if (notification.contentView == null 670 && notification.bigContentView == null 671 && notification.headsUpContentView == null) { 672 return false; 673 } 674 return true; 675 } 676 validateView(View view, NotificationEntry entry, Resources resources)677 private static void validateView(View view, 678 NotificationEntry entry, 679 Resources resources) throws InflationException { 680 String invalidReason = isValidView(view, entry, resources); 681 if (invalidReason != null) { 682 throw new InflationException(invalidReason); 683 } 684 } 685 handleInflationError( HashMap<Integer, CancellationSignal> runningInflations, Exception e, NotificationEntry notification, @Nullable InflationCallback callback, NotificationContentInflaterLogger logger, String logContext)686 private static void handleInflationError( 687 HashMap<Integer, CancellationSignal> runningInflations, Exception e, 688 NotificationEntry notification, @Nullable InflationCallback callback, 689 NotificationContentInflaterLogger logger, String logContext) { 690 Assert.isMainThread(); 691 logger.logAsyncTaskException(notification, logContext, e); 692 runningInflations.values().forEach(CancellationSignal::cancel); 693 if (callback != null) { 694 callback.handleInflationException(notification, e); 695 } 696 } 697 698 /** 699 * Finish the inflation of the views 700 * 701 * @return true if the inflation was finished 702 */ finishIfDone(InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, ExpandableNotificationRow row, NotificationContentInflaterLogger logger)703 private static boolean finishIfDone(InflationProgress result, 704 @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, 705 HashMap<Integer, CancellationSignal> runningInflations, 706 @Nullable InflationCallback endListener, NotificationEntry entry, 707 ExpandableNotificationRow row, NotificationContentInflaterLogger logger) { 708 Assert.isMainThread(); 709 NotificationContentView privateLayout = row.getPrivateLayout(); 710 NotificationContentView publicLayout = row.getPublicLayout(); 711 if (runningInflations.isEmpty()) { 712 logger.logAsyncTaskProgress(entry, "finishing"); 713 boolean setRepliesAndActions = true; 714 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 715 if (result.inflatedContentView != null) { 716 // New view case 717 privateLayout.setContractedChild(result.inflatedContentView); 718 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 719 result.newContentView); 720 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { 721 // Reinflation case. Only update if it's still cached (i.e. view has not been 722 // freed while inflating). 723 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 724 result.newContentView); 725 } 726 setRepliesAndActions = true; 727 } 728 729 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 730 if (result.inflatedExpandedView != null) { 731 privateLayout.setExpandedChild(result.inflatedExpandedView); 732 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 733 result.newExpandedView); 734 } else if (result.newExpandedView == null) { 735 privateLayout.setExpandedChild(null); 736 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 737 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { 738 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 739 result.newExpandedView); 740 } 741 if (result.newExpandedView != null) { 742 privateLayout.setExpandedInflatedSmartReplies( 743 result.expandedInflatedSmartReplies); 744 } else { 745 privateLayout.setExpandedInflatedSmartReplies(null); 746 } 747 row.setExpandable(result.newExpandedView != null); 748 setRepliesAndActions = true; 749 } 750 751 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 752 if (result.inflatedHeadsUpView != null) { 753 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 754 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 755 result.newHeadsUpView); 756 } else if (result.newHeadsUpView == null) { 757 privateLayout.setHeadsUpChild(null); 758 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 759 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { 760 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 761 result.newHeadsUpView); 762 } 763 if (result.newHeadsUpView != null) { 764 privateLayout.setHeadsUpInflatedSmartReplies( 765 result.headsUpInflatedSmartReplies); 766 } else { 767 privateLayout.setHeadsUpInflatedSmartReplies(null); 768 } 769 setRepliesAndActions = true; 770 } 771 if (setRepliesAndActions) { 772 privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); 773 } 774 775 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 776 if (result.inflatedPublicView != null) { 777 publicLayout.setContractedChild(result.inflatedPublicView); 778 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 779 result.newPublicView); 780 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { 781 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 782 result.newPublicView); 783 } 784 } 785 786 entry.headsUpStatusBarText = result.headsUpStatusBarText; 787 entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; 788 if (endListener != null) { 789 endListener.onAsyncInflationFinished(entry); 790 } 791 return true; 792 } 793 return false; 794 } 795 createExpandedView(Notification.Builder builder, boolean isLowPriority)796 private static RemoteViews createExpandedView(Notification.Builder builder, 797 boolean isLowPriority) { 798 RemoteViews bigContentView = builder.createBigContentView(); 799 if (bigContentView != null) { 800 return bigContentView; 801 } 802 if (isLowPriority) { 803 RemoteViews contentView = builder.createContentView(); 804 Notification.Builder.makeHeaderExpanded(contentView); 805 return contentView; 806 } 807 return null; 808 } 809 createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)810 private static RemoteViews createContentView(Notification.Builder builder, 811 boolean isLowPriority, boolean useLarge) { 812 if (isLowPriority) { 813 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 814 } 815 return builder.createContentView(useLarge); 816 } 817 818 /** 819 * @param newView The new view that will be applied 820 * @param oldView The old view that was applied to the existing view before 821 * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. 822 */ 823 @VisibleForTesting canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)824 static boolean canReapplyRemoteView(final RemoteViews newView, 825 final RemoteViews oldView) { 826 return (newView == null && oldView == null) || 827 (newView != null && oldView != null 828 && oldView.getPackage() != null 829 && newView.getPackage() != null 830 && newView.getPackage().equals(oldView.getPackage()) 831 && newView.getLayoutId() == oldView.getLayoutId() 832 && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)); 833 } 834 835 /** 836 * Sets whether to perform inflation on the same thread as the caller. This method should only 837 * be used in tests, not in production. 838 */ 839 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)840 public void setInflateSynchronously(boolean inflateSynchronously) { 841 mInflateSynchronously = inflateSynchronously; 842 } 843 844 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 845 implements InflationCallback, InflationTask { 846 847 private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L; 848 private final NotificationEntry mEntry; 849 private final Context mContext; 850 private final boolean mInflateSynchronously; 851 private final boolean mIsLowPriority; 852 private final boolean mUsesIncreasedHeight; 853 private final InflationCallback mCallback; 854 private final boolean mUsesIncreasedHeadsUpHeight; 855 private final @InflationFlag int mReInflateFlags; 856 private final NotifRemoteViewCache mRemoteViewCache; 857 private final Executor mBgExecutor; 858 private ExpandableNotificationRow mRow; 859 private Exception mError; 860 private RemoteViews.InteractionHandler mRemoteViewClickHandler; 861 private CancellationSignal mCancellationSignal; 862 private final ConversationNotificationProcessor mConversationProcessor; 863 private final boolean mIsMediaInQS; 864 private final SmartReplyStateInflater mSmartRepliesInflater; 865 private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; 866 private final NotificationContentInflaterLogger mLogger; 867 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, NotifLayoutInflaterFactory notifLayoutInflaterFactory, NotificationContentInflaterLogger logger)868 private AsyncInflationTask( 869 Executor bgExecutor, 870 boolean inflateSynchronously, 871 @InflationFlag int reInflateFlags, 872 NotifRemoteViewCache cache, 873 NotificationEntry entry, 874 ConversationNotificationProcessor conversationProcessor, 875 ExpandableNotificationRow row, 876 boolean isLowPriority, 877 boolean usesIncreasedHeight, 878 boolean usesIncreasedHeadsUpHeight, 879 InflationCallback callback, 880 RemoteViews.InteractionHandler remoteViewClickHandler, 881 boolean isMediaFlagEnabled, 882 SmartReplyStateInflater smartRepliesInflater, 883 NotifLayoutInflaterFactory notifLayoutInflaterFactory, 884 NotificationContentInflaterLogger logger) { 885 mEntry = entry; 886 mRow = row; 887 mBgExecutor = bgExecutor; 888 mInflateSynchronously = inflateSynchronously; 889 mReInflateFlags = reInflateFlags; 890 mRemoteViewCache = cache; 891 mSmartRepliesInflater = smartRepliesInflater; 892 mContext = mRow.getContext(); 893 mIsLowPriority = isLowPriority; 894 mUsesIncreasedHeight = usesIncreasedHeight; 895 mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; 896 mRemoteViewClickHandler = remoteViewClickHandler; 897 mCallback = callback; 898 mConversationProcessor = conversationProcessor; 899 mIsMediaInQS = isMediaFlagEnabled; 900 mNotifLayoutInflaterFactory = notifLayoutInflaterFactory; 901 mLogger = logger; 902 entry.setInflationTask(this); 903 } 904 905 @VisibleForTesting 906 @InflationFlag getReInflateFlags()907 public int getReInflateFlags() { 908 return mReInflateFlags; 909 } 910 updateApplicationInfo(StatusBarNotification sbn)911 void updateApplicationInfo(StatusBarNotification sbn) { 912 String packageName = sbn.getPackageName(); 913 int userId = UserHandle.getUserId(sbn.getUid()); 914 final ApplicationInfo appInfo; 915 try { 916 // This method has an internal cache, so we don't need to add our own caching here. 917 appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName, 918 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 919 } catch (PackageManager.NameNotFoundException e) { 920 return; 921 } 922 Notification.addFieldsFromContext(appInfo, sbn.getNotification()); 923 } 924 925 @Override doInBackground(Void... params)926 protected InflationProgress doInBackground(Void... params) { 927 try { 928 final StatusBarNotification sbn = mEntry.getSbn(); 929 // Ensure the ApplicationInfo is updated before a builder is recovered. 930 updateApplicationInfo(sbn); 931 final Notification.Builder recoveredBuilder 932 = Notification.Builder.recoverBuilder(mContext, 933 sbn.getNotification()); 934 935 Context packageContext = sbn.getPackageContext(mContext); 936 if (recoveredBuilder.usesTemplate()) { 937 // For all of our templates, we want it to be RTL 938 packageContext = new RtlEnabledContext(packageContext); 939 } 940 if (mEntry.getRanking().isConversation()) { 941 mConversationProcessor.processNotification(mEntry, recoveredBuilder, mLogger); 942 } 943 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, 944 recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, 945 mUsesIncreasedHeadsUpHeight, packageContext, mRow, 946 mNotifLayoutInflaterFactory, mLogger); 947 mLogger.logAsyncTaskProgress(mEntry, 948 "getting existing smart reply state (on wrong thread!)"); 949 InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); 950 mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views"); 951 InflationProgress result = inflateSmartReplyViews( 952 inflationProgress, 953 mReInflateFlags, 954 mEntry, 955 mContext, 956 packageContext, 957 previousSmartReplyState, 958 mSmartRepliesInflater, 959 mLogger); 960 961 mLogger.logAsyncTaskProgress(mEntry, 962 "getting row image resolver (on wrong thread!)"); 963 final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); 964 // wait for image resolver to finish preloading 965 mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images"); 966 imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS); 967 968 return result; 969 } catch (Exception e) { 970 mError = e; 971 mLogger.logAsyncTaskException(mEntry, "inflating", e); 972 return null; 973 } 974 } 975 976 @Override onPostExecute(InflationProgress result)977 protected void onPostExecute(InflationProgress result) { 978 if (mError == null) { 979 // Logged in detail in apply. 980 mCancellationSignal = apply( 981 mBgExecutor, 982 mInflateSynchronously, 983 result, 984 mReInflateFlags, 985 mRemoteViewCache, 986 mEntry, 987 mRow, 988 mRemoteViewClickHandler, 989 this /* callback */, 990 mLogger); 991 } else { 992 handleError(mError); 993 } 994 } 995 handleError(Exception e)996 private void handleError(Exception e) { 997 mEntry.onInflationTaskFinished(); 998 StatusBarNotification sbn = mEntry.getSbn(); 999 final String ident = sbn.getPackageName() + "/0x" 1000 + Integer.toHexString(sbn.getId()); 1001 Log.e(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e); 1002 if (mCallback != null) { 1003 mCallback.handleInflationException(mRow.getEntry(), 1004 new InflationException("Couldn't inflate contentViews" + e)); 1005 } 1006 1007 // Cancel any image loading tasks, not useful any more 1008 mRow.getImageResolver().cancelRunningTasks(); 1009 } 1010 1011 @Override abort()1012 public void abort() { 1013 mLogger.logAsyncTaskProgress(mEntry, "cancelling inflate"); 1014 cancel(true /* mayInterruptIfRunning */); 1015 if (mCancellationSignal != null) { 1016 mLogger.logAsyncTaskProgress(mEntry, "cancelling apply"); 1017 mCancellationSignal.cancel(); 1018 } 1019 mLogger.logAsyncTaskProgress(mEntry, "aborted"); 1020 } 1021 1022 @Override handleInflationException(NotificationEntry entry, Exception e)1023 public void handleInflationException(NotificationEntry entry, Exception e) { 1024 handleError(e); 1025 } 1026 1027 @Override onAsyncInflationFinished(NotificationEntry entry)1028 public void onAsyncInflationFinished(NotificationEntry entry) { 1029 mEntry.onInflationTaskFinished(); 1030 mRow.onNotificationUpdated(); 1031 if (mCallback != null) { 1032 mCallback.onAsyncInflationFinished(mEntry); 1033 } 1034 1035 // Notify the resolver that the inflation task has finished, 1036 // try to purge unnecessary cached entries. 1037 mRow.getImageResolver().purgeCache(); 1038 1039 // Cancel any image loading tasks that have not completed at this point 1040 mRow.getImageResolver().cancelRunningTasks(); 1041 } 1042 1043 private static class RtlEnabledContext extends ContextWrapper { RtlEnabledContext(Context packageContext)1044 private RtlEnabledContext(Context packageContext) { 1045 super(packageContext); 1046 } 1047 1048 @Override getApplicationInfo()1049 public ApplicationInfo getApplicationInfo() { 1050 ApplicationInfo applicationInfo = new ApplicationInfo(super.getApplicationInfo()); 1051 applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; 1052 return applicationInfo; 1053 } 1054 } 1055 } 1056 1057 @VisibleForTesting 1058 static class InflationProgress { 1059 private RemoteViews newContentView; 1060 private RemoteViews newHeadsUpView; 1061 private RemoteViews newExpandedView; 1062 private RemoteViews newPublicView; 1063 1064 @VisibleForTesting 1065 Context packageContext; 1066 1067 private View inflatedContentView; 1068 private View inflatedHeadsUpView; 1069 private View inflatedExpandedView; 1070 private View inflatedPublicView; 1071 private CharSequence headsUpStatusBarText; 1072 private CharSequence headsUpStatusBarTextPublic; 1073 1074 private InflatedSmartReplyState inflatedSmartReplyState; 1075 private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; 1076 private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; 1077 } 1078 1079 @VisibleForTesting 1080 abstract static class ApplyCallback { setResultView(View v)1081 public abstract void setResultView(View v); getRemoteView()1082 public abstract RemoteViews getRemoteView(); 1083 } 1084 } 1085