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