1 /* 2 * Copyright (C) 2015 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.usb; 18 19 import android.annotation.NonNull; 20 import android.app.Notification; 21 import android.app.Notification.Action; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.MoveCallback; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.StrictMode; 33 import android.os.UserHandle; 34 import android.os.storage.DiskInfo; 35 import android.os.storage.StorageEventListener; 36 import android.os.storage.StorageManager; 37 import android.os.storage.VolumeInfo; 38 import android.os.storage.VolumeRecord; 39 import android.provider.Settings; 40 import android.text.TextUtils; 41 import android.text.format.DateUtils; 42 import android.util.Log; 43 import android.util.SparseArray; 44 45 import com.android.internal.R; 46 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 47 import com.android.systemui.CoreStartable; 48 import com.android.systemui.SystemUIApplication; 49 import com.android.systemui.broadcast.BroadcastDispatcher; 50 import com.android.systemui.dagger.SysUISingleton; 51 import com.android.systemui.util.NotificationChannels; 52 53 import java.util.List; 54 55 import javax.inject.Inject; 56 57 /** */ 58 @SysUISingleton 59 public class StorageNotification implements CoreStartable { 60 private static final String TAG = "StorageNotification"; 61 62 private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME"; 63 private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD"; 64 private final Context mContext; 65 private final BroadcastDispatcher mBroadcastDispatcher; 66 67 // TODO: delay some notifications to avoid bumpy fast operations 68 69 private final NotificationManager mNotificationManager; 70 private final StorageManager mStorageManager; 71 72 @Inject StorageNotification( Context context, BroadcastDispatcher broadcastDispatcher, NotificationManager notificationManager, StorageManager storageManager )73 public StorageNotification( 74 Context context, 75 BroadcastDispatcher broadcastDispatcher, 76 NotificationManager notificationManager, 77 StorageManager storageManager 78 ) { 79 mContext = context; 80 mBroadcastDispatcher = broadcastDispatcher; 81 mNotificationManager = notificationManager; 82 mStorageManager = storageManager; 83 } 84 85 private static class MoveInfo { 86 public int moveId; 87 public Bundle extras; 88 public String packageName; 89 public String label; 90 public String volumeUuid; 91 } 92 93 private final SparseArray<MoveInfo> mMoves = new SparseArray<>(); 94 95 private final StorageEventListener mListener = new StorageEventListener() { 96 @Override 97 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 98 onVolumeStateChangedInternal(vol); 99 } 100 101 @Override 102 public void onVolumeRecordChanged(VolumeRecord rec) { 103 // Avoid kicking notifications when getting early metadata before 104 // mounted. If already mounted, we're being kicked because of a 105 // nickname or init'ed change. 106 final VolumeInfo vol = mStorageManager.findVolumeByUuid(rec.getFsUuid()); 107 if (vol != null && vol.isMountedReadable()) { 108 onVolumeStateChangedInternal(vol); 109 } 110 } 111 112 @Override 113 public void onVolumeForgotten(String fsUuid) { 114 // Stop annoying the user 115 mNotificationManager.cancelAsUser(fsUuid, SystemMessage.NOTE_STORAGE_PRIVATE, 116 UserHandle.ALL); 117 } 118 119 @Override 120 public void onDiskScanned(DiskInfo disk, int volumeCount) { 121 onDiskScannedInternal(disk, volumeCount); 122 } 123 124 @Override 125 public void onDiskDestroyed(DiskInfo disk) { 126 onDiskDestroyedInternal(disk); 127 } 128 }; 129 130 private final BroadcastReceiver mSnoozeReceiver = new BroadcastReceiver() { 131 @Override 132 public void onReceive(Context context, Intent intent) { 133 // TODO: kick this onto background thread 134 final String fsUuid = intent.getStringExtra(VolumeRecord.EXTRA_FS_UUID); 135 mStorageManager.setVolumeSnoozed(fsUuid, true); 136 } 137 }; 138 139 private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() { 140 @Override 141 public void onReceive(Context context, Intent intent) { 142 // When finishing the adoption wizard, clean up any notifications 143 // for moving primary storage 144 mNotificationManager.cancelAsUser(null, SystemMessage.NOTE_STORAGE_MOVE, 145 UserHandle.ALL); 146 } 147 }; 148 149 private final MoveCallback mMoveCallback = new MoveCallback() { 150 @Override 151 public void onCreated(int moveId, Bundle extras) { 152 final MoveInfo move = new MoveInfo(); 153 move.moveId = moveId; 154 move.extras = extras; 155 if (extras != null) { 156 move.packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 157 move.label = extras.getString(Intent.EXTRA_TITLE); 158 move.volumeUuid = extras.getString(VolumeRecord.EXTRA_FS_UUID); 159 } 160 mMoves.put(moveId, move); 161 } 162 163 @Override 164 public void onStatusChanged(int moveId, int status, long estMillis) { 165 final MoveInfo move = mMoves.get(moveId); 166 if (move == null) { 167 Log.w(TAG, "Ignoring unknown move " + moveId); 168 return; 169 } 170 171 if (PackageManager.isMoveStatusFinished(status)) { 172 onMoveFinished(move, status); 173 } else { 174 onMoveProgress(move, status, estMillis); 175 } 176 } 177 }; 178 179 @Override start()180 public void start() { 181 mStorageManager.registerListener(mListener); 182 183 mBroadcastDispatcher.registerReceiver( 184 mSnoozeReceiver, 185 new IntentFilter(ACTION_SNOOZE_VOLUME), 186 null, 187 null, 188 Context.RECEIVER_EXPORTED_UNAUDITED, 189 android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); 190 mBroadcastDispatcher.registerReceiver( 191 mFinishReceiver, 192 new IntentFilter(ACTION_FINISH_WIZARD), 193 null, 194 null, 195 Context.RECEIVER_EXPORTED_UNAUDITED, 196 android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); 197 198 // Kick current state into place 199 final List<DiskInfo> disks = mStorageManager.getDisks(); 200 for (DiskInfo disk : disks) { 201 onDiskScannedInternal(disk, disk.volumeCount); 202 } 203 204 final List<VolumeInfo> vols = mStorageManager.getVolumes(); 205 for (VolumeInfo vol : vols) { 206 onVolumeStateChangedInternal(vol); 207 } 208 209 mContext.getPackageManager().registerMoveCallback(mMoveCallback, new Handler()); 210 211 updateMissingPrivateVolumes(); 212 } 213 updateMissingPrivateVolumes()214 private void updateMissingPrivateVolumes() { 215 if (isTv() || isAutomotive()) { 216 // On TV, TvSettings displays a modal full-screen activity in this case. 217 // Not applicable for automotive. 218 return; 219 } 220 221 final List<VolumeRecord> recs = mStorageManager.getVolumeRecords(); 222 for (VolumeRecord rec : recs) { 223 if (rec.getType() != VolumeInfo.TYPE_PRIVATE) continue; 224 225 final String fsUuid = rec.getFsUuid(); 226 final VolumeInfo info = mStorageManager.findVolumeByUuid(fsUuid); 227 if ((info != null && info.isMountedWritable()) || rec.isSnoozed()) { 228 // Yay, private volume is here, or user snoozed 229 mNotificationManager.cancelAsUser(fsUuid, SystemMessage.NOTE_STORAGE_PRIVATE, 230 UserHandle.ALL); 231 232 } else { 233 // Boo, annoy the user to reinsert the private volume 234 final CharSequence title = 235 mContext.getString(R.string.ext_media_missing_title, 236 rec.getNickname()); 237 final CharSequence text = 238 mContext.getString(R.string.ext_media_missing_message); 239 240 Notification.Builder builder = 241 new Notification.Builder(mContext, NotificationChannels.STORAGE) 242 .setSmallIcon(R.drawable.ic_sd_card_48dp) 243 .setColor(mContext.getColor( 244 R.color.system_notification_accent_color)) 245 .setContentTitle(title) 246 .setContentText(text) 247 .setContentIntent(buildForgetPendingIntent(rec)) 248 .setStyle(new Notification.BigTextStyle().bigText(text)) 249 .setVisibility(Notification.VISIBILITY_PUBLIC) 250 .setLocalOnly(true) 251 .setCategory(Notification.CATEGORY_SYSTEM) 252 .setDeleteIntent(buildSnoozeIntent(fsUuid)) 253 .extend(new Notification.TvExtender()); 254 SystemUIApplication.overrideNotificationAppName(mContext, builder, false); 255 256 mNotificationManager.notifyAsUser(fsUuid, SystemMessage.NOTE_STORAGE_PRIVATE, 257 builder.build(), UserHandle.ALL); 258 } 259 } 260 } 261 onDiskScannedInternal(DiskInfo disk, int volumeCount)262 private void onDiskScannedInternal(DiskInfo disk, int volumeCount) { 263 if (volumeCount == 0 && disk.size > 0) { 264 // No supported volumes found, give user option to format 265 final CharSequence title = mContext.getString( 266 R.string.ext_media_unsupported_notification_title, disk.getDescription()); 267 final CharSequence text = mContext.getString( 268 R.string.ext_media_unsupported_notification_message, disk.getDescription()); 269 270 Notification.Builder builder = 271 new Notification.Builder(mContext, NotificationChannels.STORAGE) 272 .setSmallIcon(getSmallIcon(disk, VolumeInfo.STATE_UNMOUNTABLE)) 273 .setColor(mContext.getColor(R.color.system_notification_accent_color)) 274 .setContentTitle(title) 275 .setContentText(text) 276 .setContentIntent(buildInitPendingIntent(disk)) 277 .setStyle(new Notification.BigTextStyle().bigText(text)) 278 .setVisibility(Notification.VISIBILITY_PUBLIC) 279 .setLocalOnly(true) 280 .setCategory(Notification.CATEGORY_ERROR) 281 .extend(new Notification.TvExtender()); 282 SystemUIApplication.overrideNotificationAppName(mContext, builder, false); 283 284 mNotificationManager.notifyAsUser(disk.getId(), SystemMessage.NOTE_STORAGE_DISK, 285 builder.build(), UserHandle.ALL); 286 287 } else { 288 // Yay, we have volumes! 289 mNotificationManager.cancelAsUser(disk.getId(), SystemMessage.NOTE_STORAGE_DISK, 290 UserHandle.ALL); 291 } 292 } 293 294 /** 295 * Remove all notifications for a disk when it goes away. 296 * 297 * @param disk The disk that went away. 298 */ onDiskDestroyedInternal(@onNull DiskInfo disk)299 private void onDiskDestroyedInternal(@NonNull DiskInfo disk) { 300 mNotificationManager.cancelAsUser(disk.getId(), SystemMessage.NOTE_STORAGE_DISK, 301 UserHandle.ALL); 302 } 303 onVolumeStateChangedInternal(VolumeInfo vol)304 private void onVolumeStateChangedInternal(VolumeInfo vol) { 305 switch (vol.getType()) { 306 case VolumeInfo.TYPE_PRIVATE: 307 onPrivateVolumeStateChangedInternal(vol); 308 break; 309 case VolumeInfo.TYPE_PUBLIC: 310 onPublicVolumeStateChangedInternal(vol); 311 break; 312 } 313 } 314 onPrivateVolumeStateChangedInternal(VolumeInfo vol)315 private void onPrivateVolumeStateChangedInternal(VolumeInfo vol) { 316 Log.d(TAG, "Notifying about private volume: " + vol.toString()); 317 318 updateMissingPrivateVolumes(); 319 } 320 onPublicVolumeStateChangedInternal(VolumeInfo vol)321 private void onPublicVolumeStateChangedInternal(VolumeInfo vol) { 322 Log.d(TAG, "Notifying about public volume: " + vol.toString()); 323 324 // Volume state change event may come from removed user, in this case, mountedUserId will 325 // equals to UserHandle.USER_NULL (-10000) which will do nothing when call cancelAsUser(), 326 // but cause crash when call notifyAsUser(). Here we return directly for USER_NULL, and 327 // leave all notifications belong to removed user to NotificationManagerService, the latter 328 // will remove all notifications of the removed user when handles user stopped broadcast. 329 if (vol.getMountUserId() == UserHandle.USER_NULL) { 330 Log.d(TAG, "Ignore public volume state change event of removed user"); 331 return; 332 } 333 334 final Notification notif; 335 switch (vol.getState()) { 336 case VolumeInfo.STATE_UNMOUNTED: 337 notif = onVolumeUnmounted(vol); 338 break; 339 case VolumeInfo.STATE_CHECKING: 340 notif = onVolumeChecking(vol); 341 break; 342 case VolumeInfo.STATE_MOUNTED: 343 case VolumeInfo.STATE_MOUNTED_READ_ONLY: 344 notif = onVolumeMounted(vol); 345 break; 346 case VolumeInfo.STATE_FORMATTING: 347 notif = onVolumeFormatting(vol); 348 break; 349 case VolumeInfo.STATE_EJECTING: 350 notif = onVolumeEjecting(vol); 351 break; 352 case VolumeInfo.STATE_UNMOUNTABLE: 353 notif = onVolumeUnmountable(vol); 354 break; 355 case VolumeInfo.STATE_REMOVED: 356 notif = onVolumeRemoved(vol); 357 break; 358 case VolumeInfo.STATE_BAD_REMOVAL: 359 notif = onVolumeBadRemoval(vol); 360 break; 361 default: 362 notif = null; 363 break; 364 } 365 366 if (notif != null) { 367 mNotificationManager.notifyAsUser(vol.getId(), SystemMessage.NOTE_STORAGE_PUBLIC, 368 notif, UserHandle.of(vol.getMountUserId())); 369 } else { 370 mNotificationManager.cancelAsUser(vol.getId(), SystemMessage.NOTE_STORAGE_PUBLIC, 371 UserHandle.of(vol.getMountUserId())); 372 } 373 } 374 onVolumeUnmounted(VolumeInfo vol)375 private Notification onVolumeUnmounted(VolumeInfo vol) { 376 // Ignored 377 return null; 378 } 379 onVolumeChecking(VolumeInfo vol)380 private Notification onVolumeChecking(VolumeInfo vol) { 381 final DiskInfo disk = vol.getDisk(); 382 final CharSequence title = mContext.getString( 383 R.string.ext_media_checking_notification_title, disk.getDescription()); 384 final CharSequence text = mContext.getString( 385 R.string.ext_media_checking_notification_message, disk.getDescription()); 386 387 return buildNotificationBuilder(vol, title, text) 388 .setCategory(Notification.CATEGORY_PROGRESS) 389 .setOngoing(true) 390 .build(); 391 } 392 onVolumeMounted(VolumeInfo vol)393 private Notification onVolumeMounted(VolumeInfo vol) { 394 final VolumeRecord rec = mStorageManager.findRecordByUuid(vol.getFsUuid()); 395 final DiskInfo disk = vol.getDisk(); 396 397 // Don't annoy when user dismissed in past. (But make sure the disk is adoptable; we 398 // used to allow snoozing non-adoptable disks too.) 399 if (rec == null || (rec.isSnoozed() && disk.isAdoptable())) { 400 return null; 401 } 402 if (disk.isAdoptable() && !rec.isInited() && rec.getType() != VolumeInfo.TYPE_PUBLIC 403 && rec.getType() != VolumeInfo.TYPE_PRIVATE) { 404 final CharSequence title = disk.getDescription(); 405 final CharSequence text = mContext.getString( 406 R.string.ext_media_new_notification_message, disk.getDescription()); 407 408 final PendingIntent initIntent = buildInitPendingIntent(vol); 409 final PendingIntent unmountIntent = buildUnmountPendingIntent(vol); 410 411 if (isAutomotive()) { 412 return buildNotificationBuilder(vol, title, text) 413 .setContentIntent(unmountIntent) 414 .setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())) 415 .build(); 416 } else { 417 return buildNotificationBuilder(vol, title, text) 418 .addAction(new Action(R.drawable.ic_settings_24dp, 419 mContext.getString(R.string.ext_media_init_action), initIntent)) 420 .addAction(new Action(R.drawable.ic_eject_24dp, 421 mContext.getString(R.string.ext_media_unmount_action), 422 unmountIntent)) 423 .setContentIntent(initIntent) 424 .setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())) 425 .build(); 426 } 427 } else { 428 final CharSequence title = disk.getDescription(); 429 final CharSequence text = mContext.getString( 430 R.string.ext_media_ready_notification_message, disk.getDescription()); 431 432 final PendingIntent browseIntent = buildBrowsePendingIntent(vol); 433 final Notification.Builder builder = buildNotificationBuilder(vol, title, text) 434 .addAction(new Action(R.drawable.ic_folder_24dp, 435 mContext.getString(R.string.ext_media_browse_action), 436 browseIntent)) 437 .addAction(new Action(R.drawable.ic_eject_24dp, 438 mContext.getString(R.string.ext_media_unmount_action), 439 buildUnmountPendingIntent(vol))) 440 .setContentIntent(browseIntent) 441 .setCategory(Notification.CATEGORY_SYSTEM); 442 // Non-adoptable disks can't be snoozed. 443 if (disk.isAdoptable()) { 444 builder.setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())); 445 } 446 447 return builder.build(); 448 } 449 } 450 onVolumeFormatting(VolumeInfo vol)451 private Notification onVolumeFormatting(VolumeInfo vol) { 452 // Ignored 453 return null; 454 } 455 onVolumeEjecting(VolumeInfo vol)456 private Notification onVolumeEjecting(VolumeInfo vol) { 457 final DiskInfo disk = vol.getDisk(); 458 final CharSequence title = mContext.getString( 459 R.string.ext_media_unmounting_notification_title, disk.getDescription()); 460 final CharSequence text = mContext.getString( 461 R.string.ext_media_unmounting_notification_message, disk.getDescription()); 462 463 return buildNotificationBuilder(vol, title, text) 464 .setCategory(Notification.CATEGORY_PROGRESS) 465 .setOngoing(true) 466 .build(); 467 } 468 onVolumeUnmountable(VolumeInfo vol)469 private Notification onVolumeUnmountable(VolumeInfo vol) { 470 final DiskInfo disk = vol.getDisk(); 471 final CharSequence title = mContext.getString( 472 R.string.ext_media_unmountable_notification_title, disk.getDescription()); 473 final CharSequence text = mContext.getString( 474 R.string.ext_media_unmountable_notification_message, disk.getDescription()); 475 PendingIntent action; 476 if (isAutomotive()) { 477 action = buildUnmountPendingIntent(vol); 478 } else { 479 action = buildInitPendingIntent(vol); 480 } 481 482 return buildNotificationBuilder(vol, title, text) 483 .setContentIntent(action) 484 .setCategory(Notification.CATEGORY_ERROR) 485 .build(); 486 } 487 onVolumeRemoved(VolumeInfo vol)488 private Notification onVolumeRemoved(VolumeInfo vol) { 489 if (!vol.isPrimary()) { 490 // Ignore non-primary media 491 return null; 492 } 493 494 final DiskInfo disk = vol.getDisk(); 495 final CharSequence title = mContext.getString( 496 R.string.ext_media_nomedia_notification_title, disk.getDescription()); 497 final CharSequence text = mContext.getString( 498 R.string.ext_media_nomedia_notification_message, disk.getDescription()); 499 500 return buildNotificationBuilder(vol, title, text) 501 .setCategory(Notification.CATEGORY_ERROR) 502 .build(); 503 } 504 onVolumeBadRemoval(VolumeInfo vol)505 private Notification onVolumeBadRemoval(VolumeInfo vol) { 506 if (!vol.isPrimary()) { 507 // Ignore non-primary media 508 return null; 509 } 510 511 final DiskInfo disk = vol.getDisk(); 512 final CharSequence title = mContext.getString( 513 R.string.ext_media_badremoval_notification_title, disk.getDescription()); 514 final CharSequence text = mContext.getString( 515 R.string.ext_media_badremoval_notification_message, disk.getDescription()); 516 517 return buildNotificationBuilder(vol, title, text) 518 .setCategory(Notification.CATEGORY_ERROR) 519 .build(); 520 } 521 onMoveProgress(MoveInfo move, int status, long estMillis)522 private void onMoveProgress(MoveInfo move, int status, long estMillis) { 523 final CharSequence title; 524 if (!TextUtils.isEmpty(move.label)) { 525 title = mContext.getString(R.string.ext_media_move_specific_title, move.label); 526 } else { 527 title = mContext.getString(R.string.ext_media_move_title); 528 } 529 530 final CharSequence text; 531 if (estMillis < 0) { 532 text = null; 533 } else { 534 text = DateUtils.formatDuration(estMillis); 535 } 536 537 final PendingIntent intent; 538 if (move.packageName != null) { 539 intent = buildWizardMovePendingIntent(move); 540 } else { 541 intent = buildWizardMigratePendingIntent(move); 542 } 543 544 Notification.Builder builder = 545 new Notification.Builder(mContext, NotificationChannels.STORAGE) 546 .setSmallIcon(R.drawable.ic_sd_card_48dp) 547 .setColor(mContext.getColor(R.color.system_notification_accent_color)) 548 .setContentTitle(title) 549 .setContentText(text) 550 .setContentIntent(intent) 551 .setStyle(new Notification.BigTextStyle().bigText(text)) 552 .setVisibility(Notification.VISIBILITY_PUBLIC) 553 .setLocalOnly(true) 554 .setCategory(Notification.CATEGORY_PROGRESS) 555 .setProgress(100, status, false) 556 .setOngoing(true); 557 SystemUIApplication.overrideNotificationAppName(mContext, builder, false); 558 559 mNotificationManager.notifyAsUser(move.packageName, SystemMessage.NOTE_STORAGE_MOVE, 560 builder.build(), UserHandle.ALL); 561 } 562 onMoveFinished(MoveInfo move, int status)563 private void onMoveFinished(MoveInfo move, int status) { 564 if (move.packageName != null) { 565 // We currently ignore finished app moves; just clear the last 566 // published progress 567 mNotificationManager.cancelAsUser(move.packageName, SystemMessage.NOTE_STORAGE_MOVE, 568 UserHandle.ALL); 569 return; 570 } 571 572 final VolumeInfo privateVol = mContext.getPackageManager().getPrimaryStorageCurrentVolume(); 573 final String descrip = mStorageManager.getBestVolumeDescription(privateVol); 574 575 final CharSequence title; 576 final CharSequence text; 577 if (status == PackageManager.MOVE_SUCCEEDED) { 578 title = mContext.getString(R.string.ext_media_move_success_title); 579 text = mContext.getString(R.string.ext_media_move_success_message, descrip); 580 } else { 581 title = mContext.getString(R.string.ext_media_move_failure_title); 582 text = mContext.getString(R.string.ext_media_move_failure_message); 583 } 584 585 // Jump back into the wizard flow if we moved to a real disk 586 final PendingIntent intent; 587 if (privateVol != null && privateVol.getDisk() != null) { 588 intent = buildWizardReadyPendingIntent(privateVol.getDisk()); 589 } else if (privateVol != null) { 590 intent = buildVolumeSettingsPendingIntent(privateVol); 591 } else { 592 intent = null; 593 } 594 595 Notification.Builder builder = 596 new Notification.Builder(mContext, NotificationChannels.STORAGE) 597 .setSmallIcon(R.drawable.ic_sd_card_48dp) 598 .setColor(mContext.getColor(R.color.system_notification_accent_color)) 599 .setContentTitle(title) 600 .setContentText(text) 601 .setContentIntent(intent) 602 .setStyle(new Notification.BigTextStyle().bigText(text)) 603 .setVisibility(Notification.VISIBILITY_PUBLIC) 604 .setLocalOnly(true) 605 .setCategory(Notification.CATEGORY_SYSTEM) 606 .setAutoCancel(true); 607 SystemUIApplication.overrideNotificationAppName(mContext, builder, false); 608 609 mNotificationManager.notifyAsUser(move.packageName, SystemMessage.NOTE_STORAGE_MOVE, 610 builder.build(), UserHandle.ALL); 611 } 612 getSmallIcon(DiskInfo disk, int state)613 private int getSmallIcon(DiskInfo disk, int state) { 614 if (disk.isSd()) { 615 switch (state) { 616 case VolumeInfo.STATE_CHECKING: 617 case VolumeInfo.STATE_EJECTING: 618 return R.drawable.ic_sd_card_48dp; 619 default: 620 return R.drawable.ic_sd_card_48dp; 621 } 622 } else if (disk.isUsb()) { 623 return R.drawable.ic_usb_48dp; 624 } else { 625 return R.drawable.ic_sd_card_48dp; 626 } 627 } 628 buildNotificationBuilder(VolumeInfo vol, CharSequence title, CharSequence text)629 private Notification.Builder buildNotificationBuilder(VolumeInfo vol, CharSequence title, 630 CharSequence text) { 631 Notification.Builder builder = 632 new Notification.Builder(mContext, NotificationChannels.STORAGE) 633 .setSmallIcon(getSmallIcon(vol.getDisk(), vol.getState())) 634 .setColor(mContext.getColor(R.color.system_notification_accent_color)) 635 .setContentTitle(title) 636 .setContentText(text) 637 .setStyle(new Notification.BigTextStyle().bigText(text)) 638 .setVisibility(Notification.VISIBILITY_PUBLIC) 639 .setLocalOnly(true) 640 .extend(new Notification.TvExtender()); 641 SystemUIApplication.overrideNotificationAppName(mContext, builder, false); 642 return builder; 643 } 644 buildInitPendingIntent(DiskInfo disk)645 private PendingIntent buildInitPendingIntent(DiskInfo disk) { 646 final Intent intent = new Intent(); 647 if (isTv()) { 648 intent.setPackage("com.android.tv.settings"); 649 intent.setAction("com.android.tv.settings.action.NEW_STORAGE"); 650 } else if (isAutomotive()) { 651 // TODO(b/151671685): add intent to handle unsupported usb 652 return null; 653 } else { 654 intent.setClassName("com.android.settings", 655 "com.android.settings.deviceinfo.StorageWizardInit"); 656 } 657 intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId()); 658 659 final int requestKey = disk.getId().hashCode(); 660 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 661 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 662 null, UserHandle.CURRENT); 663 } 664 buildInitPendingIntent(VolumeInfo vol)665 private PendingIntent buildInitPendingIntent(VolumeInfo vol) { 666 final Intent intent = new Intent(); 667 if (isTv()) { 668 intent.setPackage("com.android.tv.settings"); 669 intent.setAction("com.android.tv.settings.action.NEW_STORAGE"); 670 } else if (isAutomotive()) { 671 // TODO(b/151671685): add intent to handle unmountable usb 672 return null; 673 } else { 674 intent.setClassName("com.android.settings", 675 "com.android.settings.deviceinfo.StorageWizardInit"); 676 } 677 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 678 679 final int requestKey = vol.getId().hashCode(); 680 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 681 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 682 null, UserHandle.CURRENT); 683 } 684 buildUnmountPendingIntent(VolumeInfo vol)685 private PendingIntent buildUnmountPendingIntent(VolumeInfo vol) { 686 final Intent intent = new Intent(); 687 if (isTv()) { 688 intent.setPackage("com.android.tv.settings"); 689 intent.setAction("com.android.tv.settings.action.UNMOUNT_STORAGE"); 690 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 691 692 final int requestKey = vol.getId().hashCode(); 693 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 694 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 695 null, UserHandle.CURRENT); 696 } else if (isAutomotive()) { 697 intent.setClassName("com.android.car.settings", 698 "com.android.car.settings.storage.StorageUnmountReceiver"); 699 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 700 701 final int requestKey = vol.getId().hashCode(); 702 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 703 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 704 UserHandle.CURRENT); 705 } else { 706 intent.setClassName("com.android.settings", 707 "com.android.settings.deviceinfo.StorageUnmountReceiver"); 708 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 709 710 final int requestKey = vol.getId().hashCode(); 711 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 712 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 713 UserHandle.CURRENT); 714 } 715 } 716 buildBrowsePendingIntent(VolumeInfo vol)717 private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) { 718 final StrictMode.VmPolicy oldPolicy = StrictMode.allowVmViolations(); 719 try { 720 final Intent intent = vol.buildBrowseIntentForUser(vol.getMountUserId()); 721 722 final int requestKey = vol.getId().hashCode(); 723 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 724 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 725 null, UserHandle.CURRENT); 726 } finally { 727 StrictMode.setVmPolicy(oldPolicy); 728 } 729 } 730 buildVolumeSettingsPendingIntent(VolumeInfo vol)731 private PendingIntent buildVolumeSettingsPendingIntent(VolumeInfo vol) { 732 final Intent intent = new Intent(); 733 if (isTv()) { 734 intent.setPackage("com.android.tv.settings"); 735 intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 736 } else if (isAutomotive()) { 737 // TODO(b/151671685): add volume settings intent for automotive 738 return null; 739 } else { 740 switch (vol.getType()) { 741 case VolumeInfo.TYPE_PRIVATE: 742 intent.setClassName("com.android.settings", 743 "com.android.settings.Settings$PrivateVolumeSettingsActivity"); 744 break; 745 case VolumeInfo.TYPE_PUBLIC: 746 intent.setClassName("com.android.settings", 747 "com.android.settings.Settings$PublicVolumeSettingsActivity"); 748 break; 749 default: 750 return null; 751 } 752 } 753 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 754 755 final int requestKey = vol.getId().hashCode(); 756 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 757 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 758 null, UserHandle.CURRENT); 759 } 760 buildSnoozeIntent(String fsUuid)761 private PendingIntent buildSnoozeIntent(String fsUuid) { 762 final Intent intent = new Intent(ACTION_SNOOZE_VOLUME); 763 intent.putExtra(VolumeRecord.EXTRA_FS_UUID, fsUuid); 764 765 final int requestKey = fsUuid.hashCode(); 766 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 767 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 768 UserHandle.CURRENT); 769 } 770 buildForgetPendingIntent(VolumeRecord rec)771 private PendingIntent buildForgetPendingIntent(VolumeRecord rec) { 772 // Not used on TV and Automotive 773 final Intent intent = new Intent(); 774 intent.setClassName("com.android.settings", 775 "com.android.settings.Settings$PrivateVolumeForgetActivity"); 776 intent.putExtra(VolumeRecord.EXTRA_FS_UUID, rec.getFsUuid()); 777 778 final int requestKey = rec.getFsUuid().hashCode(); 779 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 780 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 781 null, UserHandle.CURRENT); 782 } 783 buildWizardMigratePendingIntent(MoveInfo move)784 private PendingIntent buildWizardMigratePendingIntent(MoveInfo move) { 785 final Intent intent = new Intent(); 786 if (isTv()) { 787 intent.setPackage("com.android.tv.settings"); 788 intent.setAction("com.android.tv.settings.action.MIGRATE_STORAGE"); 789 } else if (isAutomotive()) { 790 // TODO(b/151671685): add storage migrate intent for automotive 791 return null; 792 } else { 793 intent.setClassName("com.android.settings", 794 "com.android.settings.deviceinfo.StorageWizardMigrateProgress"); 795 } 796 intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); 797 798 final VolumeInfo vol = mStorageManager.findVolumeByQualifiedUuid(move.volumeUuid); 799 if (vol != null) { 800 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 801 } 802 return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, 803 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 804 null, UserHandle.CURRENT); 805 } 806 buildWizardMovePendingIntent(MoveInfo move)807 private PendingIntent buildWizardMovePendingIntent(MoveInfo move) { 808 final Intent intent = new Intent(); 809 if (isTv()) { 810 intent.setPackage("com.android.tv.settings"); 811 intent.setAction("com.android.tv.settings.action.MOVE_APP"); 812 } else if (isAutomotive()) { 813 // TODO(b/151671685): add storage move intent for automotive 814 return null; 815 } else { 816 intent.setClassName("com.android.settings", 817 "com.android.settings.deviceinfo.StorageWizardMoveProgress"); 818 } 819 intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); 820 821 return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, 822 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 823 null, UserHandle.CURRENT); 824 } 825 buildWizardReadyPendingIntent(DiskInfo disk)826 private PendingIntent buildWizardReadyPendingIntent(DiskInfo disk) { 827 final Intent intent = new Intent(); 828 if (isTv()) { 829 intent.setPackage("com.android.tv.settings"); 830 intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 831 } else if (isAutomotive()) { 832 // TODO(b/151671685): add storage ready intent for automotive 833 return null; 834 } else { 835 intent.setClassName("com.android.settings", 836 "com.android.settings.deviceinfo.StorageWizardReady"); 837 } 838 intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId()); 839 840 final int requestKey = disk.getId().hashCode(); 841 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 842 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 843 null, UserHandle.CURRENT); 844 } 845 isAutomotive()846 private boolean isAutomotive() { 847 PackageManager packageManager = mContext.getPackageManager(); 848 return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 849 } 850 isTv()851 private boolean isTv() { 852 PackageManager packageManager = mContext.getPackageManager(); 853 return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK); 854 } 855 } 856