1 /*
2  * Copyright (C) 2018 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;
18 
19 import android.content.Context;
20 import android.os.Bundle;
21 import android.os.Handler;
22 import android.os.storage.DiskInfo;
23 import android.os.storage.StorageManager;
24 import android.os.storage.VolumeInfo;
25 import android.os.storage.VolumeRecord;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import androidx.annotation.Keep;
30 import androidx.preference.Preference;
31 import androidx.preference.PreferenceCategory;
32 
33 import com.android.internal.logging.nano.MetricsProto;
34 import com.android.tv.settings.R;
35 import com.android.tv.settings.SettingsPreferenceFragment;
36 import com.android.tv.settings.device.storage.MissingStorageFragment;
37 import com.android.tv.settings.device.storage.NewStorageActivity;
38 import com.android.tv.settings.device.storage.StorageFragment;
39 import com.android.tv.settings.device.storage.StoragePreference;
40 
41 import java.io.File;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Set;
45 
46 /**
47  * The "Storage" screen in TV settings.
48  */
49 @Keep
50 public class StorageSummaryFragment extends SettingsPreferenceFragment {
51     private static final String TAG = "StorageSummaryFragment";
52 
53     private static final String KEY_DEVICE_CATEGORY = "device_storage";
54     private static final String KEY_REMOVABLE_CATEGORY = "removable_storage";
55 
56     private static final int REFRESH_DELAY_MILLIS = 500;
57 
58     private StorageManager mStorageManager;
59     private final StorageSummaryFragment.StorageEventListener
60             mStorageEventListener = new StorageSummaryFragment.StorageEventListener();
61 
62     private final Handler mHandler = new Handler();
63     private final Runnable mRefreshRunnable = new Runnable() {
64         @Override
65         public void run() {
66             refresh();
67         }
68     };
69 
newInstance()70     public static StorageSummaryFragment newInstance() {
71         return new StorageSummaryFragment();
72     }
73 
74     @Override
onCreate(Bundle savedInstanceState)75     public void onCreate(Bundle savedInstanceState) {
76         mStorageManager = getContext().getSystemService(StorageManager.class);
77         super.onCreate(savedInstanceState);
78     }
79 
80     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)81     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
82         setPreferencesFromResource(R.xml.storage_summary, null);
83         findPreference(KEY_REMOVABLE_CATEGORY).setVisible(false);
84     }
85 
86     @Override
onStart()87     public void onStart() {
88         super.onStart();
89         mStorageManager.registerListener(mStorageEventListener);
90     }
91 
92     @Override
onResume()93     public void onResume() {
94         super.onResume();
95         mHandler.removeCallbacks(mRefreshRunnable);
96         // Delay to allow entrance animations to complete
97         mHandler.postDelayed(mRefreshRunnable, REFRESH_DELAY_MILLIS);
98     }
99 
100     @Override
onPause()101     public void onPause() {
102         super.onPause();
103         mHandler.removeCallbacks(mRefreshRunnable);
104     }
105 
106     @Override
onStop()107     public void onStop() {
108         super.onStop();
109         mStorageManager.unregisterListener(mStorageEventListener);
110     }
111 
refresh()112     private void refresh() {
113         if (!isResumed()) {
114             return;
115         }
116         final Context themedContext = getPreferenceManager().getContext();
117 
118         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
119         volumes.sort(VolumeInfo.getDescriptionComparator());
120 
121         final List<VolumeInfo> privateVolumes = new ArrayList<>(volumes.size());
122         final List<VolumeInfo> publicVolumes = new ArrayList<>(volumes.size());
123 
124         // Find mounted volumes
125         for (final VolumeInfo vol : volumes) {
126             if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
127                 privateVolumes.add(vol);
128             } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
129                 publicVolumes.add(vol);
130             } else {
131                 Log.d(TAG, "Skipping volume " + vol.toString());
132             }
133         }
134 
135         // Find missing private filesystems
136         final List<VolumeRecord> volumeRecords = mStorageManager.getVolumeRecords();
137         final List<VolumeRecord> privateMissingVolumes = new ArrayList<>(volumeRecords.size());
138 
139         for (final VolumeRecord record : volumeRecords) {
140             if (record.getType() == VolumeInfo.TYPE_PRIVATE
141                     && mStorageManager.findVolumeByUuid(record.getFsUuid()) == null) {
142                 privateMissingVolumes.add(record);
143             }
144         }
145 
146         // Find unreadable disks
147         final List<DiskInfo> disks = mStorageManager.getDisks();
148         final List<DiskInfo> unsupportedDisks = new ArrayList<>(disks.size());
149         for (final DiskInfo disk : disks) {
150             if (disk.volumeCount == 0 && disk.size > 0) {
151                 unsupportedDisks.add(disk);
152             }
153         }
154 
155         // Add the prefs
156         final PreferenceCategory deviceCategory =
157                 (PreferenceCategory) findPreference(KEY_DEVICE_CATEGORY);
158         final Set<String> touchedDeviceKeys =
159                 new ArraySet<>(privateVolumes.size() + privateMissingVolumes.size());
160 
161         for (final VolumeInfo volumeInfo : privateVolumes) {
162             final String key = StorageSummaryFragment.VolPreference.makeKey(volumeInfo);
163             touchedDeviceKeys.add(key);
164             StorageSummaryFragment.VolPreference volPreference =
165                         (StorageSummaryFragment.VolPreference) deviceCategory.findPreference(key);
166             if (volPreference == null) {
167                 volPreference = new StorageSummaryFragment.VolPreference(themedContext, volumeInfo);
168             }
169             volPreference.refresh(themedContext, mStorageManager, volumeInfo);
170             deviceCategory.addPreference(volPreference);
171         }
172 
173         for (final VolumeRecord volumeRecord : privateMissingVolumes) {
174             final String key = StorageSummaryFragment.MissingPreference.makeKey(volumeRecord);
175             touchedDeviceKeys.add(key);
176             StorageSummaryFragment.MissingPreference missingPreference =
177                     (StorageSummaryFragment.MissingPreference) deviceCategory.findPreference(key);
178             if (missingPreference == null) {
179                 missingPreference = new StorageSummaryFragment.MissingPreference(
180                             themedContext, volumeRecord);
181             }
182             deviceCategory.addPreference(missingPreference);
183         }
184 
185         for (int i = 0; i < deviceCategory.getPreferenceCount();) {
186             final Preference pref = deviceCategory.getPreference(i);
187             if (touchedDeviceKeys.contains(pref.getKey())) {
188                 i++;
189             } else {
190                 deviceCategory.removePreference(pref);
191             }
192         }
193 
194         final PreferenceCategory removableCategory =
195                 (PreferenceCategory) findPreference(KEY_REMOVABLE_CATEGORY);
196         final int publicCount = publicVolumes.size() + unsupportedDisks.size();
197         final Set<String> touchedRemovableKeys = new ArraySet<>(publicCount);
198         // Only show section if there are public/unknown volumes present
199         removableCategory.setVisible(publicCount > 0);
200 
201         for (final VolumeInfo volumeInfo : publicVolumes) {
202             final String key = StorageSummaryFragment.VolPreference.makeKey(volumeInfo);
203             touchedRemovableKeys.add(key);
204             StorageSummaryFragment.VolPreference volPreference =
205                     (StorageSummaryFragment.VolPreference) removableCategory.findPreference(key);
206             if (volPreference == null) {
207                 volPreference = new StorageSummaryFragment.VolPreference(themedContext, volumeInfo);
208             }
209             volPreference.refresh(themedContext, mStorageManager, volumeInfo);
210             removableCategory.addPreference(volPreference);
211         }
212         for (final DiskInfo diskInfo : unsupportedDisks) {
213             final String key = StorageSummaryFragment.UnsupportedDiskPreference.makeKey(diskInfo);
214             touchedRemovableKeys.add(key);
215             StorageSummaryFragment.UnsupportedDiskPreference unsupportedDiskPreference =
216                     (StorageSummaryFragment.UnsupportedDiskPreference) findPreference(key);
217             if (unsupportedDiskPreference == null) {
218                 unsupportedDiskPreference = new StorageSummaryFragment.UnsupportedDiskPreference(
219                             themedContext, diskInfo);
220             }
221             removableCategory.addPreference(unsupportedDiskPreference);
222         }
223 
224         for (int i = 0; i < removableCategory.getPreferenceCount();) {
225             final Preference pref = removableCategory.getPreference(i);
226             if (touchedRemovableKeys.contains(pref.getKey())) {
227                 i++;
228             } else {
229                 removableCategory.removePreference(pref);
230             }
231         }
232     }
233 
234     private static class VolPreference extends Preference {
VolPreference(Context context, VolumeInfo volumeInfo)235         VolPreference(Context context, VolumeInfo volumeInfo) {
236             super(context);
237             setKey(makeKey(volumeInfo));
238         }
239 
refresh(Context context, StorageManager storageManager, VolumeInfo volumeInfo)240         private void refresh(Context context, StorageManager storageManager,
241                 VolumeInfo volumeInfo) {
242             final String description = storageManager
243                     .getBestVolumeDescription(volumeInfo);
244             setTitle(description);
245             if (volumeInfo.isMountedReadable()) {
246                 setSummary(getSizeString(volumeInfo));
247                 setFragment(StorageFragment.class.getName());
248                 StorageFragment.prepareArgs(getExtras(), volumeInfo);
249             } else {
250                 setSummary(context.getString(R.string.storage_unmount_success, description));
251             }
252         }
253 
getSizeString(VolumeInfo vol)254         private String getSizeString(VolumeInfo vol) {
255             final File path = vol.getPath();
256             if (vol.isMountedReadable() && path != null) {
257                 return String.format(getContext().getString(R.string.storage_size),
258                         StoragePreference.formatSize(getContext(), path.getTotalSpace()));
259             } else {
260                 return null;
261             }
262         }
263 
makeKey(VolumeInfo volumeInfo)264         public static String makeKey(VolumeInfo volumeInfo) {
265             return "VolPref:" + volumeInfo.getId();
266         }
267     }
268 
269     private static class MissingPreference extends Preference {
MissingPreference(Context context, VolumeRecord volumeRecord)270         MissingPreference(Context context, VolumeRecord volumeRecord) {
271             super(context);
272             setKey(makeKey(volumeRecord));
273             setTitle(volumeRecord.getNickname());
274             setSummary(R.string.storage_not_connected);
275             setFragment(MissingStorageFragment.class.getName());
276             MissingStorageFragment.prepareArgs(getExtras(), volumeRecord.getFsUuid());
277         }
278 
makeKey(VolumeRecord volumeRecord)279         public static String makeKey(VolumeRecord volumeRecord) {
280             return "MissingPref:" + volumeRecord.getFsUuid();
281         }
282     }
283 
284     private static class UnsupportedDiskPreference extends Preference {
UnsupportedDiskPreference(Context context, DiskInfo info)285         UnsupportedDiskPreference(Context context, DiskInfo info) {
286             super(context);
287             setKey(makeKey(info));
288             setTitle(info.getDescription());
289             setIntent(NewStorageActivity.getNewStorageLaunchIntent(context, null, info.getId()));
290         }
291 
makeKey(DiskInfo info)292         public static String makeKey(DiskInfo info) {
293             return "UnsupportedPref:" + info.getId();
294         }
295     }
296 
297     private class StorageEventListener extends android.os.storage.StorageEventListener {
298         @Override
onStorageStateChanged(String path, String oldState, String newState)299         public void onStorageStateChanged(String path, String oldState, String newState) {
300             refresh();
301         }
302 
303         @Override
onVolumeStateChanged(VolumeInfo vol, int oldState, int newState)304         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
305             refresh();
306         }
307 
308         @Override
onVolumeRecordChanged(VolumeRecord rec)309         public void onVolumeRecordChanged(VolumeRecord rec) {
310             refresh();
311         }
312 
313         @Override
onVolumeForgotten(String fsUuid)314         public void onVolumeForgotten(String fsUuid) {
315             refresh();
316         }
317 
318         @Override
onDiskScanned(DiskInfo disk, int volumeCount)319         public void onDiskScanned(DiskInfo disk, int volumeCount) {
320             refresh();
321         }
322 
323         @Override
onDiskDestroyed(DiskInfo disk)324         public void onDiskDestroyed(DiskInfo disk) {
325             refresh();
326         }
327 
328     }
329 
330     @Override
getMetricsCategory()331     public int getMetricsCategory() {
332         return MetricsProto.MetricsEvent.SETTINGS_STORAGE_CATEGORY;
333     }
334 }
335