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