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