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