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