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