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.deviceinfo.storage; 18 19 import android.app.Dialog; 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ResolveInfo; 24 import android.graphics.drawable.Drawable; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.os.storage.DiskInfo; 28 import android.os.storage.StorageManager; 29 import android.os.storage.VolumeInfo; 30 import android.os.storage.VolumeRecord; 31 import android.text.SpannableString; 32 import android.text.TextUtils; 33 import android.text.format.Formatter; 34 import android.text.style.TtsSpan; 35 import android.util.Log; 36 import android.widget.Toast; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.appcompat.app.AlertDialog; 41 import androidx.fragment.app.Fragment; 42 43 import com.android.settings.R; 44 import com.android.settings.Utils; 45 import com.android.settings.core.SubSettingLauncher; 46 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 47 import com.android.settings.deviceinfo.PrivateVolumeForget; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 import java.util.stream.Collectors; 52 53 /** Storage utilities */ 54 public class StorageUtils { 55 56 private static final String TAG = "StorageUtils"; 57 58 /** 59 * Collects and returns all kinds of StorageEntry which will show in Storage Settings. 60 */ getAllStorageEntries(Context context, StorageManager storageManager)61 public static List<StorageEntry> getAllStorageEntries(Context context, 62 StorageManager storageManager) { 63 final List<StorageEntry> storageEntries = new ArrayList<>(); 64 storageEntries.addAll(storageManager.getVolumes().stream() 65 .filter(volumeInfo -> isStorageSettingsInterestedVolume(volumeInfo)) 66 .map(volumeInfo -> new StorageEntry(context, volumeInfo)) 67 .collect(Collectors.toList())); 68 storageEntries.addAll(storageManager.getDisks().stream() 69 .filter(disk -> isDiskUnsupported(disk)) 70 .map(disk -> new StorageEntry(disk)) 71 .collect(Collectors.toList())); 72 storageEntries.addAll(storageManager.getVolumeRecords().stream() 73 .filter(volumeRecord -> isVolumeRecordMissed(storageManager, volumeRecord)) 74 .map(volumeRecord -> new StorageEntry(volumeRecord)) 75 .collect(Collectors.toList())); 76 return storageEntries; 77 } 78 79 /** 80 * Returns true if the volumeInfo may be displayed in Storage Settings. 81 */ isStorageSettingsInterestedVolume(VolumeInfo volumeInfo)82 public static boolean isStorageSettingsInterestedVolume(VolumeInfo volumeInfo) { 83 switch (volumeInfo.getType()) { 84 case VolumeInfo.TYPE_PRIVATE: 85 case VolumeInfo.TYPE_PUBLIC: 86 case VolumeInfo.TYPE_STUB: 87 return true; 88 default: 89 return false; 90 } 91 } 92 93 /** 94 * VolumeRecord is a metadata of VolumeInfo, this is the case where a VolumeInfo is missing. 95 * (e.g., internal SD card is removed.) 96 */ isVolumeRecordMissed(StorageManager storageManager, VolumeRecord volumeRecord)97 public static boolean isVolumeRecordMissed(StorageManager storageManager, 98 VolumeRecord volumeRecord) { 99 return volumeRecord.getType() == VolumeInfo.TYPE_PRIVATE 100 && storageManager.findVolumeByUuid(volumeRecord.getFsUuid()) == null; 101 } 102 103 /** 104 * A unsupported disk is the disk of problem format, android is not able to mount automatically. 105 */ isDiskUnsupported(DiskInfo disk)106 public static boolean isDiskUnsupported(DiskInfo disk) { 107 return disk.volumeCount == 0 && disk.size > 0; 108 } 109 110 /** Launches the fragment to forget a specified missing volume record. */ launchForgetMissingVolumeRecordFragment(Context context, StorageEntry storageEntry)111 public static void launchForgetMissingVolumeRecordFragment(Context context, 112 StorageEntry storageEntry) { 113 if (storageEntry == null || !storageEntry.isVolumeRecordMissed()) { 114 return; 115 } 116 117 final Bundle args = new Bundle(); 118 args.putString(VolumeRecord.EXTRA_FS_UUID, storageEntry.getFsUuid()); 119 new SubSettingLauncher(context) 120 .setDestination(PrivateVolumeForget.class.getCanonicalName()) 121 .setTitleRes(R.string.storage_menu_forget) 122 .setSourceMetricsCategory(SettingsEnums.SETTINGS_STORAGE_CATEGORY) 123 .setArguments(args) 124 .launch(); 125 } 126 127 /** Returns size label of changing units. (e.g., 1kB, 2MB, 3GB) */ getStorageSizeLabel(@onNull Context context, long bytes)128 public static @Nullable CharSequence getStorageSizeLabel(@NonNull Context context, long bytes) { 129 final Formatter.BytesResult result = Formatter.formatBytes(context.getResources(), 130 bytes, Formatter.FLAG_SHORTER); 131 String storageSize = TextUtils.expandTemplate(context.getText(R.string.storage_size_large), 132 result.value, result.units).toString(); 133 134 // If storage size is less than 1KB, use TtsSpan to add additional metadata for 135 // text-to-speech engines. 136 if (bytes < 1024) { 137 TtsSpan ttsSpan = new TtsSpan.MeasureBuilder().setNumber(bytes).setUnit("byte").build(); 138 SpannableString phraseSpannable = new SpannableString(storageSize); 139 phraseSpannable.setSpan(ttsSpan, 0, phraseSpannable.length(), 0); 140 return phraseSpannable; 141 } 142 143 return storageSize; 144 } 145 146 /** An AsyncTask to unmount a specified volume. */ 147 public static class UnmountTask extends AsyncTask<Void, Void, Exception> { 148 private final Context mContext; 149 private final StorageManager mStorageManager; 150 private final String mVolumeId; 151 private final String mDescription; 152 UnmountTask(Context context, VolumeInfo volume)153 public UnmountTask(Context context, VolumeInfo volume) { 154 mContext = context.getApplicationContext(); 155 mStorageManager = mContext.getSystemService(StorageManager.class); 156 mVolumeId = volume.getId(); 157 mDescription = mStorageManager.getBestVolumeDescription(volume); 158 } 159 160 @Override doInBackground(Void... params)161 protected Exception doInBackground(Void... params) { 162 try { 163 mStorageManager.unmount(mVolumeId); 164 return null; 165 } catch (Exception e) { 166 return e; 167 } 168 } 169 170 @Override onPostExecute(Exception e)171 protected void onPostExecute(Exception e) { 172 if (e == null) { 173 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_success, 174 mDescription), Toast.LENGTH_SHORT).show(); 175 } else { 176 Log.e(TAG, "Failed to unmount " + mVolumeId, e); 177 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_failure, 178 mDescription), Toast.LENGTH_SHORT).show(); 179 } 180 } 181 } 182 183 /** An AsyncTask to mount a specified volume. */ 184 public static class MountTask extends AsyncTask<Void, Void, Exception> { 185 private final Context mContext; 186 private final StorageManager mStorageManager; 187 private final String mVolumeId; 188 private final String mDescription; 189 MountTask(Context context, VolumeInfo volume)190 public MountTask(Context context, VolumeInfo volume) { 191 mContext = context.getApplicationContext(); 192 mStorageManager = mContext.getSystemService(StorageManager.class); 193 mVolumeId = volume.getId(); 194 mDescription = mStorageManager.getBestVolumeDescription(volume); 195 } 196 197 @Override doInBackground(Void... params)198 protected Exception doInBackground(Void... params) { 199 try { 200 mStorageManager.mount(mVolumeId); 201 return null; 202 } catch (Exception e) { 203 return e; 204 } 205 } 206 207 @Override onPostExecute(Exception e)208 protected void onPostExecute(Exception e) { 209 if (e == null) { 210 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_success, 211 mDescription), Toast.LENGTH_SHORT).show(); 212 } else { 213 Log.e(TAG, "Failed to mount " + mVolumeId, e); 214 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_failure, 215 mDescription), Toast.LENGTH_SHORT).show(); 216 } 217 } 218 } 219 220 /** Shows information about system storage. */ 221 public static class SystemInfoFragment extends InstrumentedDialogFragment { 222 /** Shows the fragment. */ show(@onNull Fragment parent)223 public static void show(@NonNull Fragment parent) { 224 if (!parent.isAdded()) return; 225 226 final SystemInfoFragment dialog = new SystemInfoFragment(); 227 dialog.setTargetFragment(parent, 0); 228 dialog.show(parent.getFragmentManager(), "systemInfo"); 229 } 230 231 @Override getMetricsCategory()232 public int getMetricsCategory() { 233 return SettingsEnums.DIALOG_STORAGE_SYSTEM_INFO; 234 } 235 236 @Override onCreateDialog(Bundle savedInstanceState)237 public Dialog onCreateDialog(Bundle savedInstanceState) { 238 return new AlertDialog.Builder(getActivity()) 239 .setMessage(getContext().getString(R.string.storage_os_detail_dialog_system)) 240 .setPositiveButton(android.R.string.ok, null) 241 .create(); 242 } 243 } 244 245 /** Shows information about temporary system files. */ 246 public static class TemporaryFilesInfoFragment extends InstrumentedDialogFragment { 247 /** Shows the fragment. */ show(@onNull Fragment parent)248 public static void show(@NonNull Fragment parent) { 249 if (!parent.isAdded()) return; 250 251 final TemporaryFilesInfoFragment dialog = new TemporaryFilesInfoFragment(); 252 dialog.setTargetFragment(parent, 0); 253 dialog.show(parent.getFragmentManager(), "temporaryFilesInfo"); 254 } 255 256 @Override getMetricsCategory()257 public int getMetricsCategory() { 258 return SettingsEnums.DIALOG_TEMPORARY_FILES_INFO; 259 } 260 261 @Override onCreateDialog(Bundle savedInstanceState)262 public Dialog onCreateDialog(Bundle savedInstanceState) { 263 return new AlertDialog.Builder(getActivity()) 264 .setMessage(getContext().getString( 265 R.string.storage_other_files_detail_dialog_system)) 266 .setPositiveButton(android.R.string.ok, null) 267 .create(); 268 } 269 } 270 271 /** Gets a summary which has a byte size information. */ getStorageSummary(Context context, int resId, long bytes)272 public static String getStorageSummary(Context context, int resId, long bytes) { 273 final Formatter.BytesResult result = Formatter.formatBytes(context.getResources(), 274 bytes, Formatter.FLAG_SHORTER); 275 return context.getString(resId, result.value, result.units); 276 } 277 278 /** Gets icon for Preference of Free up space. */ getManageStorageIcon(Context context, int userId)279 public static Drawable getManageStorageIcon(Context context, int userId) { 280 ResolveInfo resolveInfo = context.getPackageManager().resolveActivityAsUser( 281 new Intent(StorageManager.ACTION_MANAGE_STORAGE), 0 /* flags */, userId); 282 if (resolveInfo == null || resolveInfo.activityInfo == null) { 283 return null; 284 } 285 286 return Utils.getBadgedIcon(context, resolveInfo.activityInfo.applicationInfo); 287 } 288 } 289