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