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 
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
start()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 
updateMissingPrivateVolumes()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 
onDiskScannedInternal(DiskInfo disk, int volumeCount)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      */
onDiskDestroyedInternal(@onNull DiskInfo disk)267     private void onDiskDestroyedInternal(@NonNull DiskInfo disk) {
268         mNotificationManager.cancelAsUser(disk.getId(), SystemMessage.NOTE_STORAGE_DISK,
269                 UserHandle.ALL);
270     }
271 
onVolumeStateChangedInternal(VolumeInfo vol)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 
onPrivateVolumeStateChangedInternal(VolumeInfo vol)283     private void onPrivateVolumeStateChangedInternal(VolumeInfo vol) {
284         Log.d(TAG, "Notifying about private volume: " + vol.toString());
285 
286         updateMissingPrivateVolumes();
287     }
288 
onPublicVolumeStateChangedInternal(VolumeInfo vol)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 
onVolumeUnmounted(VolumeInfo vol)333     private Notification onVolumeUnmounted(VolumeInfo vol) {
334         // Ignored
335         return null;
336     }
337 
onVolumeChecking(VolumeInfo vol)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 
onVolumeMounted(VolumeInfo vol)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 
onVolumeFormatting(VolumeInfo vol)401     private Notification onVolumeFormatting(VolumeInfo vol) {
402         // Ignored
403         return null;
404     }
405 
onVolumeEjecting(VolumeInfo vol)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 
onVolumeUnmountable(VolumeInfo vol)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 
onVolumeRemoved(VolumeInfo vol)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 
onVolumeBadRemoval(VolumeInfo vol)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 
onMoveProgress(MoveInfo move, int status, long estMillis)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 
onMoveFinished(MoveInfo move, int status)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 
getSmallIcon(DiskInfo disk, int state)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 
buildNotificationBuilder(VolumeInfo vol, CharSequence title, CharSequence text)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 
buildInitPendingIntent(DiskInfo disk)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 
buildInitPendingIntent(VolumeInfo vol)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 
buildUnmountPendingIntent(VolumeInfo vol)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 if (isAutomotive()) {
632             intent.setClassName("com.android.car.settings",
633                     "com.android.car.settings.storage.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         } else {
640             intent.setClassName("com.android.settings",
641                     "com.android.settings.deviceinfo.StorageUnmountReceiver");
642             intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
643 
644             final int requestKey = vol.getId().hashCode();
645             return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
646                     PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
647         }
648     }
649 
buildBrowsePendingIntent(VolumeInfo vol)650     private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) {
651         final StrictMode.VmPolicy oldPolicy = StrictMode.allowVmViolations();
652         try {
653             final Intent intent = vol.buildBrowseIntentForUser(vol.getMountUserId());
654 
655             final int requestKey = vol.getId().hashCode();
656             return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
657                     PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
658         } finally {
659             StrictMode.setVmPolicy(oldPolicy);
660         }
661     }
662 
buildVolumeSettingsPendingIntent(VolumeInfo vol)663     private PendingIntent buildVolumeSettingsPendingIntent(VolumeInfo vol) {
664         final Intent intent = new Intent();
665         if (isTv()) {
666             intent.setPackage("com.android.tv.settings");
667             intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
668         } else {
669             switch (vol.getType()) {
670                 case VolumeInfo.TYPE_PRIVATE:
671                     intent.setClassName("com.android.settings",
672                             "com.android.settings.Settings$PrivateVolumeSettingsActivity");
673                     break;
674                 case VolumeInfo.TYPE_PUBLIC:
675                     intent.setClassName("com.android.settings",
676                             "com.android.settings.Settings$PublicVolumeSettingsActivity");
677                     break;
678                 default:
679                     return null;
680             }
681         }
682         intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
683 
684         final int requestKey = vol.getId().hashCode();
685         return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
686                 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
687     }
688 
buildSnoozeIntent(String fsUuid)689     private PendingIntent buildSnoozeIntent(String fsUuid) {
690         final Intent intent = new Intent(ACTION_SNOOZE_VOLUME);
691         intent.putExtra(VolumeRecord.EXTRA_FS_UUID, fsUuid);
692 
693         final int requestKey = fsUuid.hashCode();
694         return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
695                 PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
696     }
697 
buildForgetPendingIntent(VolumeRecord rec)698     private PendingIntent buildForgetPendingIntent(VolumeRecord rec) {
699         // Not used on TV
700         final Intent intent = new Intent();
701         intent.setClassName("com.android.settings",
702                 "com.android.settings.Settings$PrivateVolumeForgetActivity");
703         intent.putExtra(VolumeRecord.EXTRA_FS_UUID, rec.getFsUuid());
704 
705         final int requestKey = rec.getFsUuid().hashCode();
706         return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
707                 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
708     }
709 
buildWizardMigratePendingIntent(MoveInfo move)710     private PendingIntent buildWizardMigratePendingIntent(MoveInfo move) {
711         final Intent intent = new Intent();
712         if (isTv()) {
713             intent.setPackage("com.android.tv.settings");
714             intent.setAction("com.android.tv.settings.action.MIGRATE_STORAGE");
715         } else {
716             intent.setClassName("com.android.settings",
717                     "com.android.settings.deviceinfo.StorageWizardMigrateProgress");
718         }
719         intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId);
720 
721         final VolumeInfo vol = mStorageManager.findVolumeByQualifiedUuid(move.volumeUuid);
722         if (vol != null) {
723             intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
724         }
725         return PendingIntent.getActivityAsUser(mContext, move.moveId, intent,
726                 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
727     }
728 
buildWizardMovePendingIntent(MoveInfo move)729     private PendingIntent buildWizardMovePendingIntent(MoveInfo move) {
730         final Intent intent = new Intent();
731         if (isTv()) {
732             intent.setPackage("com.android.tv.settings");
733             intent.setAction("com.android.tv.settings.action.MOVE_APP");
734         } else {
735             intent.setClassName("com.android.settings",
736                     "com.android.settings.deviceinfo.StorageWizardMoveProgress");
737         }
738         intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId);
739 
740         return PendingIntent.getActivityAsUser(mContext, move.moveId, intent,
741                 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
742     }
743 
buildWizardReadyPendingIntent(DiskInfo disk)744     private PendingIntent buildWizardReadyPendingIntent(DiskInfo disk) {
745         final Intent intent = new Intent();
746         if (isTv()) {
747             intent.setPackage("com.android.tv.settings");
748             intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
749         } else {
750             intent.setClassName("com.android.settings",
751                     "com.android.settings.deviceinfo.StorageWizardReady");
752         }
753         intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId());
754 
755         final int requestKey = disk.getId().hashCode();
756         return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
757                 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
758     }
759 
isAutomotive()760     private boolean isAutomotive() {
761         PackageManager packageManager = mContext.getPackageManager();
762         return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
763     }
764 
isTv()765     private boolean isTv() {
766         PackageManager packageManager = mContext.getPackageManager();
767         return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
768     }
769 }
770