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