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