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