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 mFragments = ProfileSelectFragment.getFragments( 197 getContext(), 198 null /* bundle */, 199 StorageCategoryFragment::new, 200 StorageCategoryFragment::new, 201 StorageCategoryFragment::new); 202 return mFragments; 203 } 204 205 @Override getPreferenceScreenResId()206 protected int getPreferenceScreenResId() { 207 return R.xml.storage_dashboard_header_fragment; 208 } 209 refreshUi()210 private void refreshUi() { 211 mStorageSelectionController.setStorageEntries(mStorageEntries); 212 mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); 213 mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); 214 215 for (Fragment fragment : getFragments()) { 216 if (!(fragment instanceof StorageCategoryFragment)) { 217 throw new IllegalStateException("Wrong fragment type to refreshUi"); 218 } 219 ((StorageCategoryFragment) fragment).refreshUi(mSelectedStorageEntry); 220 } 221 222 mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); 223 getActivity().invalidateOptionsMenu(); 224 } 225 226 @Override onCreate(Bundle icicle)227 public void onCreate(Bundle icicle) { 228 super.onCreate(icicle); 229 230 final Activity activity = getActivity(); 231 mStorageManager = activity.getSystemService(StorageManager.class); 232 233 if (icicle == null) { 234 final VolumeInfo specifiedVolumeInfo = 235 Utils.maybeInitializeVolume(mStorageManager, getArguments()); 236 mSelectedStorageEntry = specifiedVolumeInfo == null 237 ? StorageEntry.getDefaultInternalStorageEntry(getContext()) 238 : new StorageEntry(getContext(), specifiedVolumeInfo); 239 } else { 240 mSelectedStorageEntry = icicle.getParcelable(SELECTED_STORAGE_ENTRY_KEY); 241 } 242 243 initializeOptionsMenu(activity); 244 245 if (mStorageCacheHelper.hasCachedSizeInfo()) { 246 mIsLoadedFromCache = true; 247 mStorageEntries.clear(); 248 mStorageEntries.addAll( 249 StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); 250 refreshUi(); 251 } 252 } 253 254 @Override onAttach(Context context)255 public void onAttach(Context context) { 256 super.onAttach(context); 257 mStorageCacheHelper = new StorageCacheHelper(getContext(), UserHandle.myUserId()); 258 use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager( 259 getFragmentManager()); 260 mStorageSelectionController = use(StorageSelectionPreferenceController.class); 261 mStorageSelectionController.setOnItemSelectedListener(storageEntry -> { 262 mSelectedStorageEntry = storageEntry; 263 refreshUi(); 264 265 if (storageEntry.isDiskInfoUnsupported() || storageEntry.isUnmountable()) { 266 DiskInitFragment.show(this, R.string.storage_dialog_unmountable, 267 storageEntry.getDiskId()); 268 } else if (storageEntry.isVolumeRecordMissed()) { 269 StorageUtils.launchForgetMissingVolumeRecordFragment(getContext(), storageEntry); 270 } 271 }); 272 mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class); 273 } 274 275 @VisibleForTesting initializeOptionsMenu(Activity activity)276 void initializeOptionsMenu(Activity activity) { 277 mOptionMenuController = new VolumeOptionMenuController(activity, this, 278 mSelectedStorageEntry); 279 getSettingsLifecycle().addObserver(mOptionMenuController); 280 setHasOptionsMenu(true); 281 activity.invalidateOptionsMenu(); 282 } 283 284 @Override onResume()285 public void onResume() { 286 super.onResume(); 287 288 if (mIsLoadedFromCache) { 289 mIsLoadedFromCache = false; 290 } else { 291 mStorageEntries.clear(); 292 mStorageEntries.addAll( 293 StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); 294 refreshUi(); 295 } 296 mStorageManager.registerListener(mStorageEventListener); 297 } 298 299 @Override onPause()300 public void onPause() { 301 super.onPause(); 302 mStorageManager.unregisterListener(mStorageEventListener); 303 } 304 305 @Override onSaveInstanceState(Bundle outState)306 public void onSaveInstanceState(Bundle outState) { 307 outState.putParcelable(SELECTED_STORAGE_ENTRY_KEY, mSelectedStorageEntry); 308 super.onSaveInstanceState(outState); 309 } 310 311 @Override getHelpResource()312 public int getHelpResource() { 313 return R.string.help_url_storage_dashboard; 314 } 315 316 @Override getMetricsCategory()317 public int getMetricsCategory() { 318 return SettingsEnums.SETTINGS_STORAGE_PROFILE_SELECTOR; 319 } 320 321 @Override getLogTag()322 protected String getLogTag() { 323 return TAG; 324 } 325 } 326