NotificationInflater.java revision 0f66a4cc16ec1a927c90ac559c73c80ddcb5ee71
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 17package com.android.systemui.statusbar.notification; 18 19import android.annotation.Nullable; 20import android.app.Notification; 21import android.content.Context; 22import android.os.AsyncTask; 23import android.os.CancellationSignal; 24import android.service.notification.StatusBarNotification; 25import android.util.Log; 26import android.view.View; 27import android.widget.RemoteViews; 28 29import com.android.internal.annotations.VisibleForTesting; 30import com.android.systemui.statusbar.Abortable; 31import com.android.systemui.statusbar.ExpandableNotificationRow; 32import com.android.systemui.statusbar.NotificationContentView; 33import com.android.systemui.statusbar.NotificationData; 34import com.android.systemui.statusbar.phone.StatusBar; 35import com.android.systemui.util.Assert; 36 37import java.util.HashMap; 38 39/** 40 * A utility that inflates the right kind of contentView based on the state 41 */ 42public class NotificationInflater { 43 44 @VisibleForTesting 45 static final int FLAG_REINFLATE_ALL = ~0; 46 private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0; 47 @VisibleForTesting 48 static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1; 49 private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2; 50 private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3; 51 private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4; 52 53 private final ExpandableNotificationRow mRow; 54 private boolean mIsLowPriority; 55 private boolean mUsesIncreasedHeight; 56 private boolean mUsesIncreasedHeadsUpHeight; 57 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 58 private boolean mIsChildInGroup; 59 private InflationCallback mCallback; 60 private boolean mRedactAmbient; 61 62 public NotificationInflater(ExpandableNotificationRow row) { 63 mRow = row; 64 } 65 66 public void setIsLowPriority(boolean isLowPriority) { 67 mIsLowPriority = isLowPriority; 68 } 69 70 /** 71 * Set whether the notification is a child in a group 72 * 73 * @return whether the view was re-inflated 74 */ 75 public void setIsChildInGroup(boolean childInGroup) { 76 if (childInGroup != mIsChildInGroup) { 77 mIsChildInGroup = childInGroup; 78 if (mIsLowPriority) { 79 int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW; 80 inflateNotificationViews(flags); 81 } 82 } ; 83 } 84 85 public void setUsesIncreasedHeight(boolean usesIncreasedHeight) { 86 mUsesIncreasedHeight = usesIncreasedHeight; 87 } 88 89 public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) { 90 mUsesIncreasedHeadsUpHeight = usesIncreasedHeight; 91 } 92 93 public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) { 94 mRemoteViewClickHandler = remoteViewClickHandler; 95 } 96 97 public void setRedactAmbient(boolean redactAmbient) { 98 if (mRedactAmbient != redactAmbient) { 99 mRedactAmbient = redactAmbient; 100 if (mRow.getEntry() == null) { 101 return; 102 } 103 inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW); 104 } 105 } 106 107 /** 108 * Inflate all views of this notification on a background thread. This is asynchronous and will 109 * notify the callback once it's finished. 110 */ 111 public void inflateNotificationViews() { 112 inflateNotificationViews(FLAG_REINFLATE_ALL); 113 } 114 115 /** 116 * Reinflate all views for the specified flags on a background thread. This is asynchronous and 117 * will notify the callback once it's finished. 118 * 119 * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL} 120 * to reinflate all of views. 121 */ 122 @VisibleForTesting 123 void inflateNotificationViews(int reInflateFlags) { 124 StatusBarNotification sbn = mRow.getEntry().notification; 125 new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority, 126 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, 127 mCallback, mRemoteViewClickHandler).execute(); 128 } 129 130 @VisibleForTesting 131 InflationProgress inflateNotificationViews(int reInflateFlags, 132 Notification.Builder builder, Context packageContext) { 133 InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority, 134 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, 135 mRedactAmbient, packageContext); 136 apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null); 137 return result; 138 } 139 140 private static InflationProgress createRemoteViews(int reInflateFlags, 141 Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, 142 boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, 143 Context packageContext) { 144 InflationProgress result = new InflationProgress(); 145 isLowPriority = isLowPriority && !isChildInGroup; 146 if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { 147 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); 148 } 149 150 if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { 151 result.newExpandedView = createExpandedView(builder, isLowPriority); 152 } 153 154 if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { 155 result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); 156 } 157 158 if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { 159 result.newPublicView = builder.makePublicContentView(); 160 } 161 162 if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { 163 result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification() 164 : builder.makeAmbientNotification(); 165 } 166 result.packageContext = packageContext; 167 return result; 168 } 169 170 public static CancellationSignal apply(InflationProgress result, int reInflateFlags, 171 ExpandableNotificationRow row, boolean redactAmbient, 172 RemoteViews.OnClickHandler remoteViewClickHandler, 173 @Nullable InflationCallback callback) { 174 NotificationData.Entry entry = row.getEntry(); 175 NotificationContentView privateLayout = row.getPrivateLayout(); 176 NotificationContentView publicLayout = row.getPublicLayout(); 177 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 178 179 int flag = FLAG_REINFLATE_CONTENT_VIEW; 180 if ((reInflateFlags & flag) != 0) { 181 boolean isNewView = !compareRemoteViews(result.newContentView, entry.cachedContentView); 182 ApplyCallback applyCallback = new ApplyCallback() { 183 @Override 184 public void setResultView(View v) { 185 result.inflatedContentView = v; 186 } 187 188 @Override 189 public RemoteViews getRemoteView() { 190 return result.newContentView; 191 } 192 }; 193 applyRemoteView(result, reInflateFlags, flag, row, redactAmbient, 194 isNewView, remoteViewClickHandler, callback, entry, privateLayout, 195 privateLayout.getContractedChild(), 196 runningInflations, applyCallback); 197 } 198 199 flag = FLAG_REINFLATE_EXPANDED_VIEW; 200 if ((reInflateFlags & flag) != 0) { 201 if (result.newExpandedView != null) { 202 boolean isNewView = !compareRemoteViews(result.newExpandedView, 203 entry.cachedBigContentView); 204 ApplyCallback applyCallback = new ApplyCallback() { 205 @Override 206 public void setResultView(View v) { 207 result.inflatedExpandedView = v; 208 } 209 210 @Override 211 public RemoteViews getRemoteView() { 212 return result.newExpandedView; 213 } 214 }; 215 applyRemoteView(result, reInflateFlags, flag, row, 216 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 217 privateLayout, privateLayout.getExpandedChild(), runningInflations, 218 applyCallback); 219 } 220 } 221 222 flag = FLAG_REINFLATE_HEADS_UP_VIEW; 223 if ((reInflateFlags & flag) != 0) { 224 if (result.newHeadsUpView != null) { 225 boolean isNewView = !compareRemoteViews(result.newHeadsUpView, 226 entry.cachedHeadsUpContentView); 227 ApplyCallback applyCallback = new ApplyCallback() { 228 @Override 229 public void setResultView(View v) { 230 result.inflatedHeadsUpView = v; 231 } 232 233 @Override 234 public RemoteViews getRemoteView() { 235 return result.newHeadsUpView; 236 } 237 }; 238 applyRemoteView(result, reInflateFlags, flag, row, 239 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 240 privateLayout, privateLayout.getHeadsUpChild(), runningInflations, 241 applyCallback); 242 } 243 } 244 245 flag = FLAG_REINFLATE_PUBLIC_VIEW; 246 if ((reInflateFlags & flag) != 0) { 247 boolean isNewView = !compareRemoteViews(result.newPublicView, 248 entry.cachedPublicContentView); 249 ApplyCallback applyCallback = new ApplyCallback() { 250 @Override 251 public void setResultView(View v) { 252 result.inflatedPublicView = v; 253 } 254 255 @Override 256 public RemoteViews getRemoteView() { 257 return result.newPublicView; 258 } 259 }; 260 applyRemoteView(result, reInflateFlags, flag, row, 261 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 262 publicLayout, publicLayout.getContractedChild(), runningInflations, 263 applyCallback); 264 } 265 266 flag = FLAG_REINFLATE_AMBIENT_VIEW; 267 if ((reInflateFlags & flag) != 0) { 268 NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout; 269 boolean isNewView = !canReapplyAmbient(row, redactAmbient) || 270 !compareRemoteViews(result.newAmbientView, entry.cachedAmbientContentView); 271 ApplyCallback applyCallback = new ApplyCallback() { 272 @Override 273 public void setResultView(View v) { 274 result.inflatedAmbientView = v; 275 } 276 277 @Override 278 public RemoteViews getRemoteView() { 279 return result.newAmbientView; 280 } 281 }; 282 applyRemoteView(result, reInflateFlags, flag, row, 283 redactAmbient, isNewView, remoteViewClickHandler, callback, entry, 284 newParent, newParent.getAmbientChild(), runningInflations, 285 applyCallback); 286 } 287 288 // Let's try to finish, maybe nobody is even inflating anything 289 finishIfDone(result, reInflateFlags, runningInflations, callback, row, 290 redactAmbient); 291 CancellationSignal cancellationSignal = new CancellationSignal(); 292 cancellationSignal.setOnCancelListener( 293 () -> runningInflations.values().forEach(CancellationSignal::cancel)); 294 return cancellationSignal; 295 } 296 297 private static void applyRemoteView(final InflationProgress result, 298 final int reInflateFlags, int inflationId, 299 final ExpandableNotificationRow row, 300 final boolean redactAmbient, boolean isNewView, 301 RemoteViews.OnClickHandler remoteViewClickHandler, 302 @Nullable final InflationCallback callback, NotificationData.Entry entry, 303 NotificationContentView parentLayout, View existingView, 304 final HashMap<Integer, CancellationSignal> runningInflations, 305 ApplyCallback applyCallback) { 306 RemoteViews.OnViewAppliedListener listener 307 = new RemoteViews.OnViewAppliedListener() { 308 309 @Override 310 public void onViewApplied(View v) { 311 if (isNewView) { 312 v.setIsRootNamespace(true); 313 applyCallback.setResultView(v); 314 } 315 runningInflations.remove(inflationId); 316 finishIfDone(result, reInflateFlags, runningInflations, callback, row, 317 redactAmbient); 318 } 319 320 @Override 321 public void onError(Exception e) { 322 runningInflations.remove(inflationId); 323 handleInflationError(runningInflations, e, entry.notification, callback); 324 } 325 }; 326 CancellationSignal cancellationSignal; 327 RemoteViews newContentView = applyCallback.getRemoteView(); 328 if (isNewView) { 329 cancellationSignal = newContentView.applyAsync( 330 result.packageContext, 331 parentLayout, 332 null /* executor */, 333 listener, 334 remoteViewClickHandler); 335 } else { 336 cancellationSignal = newContentView.reapplyAsync( 337 result.packageContext, 338 existingView, 339 null /* executor */, 340 listener, 341 remoteViewClickHandler); 342 } 343 runningInflations.put(inflationId, cancellationSignal); 344 } 345 346 private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, 347 Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) { 348 Assert.isMainThread(); 349 runningInflations.values().forEach(CancellationSignal::cancel); 350 if (callback != null) { 351 callback.handleInflationException(notification, e); 352 } 353 } 354 355 /** 356 * Finish the inflation of the views 357 * 358 * @return true if the inflation was finished 359 */ 360 private static boolean finishIfDone(InflationProgress result, int reInflateFlags, 361 HashMap<Integer, CancellationSignal> runningInflations, 362 @Nullable InflationCallback endListener, ExpandableNotificationRow row, 363 boolean redactAmbient) { 364 Assert.isMainThread(); 365 NotificationData.Entry entry = row.getEntry(); 366 NotificationContentView privateLayout = row.getPrivateLayout(); 367 NotificationContentView publicLayout = row.getPublicLayout(); 368 if (runningInflations.isEmpty()) { 369 if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { 370 if (result.inflatedContentView != null) { 371 privateLayout.setContractedChild(result.inflatedContentView); 372 } 373 entry.cachedContentView = result.newContentView; 374 } 375 376 if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { 377 if (result.inflatedExpandedView != null) { 378 privateLayout.setExpandedChild(result.inflatedExpandedView); 379 } else if (result.newExpandedView == null) { 380 privateLayout.setExpandedChild(null); 381 } 382 entry.cachedBigContentView = result.newExpandedView; 383 row.setExpandable(result.newExpandedView != null); 384 } 385 386 if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { 387 if (result.inflatedHeadsUpView != null) { 388 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 389 } else if (result.newHeadsUpView == null) { 390 privateLayout.setHeadsUpChild(null); 391 } 392 entry.cachedHeadsUpContentView = result.newHeadsUpView; 393 } 394 395 if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { 396 if (result.inflatedPublicView != null) { 397 publicLayout.setContractedChild(result.inflatedPublicView); 398 } 399 entry.cachedPublicContentView = result.newPublicView; 400 } 401 402 if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { 403 if (result.inflatedAmbientView != null) { 404 NotificationContentView newParent = redactAmbient 405 ? publicLayout : privateLayout; 406 NotificationContentView otherParent = !redactAmbient 407 ? publicLayout : privateLayout; 408 newParent.setAmbientChild(result.inflatedAmbientView); 409 otherParent.setAmbientChild(null); 410 } 411 entry.cachedAmbientContentView = result.newAmbientView; 412 } 413 if (endListener != null) { 414 endListener.onAsyncInflationFinished(row.getEntry()); 415 } 416 return true; 417 } 418 return false; 419 } 420 421 private static RemoteViews createExpandedView(Notification.Builder builder, 422 boolean isLowPriority) { 423 RemoteViews bigContentView = builder.createBigContentView(); 424 if (bigContentView != null) { 425 return bigContentView; 426 } 427 if (isLowPriority) { 428 RemoteViews contentView = builder.createContentView(); 429 Notification.Builder.makeHeaderExpanded(contentView); 430 return contentView; 431 } 432 return null; 433 } 434 435 private static RemoteViews createContentView(Notification.Builder builder, 436 boolean isLowPriority, boolean useLarge) { 437 if (isLowPriority) { 438 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 439 } 440 return builder.createContentView(useLarge); 441 } 442 443 // Returns true if the RemoteViews are the same. 444 private static boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) { 445 return (a == null && b == null) || 446 (a != null && b != null 447 && b.getPackage() != null 448 && a.getPackage() != null 449 && a.getPackage().equals(b.getPackage()) 450 && a.getLayoutId() == b.getLayoutId()); 451 } 452 453 public void setInflationCallback(InflationCallback callback) { 454 mCallback = callback; 455 } 456 457 public interface InflationCallback { 458 void handleInflationException(StatusBarNotification notification, Exception e); 459 void onAsyncInflationFinished(NotificationData.Entry entry); 460 } 461 462 public void onDensityOrFontScaleChanged() { 463 NotificationData.Entry entry = mRow.getEntry(); 464 entry.cachedAmbientContentView = null; 465 entry.cachedBigContentView = null; 466 entry.cachedContentView = null; 467 entry.cachedHeadsUpContentView = null; 468 entry.cachedPublicContentView = null; 469 inflateNotificationViews(); 470 } 471 472 private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) { 473 NotificationContentView ambientView = redactAmbient ? row.getPublicLayout() 474 : row.getPrivateLayout(); ; 475 return ambientView.getAmbientChild() != null; 476 } 477 478 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 479 implements InflationCallback, Abortable { 480 481 private final StatusBarNotification mSbn; 482 private final Context mContext; 483 private final int mReInflateFlags; 484 private final boolean mIsLowPriority; 485 private final boolean mIsChildInGroup; 486 private final boolean mUsesIncreasedHeight; 487 private final InflationCallback mCallback; 488 private final boolean mUsesIncreasedHeadsUpHeight; 489 private final boolean mRedactAmbient; 490 private ExpandableNotificationRow mRow; 491 private Exception mError; 492 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 493 private CancellationSignal mCancellationSignal; 494 495 private AsyncInflationTask(StatusBarNotification notification, 496 int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, 497 boolean isChildInGroup, boolean usesIncreasedHeight, 498 boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, 499 InflationCallback callback, 500 RemoteViews.OnClickHandler remoteViewClickHandler) { 501 mRow = row; 502 NotificationData.Entry entry = row.getEntry(); 503 entry.setInflationTask(this); 504 mSbn = notification; 505 mReInflateFlags = reInflateFlags; 506 mContext = mRow.getContext(); 507 mIsLowPriority = isLowPriority; 508 mIsChildInGroup = isChildInGroup; 509 mUsesIncreasedHeight = usesIncreasedHeight; 510 mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; 511 mRedactAmbient = redactAmbient; 512 mRemoteViewClickHandler = remoteViewClickHandler; 513 mCallback = callback; 514 } 515 516 @Override 517 protected InflationProgress doInBackground(Void... params) { 518 try { 519 final Notification.Builder recoveredBuilder 520 = Notification.Builder.recoverBuilder(mContext, 521 mSbn.getNotification()); 522 Context packageContext = mSbn.getPackageContext(mContext); 523 Notification notification = mSbn.getNotification(); 524 if (notification.isMediaNotification()) { 525 MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, 526 packageContext); 527 processor.setIsLowPriority(mIsLowPriority); 528 processor.processNotification(notification, recoveredBuilder); 529 } 530 return createRemoteViews(mReInflateFlags, 531 recoveredBuilder, mIsLowPriority, mIsChildInGroup, 532 mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, 533 packageContext); 534 } catch (Exception e) { 535 mError = e; 536 return null; 537 } 538 } 539 540 @Override 541 protected void onPostExecute(InflationProgress result) { 542 if (mError == null) { 543 mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient, 544 mRemoteViewClickHandler, this); 545 } else { 546 handleError(mError); 547 } 548 } 549 550 private void handleError(Exception e) { 551 mRow.getEntry().onInflationTaskFinished(); 552 StatusBarNotification sbn = mRow.getStatusBarNotification(); 553 final String ident = sbn.getPackageName() + "/0x" 554 + Integer.toHexString(sbn.getId()); 555 Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); 556 mCallback.handleInflationException(sbn, 557 new InflationException("Couldn't inflate contentViews" + e)); 558 } 559 560 @Override 561 public void abort() { 562 cancel(true /* mayInterruptIfRunning */); 563 if (mCancellationSignal != null) { 564 mCancellationSignal.cancel(); 565 } 566 } 567 568 @Override 569 public void handleInflationException(StatusBarNotification notification, Exception e) { 570 handleError(e); 571 } 572 573 @Override 574 public void onAsyncInflationFinished(NotificationData.Entry entry) { 575 mRow.getEntry().onInflationTaskFinished(); 576 mRow.onNotificationUpdated(); 577 mCallback.onAsyncInflationFinished(mRow.getEntry()); 578 } 579 } 580 581 private static class InflationProgress { 582 private RemoteViews newContentView; 583 private RemoteViews newHeadsUpView; 584 private RemoteViews newExpandedView; 585 private RemoteViews newAmbientView; 586 private RemoteViews newPublicView; 587 588 private Context packageContext; 589 590 private View inflatedContentView; 591 private View inflatedHeadsUpView; 592 private View inflatedExpandedView; 593 private View inflatedAmbientView; 594 private View inflatedPublicView; 595 } 596 597 private abstract static class ApplyCallback { 598 public abstract void setResultView(View v); 599 public abstract RemoteViews getRemoteView(); 600 } 601} 602