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.tv.settings.device.storage;
18 
19 import android.app.Activity;
20 import android.app.ActivityManager;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.UserInfo;
26 import android.os.Bundle;
27 import android.os.UserHandle;
28 import android.os.UserManager;
29 import android.os.storage.DiskInfo;
30 import android.os.storage.StorageEventListener;
31 import android.os.storage.StorageManager;
32 import android.os.storage.VolumeInfo;
33 import android.os.storage.VolumeRecord;
34 import android.provider.Settings;
35 import android.support.annotation.NonNull;
36 import android.support.annotation.Nullable;
37 import android.support.v17.leanback.app.GuidedStepFragment;
38 import android.support.v17.leanback.widget.GuidanceStylist;
39 import android.support.v17.leanback.widget.GuidedAction;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.widget.Toast;
43 
44 import com.android.tv.settings.R;
45 import com.android.tv.settings.device.StorageResetActivity;
46 
47 import java.util.List;
48 
49 public class NewStorageActivity extends Activity {
50 
51     private static final String TAG = "NewStorageActivity";
52 
53     private static final String ACTION_NEW_STORAGE =
54             "com.android.tv.settings.device.storage.NewStorageActivity.NEW_STORAGE";
55     private static final String ACTION_MISSING_STORAGE =
56             "com.android.tv.settings.device.storage.NewStorageActivity.MISSING_STORAGE";
57 
getNewStorageLaunchIntent(Context context, String volumeId, String diskId)58     public static Intent getNewStorageLaunchIntent(Context context, String volumeId,
59             String diskId) {
60         final Intent i = new Intent(context, NewStorageActivity.class);
61         i.setAction(ACTION_NEW_STORAGE);
62         i.putExtra(VolumeInfo.EXTRA_VOLUME_ID, volumeId);
63         i.putExtra(DiskInfo.EXTRA_DISK_ID, diskId);
64         return i;
65     }
66 
getMissingStorageLaunchIntent(Context context, String fsUuid)67     public static Intent getMissingStorageLaunchIntent(Context context, String fsUuid) {
68         final Intent i = new Intent(context, NewStorageActivity.class);
69         i.setAction(ACTION_MISSING_STORAGE);
70         i.putExtra(VolumeRecord.EXTRA_FS_UUID, fsUuid);
71         return i;
72     }
73 
74     @Override
onCreate(@ullable Bundle savedInstanceState)75     protected void onCreate(@Nullable Bundle savedInstanceState) {
76         super.onCreate(savedInstanceState);
77 
78         if (savedInstanceState == null) {
79             final String action = getIntent().getAction();
80 
81             if (TextUtils.equals(action, ACTION_NEW_STORAGE)) {
82                 final String volumeId = getIntent().getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
83                 final String diskId = getIntent().getStringExtra(DiskInfo.EXTRA_DISK_ID);
84                 if (TextUtils.isEmpty(volumeId) && TextUtils.isEmpty(diskId)) {
85                     throw new IllegalStateException(
86                             "NewStorageActivity launched without specifying new storage");
87                 }
88 
89                 getFragmentManager().beginTransaction()
90                         .add(android.R.id.content, NewStorageFragment.newInstance(volumeId, diskId))
91                         .commit();
92             } else if (TextUtils.equals(action, ACTION_MISSING_STORAGE)) {
93                 final String fsUuid = getIntent().getStringExtra(VolumeRecord.EXTRA_FS_UUID);
94                 if (TextUtils.isEmpty(fsUuid)) {
95                     throw new IllegalStateException(
96                             "NewStorageActivity launched without specifying missing storage");
97                 }
98 
99                 getFragmentManager().beginTransaction()
100                         .add(android.R.id.content, MissingStorageFragment.newInstance(fsUuid))
101                         .commit();
102             }
103         }
104     }
105 
106     public static class NewStorageFragment extends GuidedStepFragment {
107 
108         private static final int ACTION_BROWSE = 1;
109         private static final int ACTION_FORMAT_AS_PRIVATE = 2;
110         private static final int ACTION_UNMOUNT = 3;
111         private static final int ACTION_FORMAT_AS_PUBLIC = 4;
112 
113         private String mVolumeId;
114         private String mDiskId;
115         private String mDescription;
116 
117         private final StorageEventListener mStorageEventListener = new StorageEventListener() {
118             @Override
119             public void onDiskDestroyed(DiskInfo disk) {
120                 checkForUnmount();
121             }
122 
123             @Override
124             public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
125                 checkForUnmount();
126             }
127         };
128 
newInstance(String volumeId, String diskId)129         public static NewStorageFragment newInstance(String volumeId, String diskId) {
130             final Bundle b = new Bundle(1);
131             b.putString(VolumeInfo.EXTRA_VOLUME_ID, volumeId);
132             b.putString(DiskInfo.EXTRA_DISK_ID, diskId);
133             final NewStorageFragment fragment = new NewStorageFragment();
134             fragment.setArguments(b);
135             return fragment;
136         }
137 
138         @Override
onCreate(Bundle savedInstanceState)139         public void onCreate(Bundle savedInstanceState) {
140             StorageManager storageManager = getActivity().getSystemService(StorageManager.class);
141             mVolumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID);
142             mDiskId = getArguments().getString(DiskInfo.EXTRA_DISK_ID);
143             if (TextUtils.isEmpty(mVolumeId) && TextUtils.isEmpty(mDiskId)) {
144                 throw new IllegalStateException(
145                         "NewStorageFragment launched without specifying new storage");
146             }
147             if (!TextUtils.isEmpty(mVolumeId)) {
148                 final VolumeInfo info = storageManager.findVolumeById(mVolumeId);
149                 mDescription = storageManager.getBestVolumeDescription(info);
150             } else {
151                 final DiskInfo info = storageManager.findDiskById(mDiskId);
152                 mDescription = info.getDescription();
153             }
154 
155             super.onCreate(savedInstanceState);
156         }
157 
158         @Override
onStart()159         public void onStart() {
160             super.onStart();
161             checkForUnmount();
162             getActivity().getSystemService(StorageManager.class)
163                     .registerListener(mStorageEventListener);
164         }
165 
166         @Override
onStop()167         public void onStop() {
168             super.onStop();
169             getActivity().getSystemService(StorageManager.class)
170                     .unregisterListener(mStorageEventListener);
171         }
172 
173         @Override
onCreateGuidance(Bundle savedInstanceState)174         public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
175             return new GuidanceStylist.Guidance(
176                     getString(R.string.storage_new_title),
177                     mDescription,
178                     null,
179                     getActivity().getDrawable(R.drawable.ic_storage_132dp));
180         }
181 
182         @Override
onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)183         public void onCreateActions(@NonNull List<GuidedAction> actions,
184                 Bundle savedInstanceState) {
185             if (TextUtils.isEmpty(mVolumeId)) {
186                 actions.add(new GuidedAction.Builder(getContext())
187                         .title(R.string.storage_new_action_format_public)
188                         .id(ACTION_FORMAT_AS_PUBLIC)
189                         .build());
190             } else {
191                 actions.add(new GuidedAction.Builder(getContext())
192                         .title(R.string.storage_new_action_browse)
193                         .id(ACTION_BROWSE)
194                         .build());
195             }
196             actions.add(new GuidedAction.Builder(getContext())
197                     .title(R.string.storage_new_action_adopt)
198                     .id(ACTION_FORMAT_AS_PRIVATE)
199                     .build());
200             actions.add(new GuidedAction.Builder(getContext())
201                     .title(R.string.storage_new_action_eject)
202                     .id(ACTION_UNMOUNT)
203                     .build());
204         }
205 
206         @Override
onGuidedActionClicked(GuidedAction action)207         public void onGuidedActionClicked(GuidedAction action) {
208             switch ((int) action.getId()) {
209                 case ACTION_FORMAT_AS_PUBLIC:
210                     startActivity(FormatActivity.getFormatAsPublicIntent(getActivity(), mDiskId));
211                     break;
212                 case ACTION_BROWSE:
213                     startActivity(new Intent(getActivity(), StorageResetActivity.class));
214                     break;
215                 case ACTION_FORMAT_AS_PRIVATE:
216                     startActivity(FormatActivity.getFormatAsPrivateIntent(getActivity(), mDiskId));
217                     break;
218                 case ACTION_UNMOUNT:
219                     // If we've mounted a volume, eject it. Otherwise just treat eject as cancel
220                     if (!TextUtils.isEmpty(mVolumeId)) {
221                         startActivity(
222                                 UnmountActivity.getIntent(getActivity(), mVolumeId, mDescription));
223                     }
224                     break;
225             }
226             getActivity().finish();
227         }
228 
checkForUnmount()229         private void checkForUnmount() {
230             if (!isAdded()) {
231                 return;
232             }
233 
234             final StorageManager storageManager =
235                     getContext().getSystemService(StorageManager.class);
236 
237             if (!TextUtils.isEmpty(mDiskId)) {
238                 // If the disk disappears, assume we're done
239                 final List<DiskInfo> diskInfos = storageManager.getDisks();
240                 boolean found = false;
241                 for (DiskInfo diskInfo : diskInfos) {
242                     if (TextUtils.equals(diskInfo.getId(), mDiskId)) {
243                         found = true;
244                         break;
245                     }
246                 }
247                 if (!found) {
248                     getActivity().finish();
249                 }
250             } else if (!TextUtils.isEmpty(mVolumeId)) {
251                 final List<VolumeInfo> volumeInfos = storageManager.getVolumes();
252                 boolean found = false;
253                 for (VolumeInfo volumeInfo : volumeInfos) {
254                     if (TextUtils.equals(volumeInfo.getId(), mVolumeId)) {
255                         found = true;
256                         break;
257                     }
258                 }
259                 if (!found) {
260                     getActivity().finish();
261                 }
262             }
263         }
264     }
265 
266     public static class MissingStorageFragment extends GuidedStepFragment {
267 
268         private String mFsUuid;
269         private String mDescription;
270 
271         private final BroadcastReceiver mDiskReceiver = new BroadcastReceiver() {
272             @Override
273             public void onReceive(Context context, Intent intent) {
274                 if (TextUtils.equals(intent.getAction(), VolumeInfo.ACTION_VOLUME_STATE_CHANGED)) {
275                     checkForRemount();
276                 }
277             }
278         };
279 
newInstance(String fsUuid)280         public static MissingStorageFragment newInstance(String fsUuid) {
281             final MissingStorageFragment fragment = new MissingStorageFragment();
282             final Bundle b = new Bundle(1);
283             b.putString(VolumeRecord.EXTRA_FS_UUID, fsUuid);
284             fragment.setArguments(b);
285             return fragment;
286         }
287 
288         @Override
onCreate(Bundle savedInstanceState)289         public void onCreate(Bundle savedInstanceState) {
290             StorageManager storageManager = getActivity().getSystemService(StorageManager.class);
291             mFsUuid = getArguments().getString(VolumeRecord.EXTRA_FS_UUID);
292             if (TextUtils.isEmpty(mFsUuid)) {
293                 throw new IllegalStateException(
294                         "MissingStorageFragment launched without specifying missing storage");
295             }
296             final VolumeRecord volumeRecord = storageManager.findRecordByUuid(mFsUuid);
297             mDescription = volumeRecord.getNickname();
298 
299             super.onCreate(savedInstanceState);
300         }
301 
302         @Override
onStart()303         public void onStart() {
304             super.onStart();
305             getContext().registerReceiver(mDiskReceiver,
306                     new IntentFilter(VolumeInfo.ACTION_VOLUME_STATE_CHANGED));
307             checkForRemount();
308         }
309 
310         @Override
onStop()311         public void onStop() {
312             super.onStop();
313             getContext().unregisterReceiver(mDiskReceiver);
314         }
315 
316         @Override
onCreateGuidance(Bundle savedInstanceState)317         public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
318             return new GuidanceStylist.Guidance(
319                     getString(R.string.storage_missing_title, mDescription),
320                     getString(R.string.storage_missing_description),
321                     null,
322                     getActivity().getDrawable(R.drawable.ic_error_132dp));
323         }
324 
325         @Override
onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)326         public void onCreateActions(@NonNull List<GuidedAction> actions,
327                 Bundle savedInstanceState) {
328             actions.add(new GuidedAction.Builder(getContext())
329                     .clickAction(GuidedAction.ACTION_ID_OK)
330                     .build());
331         }
332 
333         @Override
onGuidedActionClicked(GuidedAction action)334         public void onGuidedActionClicked(GuidedAction action) {
335             getActivity().finish();
336         }
337 
checkForRemount()338         private void checkForRemount() {
339             if (!isAdded()) {
340                 return;
341             }
342 
343             final List<VolumeInfo> volumeInfos =
344                     getContext().getSystemService(StorageManager.class).getVolumes();
345 
346             for (final VolumeInfo info : volumeInfos) {
347                 if (!TextUtils.equals(info.getFsUuid(), mFsUuid)) {
348                     continue;
349                 }
350                 if (info.isMountedReadable()) {
351                     getActivity().finish();
352                 }
353             }
354         }
355     }
356 
357     public static class DiskReceiver extends BroadcastReceiver {
358 
359         private StorageManager mStorageManager;
360         @Override
onReceive(Context context, Intent intent)361         public void onReceive(Context context, Intent intent) {
362             final UserManager userManager =
363                     (UserManager) context.getSystemService(Context.USER_SERVICE);
364             final UserInfo userInfo = userManager.getUserInfo(UserHandle.myUserId());
365 
366             if (userInfo.isRestricted() ||
367                     ActivityManager.getCurrentUser() != UserHandle.myUserId()) {
368                 Log.d(TAG, "Ignoring storage notification: wrong user");
369                 return;
370             }
371 
372             if (Settings.Secure.getInt(context.getContentResolver(),
373                     Settings.Secure.USER_SETUP_COMPLETE, 0) == 0) {
374                 Log.d(TAG, "Ignoring storage notification: setup not complete");
375                 return;
376             }
377 
378             mStorageManager = context.getSystemService(StorageManager.class);
379 
380             if (TextUtils.equals(intent.getAction(), VolumeInfo.ACTION_VOLUME_STATE_CHANGED)) {
381                 final int state = intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_STATE, -1);
382                 if (state == VolumeInfo.STATE_MOUNTED ||
383                         state == VolumeInfo.STATE_MOUNTED_READ_ONLY) {
384                     handleMount(context, intent);
385                 } else if (state == VolumeInfo.STATE_UNMOUNTED ||
386                         state == VolumeInfo.STATE_BAD_REMOVAL) {
387                     handleUnmount(context, intent);
388                 }
389             } else if (TextUtils.equals(intent.getAction(), DiskInfo.ACTION_DISK_SCANNED)) {
390                 handleScan(context, intent);
391             } else if (TextUtils.equals(intent.getAction(),
392                     "com.google.android.tungsten.setupwraith.TV_SETTINGS_POST_SETUP")) {
393                 handleSetupComplete(context);
394             }
395         }
396 
handleScan(Context context, Intent intent)397         private void handleScan(Context context, Intent intent) {
398             final String diskId = intent.getStringExtra(DiskInfo.EXTRA_DISK_ID);
399             if (TextUtils.isEmpty(diskId)) {
400                 Log.e(TAG, intent.getAction() + " with no " + DiskInfo.EXTRA_DISK_ID);
401                 return;
402             }
403             final DiskInfo diskInfo = mStorageManager.findDiskById(diskId);
404             if (diskInfo == null) {
405                 Log.e(TAG, "Disk ID " + diskId + " is no longer mounted");
406                 return;
407             }
408             if (diskInfo.size <= 0) {
409                 Log.d(TAG, "Disk ID " + diskId + " has no media");
410                 return;
411             }
412             if (intent.getIntExtra(DiskInfo.EXTRA_VOLUME_COUNT, -1) != 0) {
413                 Log.d(TAG, "Disk ID " + diskId + " has usable volumes, waiting for mount");
414                 return;
415             }
416             // No usable volumes, prompt the user to erase the disk
417             final Intent i = NewStorageActivity.getNewStorageLaunchIntent(context, null, diskId);
418             setPopupLaunchFlags(i);
419             context.startActivity(i);
420         }
421 
handleMount(Context context, Intent intent)422         private void handleMount(Context context, Intent intent) {
423             final String volumeId = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
424 
425             final List<VolumeInfo> volumeInfos = mStorageManager.getVolumes();
426             for (final VolumeInfo info : volumeInfos) {
427                 if (!TextUtils.equals(info.getId(), volumeId)) {
428                     continue;
429                 }
430                 final String uuid = info.getFsUuid();
431                 Log.d(TAG, "Scanning volume: " + info);
432                 if (info.getType() == VolumeInfo.TYPE_PRIVATE
433                         && !TextUtils.equals(volumeId, VolumeInfo.ID_PRIVATE_INTERNAL)) {
434                     Toast.makeText(context, R.string.storage_mount_adopted, Toast.LENGTH_SHORT)
435                             .show();
436                     return;
437                 }
438                 if (info.getType() != VolumeInfo.TYPE_PUBLIC || TextUtils.isEmpty(uuid)) {
439                     continue;
440                 }
441                 final VolumeRecord record = mStorageManager.findRecordByUuid(uuid);
442                 if (record.isInited() || record.isSnoozed()) {
443                     continue;
444                 }
445                 final DiskInfo disk = info.getDisk();
446                 if (disk.isAdoptable()) {
447                     final Intent i = NewStorageActivity.getNewStorageLaunchIntent(context,
448                             volumeId, disk.getId());
449                     setPopupLaunchFlags(i);
450                     context.startActivity(i);
451                     break;
452                 }
453             }
454         }
455 
handleUnmount(Context context, Intent intent)456         private void handleUnmount(Context context, Intent intent) {
457             final String fsUuid = intent.getStringExtra(VolumeRecord.EXTRA_FS_UUID);
458             if (TextUtils.isEmpty(fsUuid)) {
459                 Log.e(TAG, "Missing fsUuid, not launching activity.");
460                 return;
461             }
462             VolumeRecord volumeRecord = null;
463             try {
464                 volumeRecord = mStorageManager.findRecordByUuid(fsUuid);
465             } catch (Exception e) {
466                 Log.e(TAG, "Error finding volume record", e);
467             }
468             if (volumeRecord == null) {
469                 return;
470             }
471             Log.d(TAG, "Found ejected volume: " + volumeRecord + " for FSUUID: " + fsUuid);
472             if (volumeRecord.getType() == VolumeInfo.TYPE_PRIVATE) {
473                 final Intent i = NewStorageActivity.getMissingStorageLaunchIntent(context, fsUuid);
474                 setPopupLaunchFlags(i);
475                 context.startActivity(i);
476             }
477         }
478 
handleSetupComplete(Context context)479         private void handleSetupComplete(Context context) {
480             Log.d(TAG, "Scanning for storage post-setup");
481 
482             final List<DiskInfo> diskInfos = mStorageManager.getDisks();
483             for (DiskInfo diskInfo : diskInfos) {
484                 Log.d(TAG, "Scanning disk: " + diskInfo);
485                 if (diskInfo.size <= 0) {
486                     Log.d(TAG, "Disk ID " + diskInfo.id + " has no media");
487                     continue;
488                 }
489                 if (diskInfo.volumeCount != 0) {
490                     Log.d(TAG, "Disk ID " + diskInfo.id + " has usable volumes, deferring");
491                     continue;
492                 }
493                 // No usable volumes, prompt the user to erase the disk
494                 final Intent i =
495                         NewStorageActivity.getNewStorageLaunchIntent(context, null, diskInfo.id);
496                 setPopupLaunchFlags(i);
497                 context.startActivity(i);
498                 return;
499             }
500 
501             final List<VolumeInfo> volumeInfos = mStorageManager.getVolumes();
502             for (final VolumeInfo info : volumeInfos) {
503                 final String uuid = info.getFsUuid();
504                 Log.d(TAG, "Scanning volume: " + info);
505                 if (info.getType() != VolumeInfo.TYPE_PUBLIC || TextUtils.isEmpty(uuid)) {
506                     continue;
507                 }
508                 final VolumeRecord record = mStorageManager.findRecordByUuid(uuid);
509                 if (record.isInited() || record.isSnoozed()) {
510                     continue;
511                 }
512                 final DiskInfo disk = info.getDisk();
513                 if (disk.isAdoptable()) {
514                     final Intent i = NewStorageActivity.getNewStorageLaunchIntent(context,
515                             info.getId(), disk.getId());
516                     setPopupLaunchFlags(i);
517                     context.startActivity(i);
518                     return;
519                 }
520             }
521         }
522 
setPopupLaunchFlags(Intent intent)523         private void setPopupLaunchFlags(Intent intent) {
524             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
525         }
526     }
527 
528 }
529