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