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