1 /* 2 * Copyright (C) 2021 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.settings.dashboard.profileselector; 18 19 import android.app.Activity; 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.UserHandle; 24 import android.os.storage.DiskInfo; 25 import android.os.storage.StorageEventListener; 26 import android.os.storage.StorageManager; 27 import android.os.storage.VolumeInfo; 28 import android.os.storage.VolumeRecord; 29 import android.text.TextUtils; 30 31 import androidx.annotation.VisibleForTesting; 32 import androidx.fragment.app.Fragment; 33 34 import com.android.settings.R; 35 import com.android.settings.Utils; 36 import com.android.settings.deviceinfo.StorageCategoryFragment; 37 import com.android.settings.deviceinfo.VolumeOptionMenuController; 38 import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController; 39 import com.android.settings.deviceinfo.storage.DiskInitFragment; 40 import com.android.settings.deviceinfo.storage.StorageCacheHelper; 41 import com.android.settings.deviceinfo.storage.StorageEntry; 42 import com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController; 43 import com.android.settings.deviceinfo.storage.StorageUsageProgressBarPreferenceController; 44 import com.android.settings.deviceinfo.storage.StorageUtils; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * Storage Settings main UI is composed by 3 fragments: 51 * 52 * StorageDashboardFragment only shows when there is only personal profile for current user. 53 * 54 * ProfileSelectStorageFragment (controls preferences above profile tab) and 55 * StorageCategoryFragment (controls preferences below profile tab) only show when current 56 * user has installed work profile. 57 * 58 * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same 59 * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to 60 * change Storage Settings. 61 */ 62 public class ProfileSelectStorageFragment extends ProfileSelectFragment { 63 64 private static final String TAG = "ProfileSelStorageFrag"; 65 private static final String SELECTED_STORAGE_ENTRY_KEY = "selected_storage_entry_key"; 66 67 private StorageManager mStorageManager; 68 69 private final List<StorageEntry> mStorageEntries = new ArrayList<>(); 70 private StorageEntry mSelectedStorageEntry; 71 private Fragment[] mFragments; 72 73 private StorageSelectionPreferenceController mStorageSelectionController; 74 private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController; 75 private VolumeOptionMenuController mOptionMenuController; 76 private boolean mIsLoadedFromCache; 77 private StorageCacheHelper mStorageCacheHelper; 78 79 private final StorageEventListener mStorageEventListener = new StorageEventListener() { 80 @Override 81 public void onVolumeStateChanged(VolumeInfo volumeInfo, int oldState, int newState) { 82 if (!StorageUtils.isStorageSettingsInterestedVolume(volumeInfo)) { 83 return; 84 } 85 86 final StorageEntry changedStorageEntry = new StorageEntry(getContext(), volumeInfo); 87 final int volumeState = volumeInfo.getState(); 88 switch (volumeState) { 89 case VolumeInfo.STATE_REMOVED: 90 case VolumeInfo.STATE_BAD_REMOVAL: 91 // Remove removed storage from list and don't show it on spinner. 92 if (!mStorageEntries.remove(changedStorageEntry)) { 93 break; 94 } 95 case VolumeInfo.STATE_MOUNTED: 96 case VolumeInfo.STATE_MOUNTED_READ_ONLY: 97 case VolumeInfo.STATE_UNMOUNTABLE: 98 case VolumeInfo.STATE_UNMOUNTED: 99 case VolumeInfo.STATE_EJECTING: 100 // Add mounted or unmountable storage in the list and show it on spinner. 101 // Unmountable storages are the storages which has a problem format and android 102 // is not able to mount it automatically. 103 // Users can format an unmountable storage by the UI and then use the storage. 104 mStorageEntries.removeIf(storageEntry -> { 105 return storageEntry.equals(changedStorageEntry); 106 }); 107 if (volumeState != VolumeInfo.STATE_REMOVED 108 && volumeState != VolumeInfo.STATE_BAD_REMOVAL) { 109 mStorageEntries.add(changedStorageEntry); 110 } 111 if (changedStorageEntry.equals(mSelectedStorageEntry)) { 112 mSelectedStorageEntry = changedStorageEntry; 113 } 114 refreshUi(); 115 break; 116 default: 117 // Do nothing. 118 } 119 } 120 121 @Override 122 public void onVolumeRecordChanged(VolumeRecord volumeRecord) { 123 if (StorageUtils.isVolumeRecordMissed(mStorageManager, volumeRecord)) { 124 // VolumeRecord is a metadata of VolumeInfo, if a VolumeInfo is missing 125 // (e.g., internal SD card is removed.) show the missing storage to users, 126 // users can insert the SD card or manually forget the storage from the device. 127 final StorageEntry storageEntry = new StorageEntry(volumeRecord); 128 if (!mStorageEntries.contains(storageEntry)) { 129 mStorageEntries.add(storageEntry); 130 refreshUi(); 131 } 132 } else { 133 // Find mapped VolumeInfo and replace with existing one for something changed. 134 // (e.g., Renamed.) 135 final VolumeInfo mappedVolumeInfo = 136 mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()); 137 if (mappedVolumeInfo == null) { 138 return; 139 } 140 141 final boolean removeMappedStorageEntry = mStorageEntries.removeIf(storageEntry -> 142 storageEntry.isVolumeInfo() 143 && TextUtils.equals(storageEntry.getFsUuid(), volumeRecord.getFsUuid()) 144 ); 145 if (removeMappedStorageEntry) { 146 mStorageEntries.add(new StorageEntry(getContext(), mappedVolumeInfo)); 147 refreshUi(); 148 } 149 } 150 } 151 152 @Override 153 public void onVolumeForgotten(String fsUuid) { 154 final StorageEntry storageEntry = new StorageEntry( 155 new VolumeRecord(VolumeInfo.TYPE_PUBLIC, fsUuid)); 156 if (mStorageEntries.remove(storageEntry)) { 157 if (mSelectedStorageEntry.equals(storageEntry)) { 158 mSelectedStorageEntry = 159 StorageEntry.getDefaultInternalStorageEntry(getContext()); 160 } 161 refreshUi(); 162 } 163 } 164 165 @Override 166 public void onDiskScanned(DiskInfo disk, int volumeCount) { 167 if (!StorageUtils.isDiskUnsupported(disk)) { 168 return; 169 } 170 final StorageEntry storageEntry = new StorageEntry(disk); 171 if (!mStorageEntries.contains(storageEntry)) { 172 mStorageEntries.add(storageEntry); 173 refreshUi(); 174 } 175 } 176 177 @Override 178 public void onDiskDestroyed(DiskInfo disk) { 179 final StorageEntry storageEntry = new StorageEntry(disk); 180 if (mStorageEntries.remove(storageEntry)) { 181 if (mSelectedStorageEntry.equals(storageEntry)) { 182 mSelectedStorageEntry = 183 StorageEntry.getDefaultInternalStorageEntry(getContext()); 184 } 185 refreshUi(); 186 } 187 } 188 }; 189 190 @Override getFragments()191 public Fragment[] getFragments() { 192 if (mFragments != null) { 193 return mFragments; 194 } 195 196 final Bundle workBundle = new Bundle(); 197 workBundle.putInt(EXTRA_PROFILE, ProfileType.WORK); 198 final Fragment workFragment = new StorageCategoryFragment(); 199 workFragment.setArguments(workBundle); 200 201 final Bundle personalBundle = new Bundle(); 202 personalBundle.putInt(EXTRA_PROFILE, ProfileType.PERSONAL); 203 final Fragment personalFragment = new StorageCategoryFragment(); 204 personalFragment.setArguments(personalBundle); 205 206 mFragments = new Fragment[] { 207 personalFragment, 208 workFragment 209 }; 210 return mFragments; 211 } 212 213 @Override getPreferenceScreenResId()214 protected int getPreferenceScreenResId() { 215 return R.xml.storage_dashboard_header_fragment; 216 } 217 refreshUi()218 private void refreshUi() { 219 mStorageSelectionController.setStorageEntries(mStorageEntries); 220 mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); 221 mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); 222 223 for (Fragment fragment : getFragments()) { 224 if (!(fragment instanceof StorageCategoryFragment)) { 225 throw new IllegalStateException("Wrong fragment type to refreshUi"); 226 } 227 ((StorageCategoryFragment) fragment).refreshUi(mSelectedStorageEntry); 228 } 229 230 mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); 231 getActivity().invalidateOptionsMenu(); 232 } 233 234 @Override onCreate(Bundle icicle)235 public void onCreate(Bundle icicle) { 236 super.onCreate(icicle); 237 238 final Activity activity = getActivity(); 239 mStorageManager = activity.getSystemService(StorageManager.class); 240 241 if (icicle == null) { 242 final VolumeInfo specifiedVolumeInfo = 243 Utils.maybeInitializeVolume(mStorageManager, getArguments()); 244 mSelectedStorageEntry = specifiedVolumeInfo == null 245 ? StorageEntry.getDefaultInternalStorageEntry(getContext()) 246 : new StorageEntry(getContext(), specifiedVolumeInfo); 247 } else { 248 mSelectedStorageEntry = icicle.getParcelable(SELECTED_STORAGE_ENTRY_KEY); 249 } 250 251 initializeOptionsMenu(activity); 252 253 if (mStorageCacheHelper.hasCachedSizeInfo()) { 254 mIsLoadedFromCache = true; 255 mStorageEntries.clear(); 256 mStorageEntries.addAll( 257 StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); 258 refreshUi(); 259 } 260 } 261 262 @Override onAttach(Context context)263 public void onAttach(Context context) { 264 super.onAttach(context); 265 mStorageCacheHelper = new StorageCacheHelper(getContext(), UserHandle.myUserId()); 266 use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager( 267 getFragmentManager()); 268 mStorageSelectionController = use(StorageSelectionPreferenceController.class); 269 mStorageSelectionController.setOnItemSelectedListener(storageEntry -> { 270 mSelectedStorageEntry = storageEntry; 271 refreshUi(); 272 273 if (storageEntry.isDiskInfoUnsupported() || storageEntry.isUnmountable()) { 274 DiskInitFragment.show(this, R.string.storage_dialog_unmountable, 275 storageEntry.getDiskId()); 276 } else if (storageEntry.isVolumeRecordMissed()) { 277 StorageUtils.launchForgetMissingVolumeRecordFragment(getContext(), storageEntry); 278 } 279 }); 280 mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class); 281 } 282 283 @VisibleForTesting initializeOptionsMenu(Activity activity)284 void initializeOptionsMenu(Activity activity) { 285 mOptionMenuController = new VolumeOptionMenuController(activity, this, 286 mSelectedStorageEntry); 287 getSettingsLifecycle().addObserver(mOptionMenuController); 288 setHasOptionsMenu(true); 289 activity.invalidateOptionsMenu(); 290 } 291 292 @Override onResume()293 public void onResume() { 294 super.onResume(); 295 296 if (mIsLoadedFromCache) { 297 mIsLoadedFromCache = false; 298 } else { 299 mStorageEntries.clear(); 300 mStorageEntries.addAll( 301 StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); 302 refreshUi(); 303 } 304 mStorageManager.registerListener(mStorageEventListener); 305 } 306 307 @Override onPause()308 public void onPause() { 309 super.onPause(); 310 mStorageManager.unregisterListener(mStorageEventListener); 311 } 312 313 @Override onSaveInstanceState(Bundle outState)314 public void onSaveInstanceState(Bundle outState) { 315 outState.putParcelable(SELECTED_STORAGE_ENTRY_KEY, mSelectedStorageEntry); 316 super.onSaveInstanceState(outState); 317 } 318 319 @Override getHelpResource()320 public int getHelpResource() { 321 return R.string.help_url_storage_dashboard; 322 } 323 324 @Override getMetricsCategory()325 public int getMetricsCategory() { 326 return SettingsEnums.SETTINGS_STORAGE_PROFILE_SELECTOR; 327 } 328 329 @Override getLogTag()330 protected String getLogTag() { 331 return TAG; 332 } 333 } 334