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 (isAutomotive() && 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, null, UserHandle.CURRENT); 635 } 636 buildInitPendingIntent(VolumeInfo vol)637 private PendingIntent buildInitPendingIntent(VolumeInfo vol) { 638 final Intent intent = new Intent(); 639 if (isTv()) { 640 intent.setPackage("com.android.tv.settings"); 641 intent.setAction("com.android.tv.settings.action.NEW_STORAGE"); 642 } else if (isAutomotive()) { 643 // TODO(b/151671685): add intent to handle unmountable usb 644 return null; 645 } else { 646 intent.setClassName("com.android.settings", 647 "com.android.settings.deviceinfo.StorageWizardInit"); 648 } 649 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 650 651 final int requestKey = vol.getId().hashCode(); 652 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 653 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 654 } 655 buildUnmountPendingIntent(VolumeInfo vol)656 private PendingIntent buildUnmountPendingIntent(VolumeInfo vol) { 657 final Intent intent = new Intent(); 658 if (isTv()) { 659 intent.setPackage("com.android.tv.settings"); 660 intent.setAction("com.android.tv.settings.action.UNMOUNT_STORAGE"); 661 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 662 663 final int requestKey = vol.getId().hashCode(); 664 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 665 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 666 } else if (isAutomotive()) { 667 intent.setClassName("com.android.car.settings", 668 "com.android.car.settings.storage.StorageUnmountReceiver"); 669 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 670 671 final int requestKey = vol.getId().hashCode(); 672 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 673 PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT); 674 } else { 675 intent.setClassName("com.android.settings", 676 "com.android.settings.deviceinfo.StorageUnmountReceiver"); 677 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 678 679 final int requestKey = vol.getId().hashCode(); 680 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 681 PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT); 682 } 683 } 684 buildBrowsePendingIntent(VolumeInfo vol)685 private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) { 686 final StrictMode.VmPolicy oldPolicy = StrictMode.allowVmViolations(); 687 try { 688 final Intent intent = vol.buildBrowseIntentForUser(vol.getMountUserId()); 689 690 final int requestKey = vol.getId().hashCode(); 691 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 692 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 693 } finally { 694 StrictMode.setVmPolicy(oldPolicy); 695 } 696 } 697 buildVolumeSettingsPendingIntent(VolumeInfo vol)698 private PendingIntent buildVolumeSettingsPendingIntent(VolumeInfo vol) { 699 final Intent intent = new Intent(); 700 if (isTv()) { 701 intent.setPackage("com.android.tv.settings"); 702 intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 703 } else if (isAutomotive()) { 704 // TODO(b/151671685): add volume settings intent for automotive 705 return null; 706 } else { 707 switch (vol.getType()) { 708 case VolumeInfo.TYPE_PRIVATE: 709 intent.setClassName("com.android.settings", 710 "com.android.settings.Settings$PrivateVolumeSettingsActivity"); 711 break; 712 case VolumeInfo.TYPE_PUBLIC: 713 intent.setClassName("com.android.settings", 714 "com.android.settings.Settings$PublicVolumeSettingsActivity"); 715 break; 716 default: 717 return null; 718 } 719 } 720 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 721 722 final int requestKey = vol.getId().hashCode(); 723 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 724 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 725 } 726 buildSnoozeIntent(String fsUuid)727 private PendingIntent buildSnoozeIntent(String fsUuid) { 728 final Intent intent = new Intent(ACTION_SNOOZE_VOLUME); 729 intent.putExtra(VolumeRecord.EXTRA_FS_UUID, fsUuid); 730 731 final int requestKey = fsUuid.hashCode(); 732 return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, 733 PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT); 734 } 735 buildForgetPendingIntent(VolumeRecord rec)736 private PendingIntent buildForgetPendingIntent(VolumeRecord rec) { 737 // Not used on TV and Automotive 738 final Intent intent = new Intent(); 739 intent.setClassName("com.android.settings", 740 "com.android.settings.Settings$PrivateVolumeForgetActivity"); 741 intent.putExtra(VolumeRecord.EXTRA_FS_UUID, rec.getFsUuid()); 742 743 final int requestKey = rec.getFsUuid().hashCode(); 744 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 745 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 746 } 747 buildWizardMigratePendingIntent(MoveInfo move)748 private PendingIntent buildWizardMigratePendingIntent(MoveInfo move) { 749 final Intent intent = new Intent(); 750 if (isTv()) { 751 intent.setPackage("com.android.tv.settings"); 752 intent.setAction("com.android.tv.settings.action.MIGRATE_STORAGE"); 753 } else if (isAutomotive()) { 754 // TODO(b/151671685): add storage migrate intent for automotive 755 return null; 756 } else { 757 intent.setClassName("com.android.settings", 758 "com.android.settings.deviceinfo.StorageWizardMigrateProgress"); 759 } 760 intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); 761 762 final VolumeInfo vol = mStorageManager.findVolumeByQualifiedUuid(move.volumeUuid); 763 if (vol != null) { 764 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 765 } 766 return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, 767 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 768 } 769 buildWizardMovePendingIntent(MoveInfo move)770 private PendingIntent buildWizardMovePendingIntent(MoveInfo move) { 771 final Intent intent = new Intent(); 772 if (isTv()) { 773 intent.setPackage("com.android.tv.settings"); 774 intent.setAction("com.android.tv.settings.action.MOVE_APP"); 775 } else if (isAutomotive()) { 776 // TODO(b/151671685): add storage move intent for automotive 777 return null; 778 } else { 779 intent.setClassName("com.android.settings", 780 "com.android.settings.deviceinfo.StorageWizardMoveProgress"); 781 } 782 intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); 783 784 return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, 785 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 786 } 787 buildWizardReadyPendingIntent(DiskInfo disk)788 private PendingIntent buildWizardReadyPendingIntent(DiskInfo disk) { 789 final Intent intent = new Intent(); 790 if (isTv()) { 791 intent.setPackage("com.android.tv.settings"); 792 intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 793 } else if (isAutomotive()) { 794 // TODO(b/151671685): add storage ready intent for automotive 795 return null; 796 } else { 797 intent.setClassName("com.android.settings", 798 "com.android.settings.deviceinfo.StorageWizardReady"); 799 } 800 intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId()); 801 802 final int requestKey = disk.getId().hashCode(); 803 return PendingIntent.getActivityAsUser(mContext, requestKey, intent, 804 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 805 } 806 isAutomotive()807 private boolean isAutomotive() { 808 PackageManager packageManager = mContext.getPackageManager(); 809 return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 810 } 811 isTv()812 private boolean isTv() { 813 PackageManager packageManager = mContext.getPackageManager(); 814 return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK); 815 } 816 } 817