1 /*
2  * Copyright (C) 2019 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.car.settings.storage;
18 
19 import android.app.ActivityManager;
20 import android.car.userlib.CarUserManagerHelper;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.IPackageDataObserver;
25 import android.content.pm.PackageManager;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.os.UserHandle;
30 import android.os.UserManager;
31 
32 import androidx.annotation.VisibleForTesting;
33 import androidx.annotation.XmlRes;
34 import androidx.loader.app.LoaderManager;
35 
36 import com.android.car.settings.R;
37 import com.android.car.settings.common.ConfirmationDialogFragment;
38 import com.android.car.settings.common.Logger;
39 import com.android.car.settings.common.SettingsFragment;
40 import com.android.car.ui.toolbar.MenuItem;
41 import com.android.settingslib.RestrictedLockUtils;
42 import com.android.settingslib.RestrictedLockUtilsInternal;
43 import com.android.settingslib.applications.ApplicationsState;
44 import com.android.settingslib.applications.StorageStatsSource;
45 
46 import java.util.Arrays;
47 import java.util.List;
48 
49 /**
50  * Fragment to display the applications storage information. Also provide buttons to clear the
51  * applications cache data and user data.
52  */
53 public class AppStorageSettingsDetailsFragment extends SettingsFragment implements
54         AppsStorageStatsManager.Callback {
55     private static final Logger LOG = new Logger(AppStorageSettingsDetailsFragment.class);
56 
57     @VisibleForTesting
58     static final String CONFIRM_CLEAR_STORAGE_DIALOG_TAG =
59             "com.android.car.settings.storage.ConfirmClearStorageDialog";
60 
61     @VisibleForTesting
62     static final String CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG =
63             "com.android.car.settings.storage.ConfirmCannotClearStorageDialog";
64 
65     public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
66     // Result code identifiers
67     public static final int REQUEST_MANAGE_SPACE = 2;
68 
69     // Internal constants used in Handler
70     private static final int OP_SUCCESSFUL = 1;
71     private static final int OP_FAILED = 2;
72 
73     // Constant used in handler to determine when the user data is cleared.
74     private static final int MSG_CLEAR_USER_DATA = 1;
75     // Constant used in handler to determine when the cache is cleared.
76     private static final int MSG_CLEAR_CACHE = 2;
77 
78     // Keys to save the instance values.
79     private static final String KEY_CACHE_CLEARED = "cache_cleared";
80     private static final String KEY_DATA_CLEARED = "data_cleared";
81 
82     // Package information
83     protected PackageManager mPackageManager;
84     private String mPackageName;
85 
86     // Application state info
87     private ApplicationsState.AppEntry mAppEntry;
88     private ApplicationsState mAppState;
89     private ApplicationInfo mInfo;
90     private AppsStorageStatsManager mAppsStorageStatsManager;
91 
92     // User info
93     private int mUserId;
94     private CarUserManagerHelper mCarUserManagerHelper;
95 
96     //  An observer callback to get notified when the cache file deletion is complete.
97     private ClearCacheObserver mClearCacheObserver;
98     //  An observer callback to get notified when the user data deletion is complete.
99     private ClearUserDataObserver mClearDataObserver;
100 
101     // The restriction enforced by admin.
102     private RestrictedLockUtils.EnforcedAdmin mAppsControlDisallowedAdmin;
103     private boolean mAppsControlDisallowedBySystem;
104 
105     // Clear user data and cache buttons and state.
106     private MenuItem mClearStorageButton;
107     private MenuItem mClearCacheButton;
108     private boolean mCanClearData = true;
109     private boolean mCacheCleared;
110     private boolean mDataCleared;
111 
112     private final ConfirmationDialogFragment.ConfirmListener mConfirmClearStorageDialog =
113             arguments -> initiateClearUserData();
114 
115 
116     private final ConfirmationDialogFragment.ConfirmListener mConfirmCannotClearStorageDialog =
117             arguments -> mClearStorageButton.setEnabled(false);
118 
119     /** Creates an instance of this fragment, passing packageName as an argument. */
getInstance(String packageName)120     public static AppStorageSettingsDetailsFragment getInstance(String packageName) {
121         AppStorageSettingsDetailsFragment applicationDetailFragment =
122                 new AppStorageSettingsDetailsFragment();
123         Bundle bundle = new Bundle();
124         bundle.putString(EXTRA_PACKAGE_NAME, packageName);
125         applicationDetailFragment.setArguments(bundle);
126         return applicationDetailFragment;
127     }
128 
129     @Override
130     @XmlRes
getPreferenceScreenResId()131     protected int getPreferenceScreenResId() {
132         return R.xml.app_storage_settings_details_fragment;
133     }
134 
135     @Override
onAttach(Context context)136     public void onAttach(Context context) {
137         super.onAttach(context);
138         mCarUserManagerHelper = new CarUserManagerHelper(context);
139         mUserId = mCarUserManagerHelper.getCurrentProcessUserId();
140         mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME);
141         mAppState = ApplicationsState.getInstance(requireActivity().getApplication());
142         mAppEntry = mAppState.getEntry(mPackageName, mUserId);
143         StorageStatsSource storageStatsSource = new StorageStatsSource(context);
144         StorageStatsSource.AppStorageStats stats = null;
145         mPackageManager = context.getPackageManager();
146         try {
147             stats = storageStatsSource.getStatsForPackage(/* volumeUuid= */ null, mPackageName,
148                     UserHandle.of(mUserId));
149         } catch (Exception e) {
150             // This may happen if the package was removed during our calculation.
151             LOG.w("App unexpectedly not found", e);
152         }
153         mAppsStorageStatsManager = new AppsStorageStatsManager(context);
154         mAppsStorageStatsManager.registerListener(this);
155         use(StorageApplicationPreferenceController.class,
156                 R.string.pk_storage_application_details)
157                 .setAppEntry(mAppEntry)
158                 .setAppState(mAppState);
159 
160         List<StorageSizeBasePreferenceController> preferenceControllers = Arrays.asList(
161                 use(StorageApplicationSizePreferenceController.class,
162                         R.string.pk_storage_application_size),
163                 use(StorageApplicationTotalSizePreferenceController.class,
164                         R.string.pk_storage_application_total_size),
165                 use(StorageApplicationUserDataPreferenceController.class,
166                         R.string.pk_storage_application_data_size),
167                 use(StorageApplicationCacheSizePreferenceController.class,
168                         R.string.pk_storage_application_cache_size)
169         );
170 
171         for (StorageSizeBasePreferenceController pc : preferenceControllers) {
172             pc.setAppsStorageStatsManager(mAppsStorageStatsManager);
173             pc.setAppStorageStats(stats);
174         }
175     }
176 
177     @Override
getToolbarMenuItems()178     public List<MenuItem> getToolbarMenuItems() {
179         return Arrays.asList(mClearStorageButton, mClearCacheButton);
180     }
181 
182     @Override
onSaveInstanceState(Bundle outState)183     public void onSaveInstanceState(Bundle outState) {
184         super.onSaveInstanceState(outState);
185         outState.putBoolean(KEY_CACHE_CLEARED, mCacheCleared);
186         outState.putBoolean(KEY_DATA_CLEARED, mDataCleared);
187     }
188 
189     @Override
onCreate(Bundle savedInstanceState)190     public void onCreate(Bundle savedInstanceState) {
191         super.onCreate(savedInstanceState);
192         if (savedInstanceState != null) {
193             mCacheCleared = savedInstanceState.getBoolean(KEY_CACHE_CLEARED, false);
194             mDataCleared = savedInstanceState.getBoolean(KEY_DATA_CLEARED, false);
195             mCacheCleared = mCacheCleared || mDataCleared;
196         }
197         ConfirmationDialogFragment.resetListeners(
198                 (ConfirmationDialogFragment) findDialogByTag(CONFIRM_CLEAR_STORAGE_DIALOG_TAG),
199                 mConfirmClearStorageDialog, /* rejectListener= */ null);
200         ConfirmationDialogFragment.resetListeners(
201                 (ConfirmationDialogFragment) findDialogByTag(
202                         CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG),
203                 mConfirmCannotClearStorageDialog, /* rejectListener= */ null);
204 
205         mClearStorageButton = new MenuItem.Builder(getContext())
206                 .setTitle(R.string.storage_clear_user_data_text)
207                 .setOnClickListener(i -> handleClearDataClick())
208                 .setEnabled(false)
209                 .build();
210         mClearCacheButton = new MenuItem.Builder(getContext())
211                 .setTitle(R.string.storage_clear_cache_btn_text)
212                 .setOnClickListener(i -> handleClearCacheClick())
213                 .setEnabled(false)
214                 .build();
215     }
216 
217     @Override
onResume()218     public void onResume() {
219         super.onResume();
220         mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
221                 getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId);
222         mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction(
223                 getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId);
224         updateSize();
225     }
226 
227     @Override
onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, boolean dataCleared)228     public void onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared,
229             boolean dataCleared) {
230         if (data == null || mAppsControlDisallowedBySystem) {
231             mClearStorageButton.setEnabled(false);
232             mClearCacheButton.setEnabled(false);
233         } else {
234             long cacheSize = data.getCacheBytes();
235             long dataSize = data.getDataBytes() - cacheSize;
236 
237             mClearStorageButton.setEnabled(dataSize > 0 && mCanClearData && !mDataCleared);
238             mClearCacheButton.setEnabled(cacheSize > 0 && !mCacheCleared);
239         }
240     }
241 
handleClearCacheClick()242     private void handleClearCacheClick() {
243         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
244             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
245                     getActivity(), mAppsControlDisallowedAdmin);
246             return;
247         }
248         // Lazy initialization of observer.
249         if (mClearCacheObserver == null) {
250             mClearCacheObserver = new ClearCacheObserver();
251         }
252         mPackageManager.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver);
253     }
254 
handleClearDataClick()255     private void handleClearDataClick() {
256         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
257             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
258                     getActivity(), mAppsControlDisallowedAdmin);
259         } else if (mAppEntry.info.manageSpaceActivityName != null) {
260             Intent intent = new Intent(Intent.ACTION_DEFAULT);
261             intent.setClassName(mAppEntry.info.packageName,
262                     mAppEntry.info.manageSpaceActivityName);
263             startActivityForResult(intent, REQUEST_MANAGE_SPACE);
264         } else {
265             showClearDataDialog();
266         }
267     }
268 
269     /*
270      * Private method to initiate clearing user data when the user clicks the clear data
271      * button for a system package
272      */
initiateClearUserData()273     private void initiateClearUserData() {
274         mClearStorageButton.setEnabled(false);
275         // Invoke uninstall or clear user data based on sysPackage
276         String packageName = mAppEntry.info.packageName;
277         LOG.i("Clearing user data for package : " + packageName);
278         if (mClearDataObserver == null) {
279             mClearDataObserver = new ClearUserDataObserver();
280         }
281         ActivityManager am = (ActivityManager)
282                 getActivity().getSystemService(Context.ACTIVITY_SERVICE);
283         boolean res = am.clearApplicationUserData(packageName, mClearDataObserver);
284         if (!res) {
285             // Clearing data failed for some obscure reason. Just log error for now
286             LOG.i("Couldn't clear application user data for package:" + packageName);
287             showCannotClearDataDialog();
288         }
289     }
290 
291     /*
292      * Private method to handle clear message notification from observer when
293      * the async operation from PackageManager is complete
294      */
processClearMsg(Message msg)295     private void processClearMsg(Message msg) {
296         int result = msg.arg1;
297         String packageName = mAppEntry.info.packageName;
298         if (result == OP_SUCCESSFUL) {
299             LOG.i("Cleared user data for package : " + packageName);
300             updateSize();
301         } else {
302             mClearStorageButton.setEnabled(true);
303         }
304     }
305 
updateSize()306     private void updateSize() {
307         PackageManager packageManager = getActivity().getPackageManager();
308         try {
309             mInfo = packageManager.getApplicationInfo(mPackageName, 0);
310         } catch (PackageManager.NameNotFoundException e) {
311             LOG.e("Could not find package", e);
312         }
313         if (mInfo == null) {
314             return;
315         }
316         LoaderManager loaderManager = LoaderManager.getInstance(this);
317         mAppsStorageStatsManager.startLoading(loaderManager, mInfo, mUserId, mCacheCleared,
318                 mDataCleared);
319     }
320 
showClearDataDialog()321     private void showClearDataDialog() {
322         ConfirmationDialogFragment confirmClearStorageDialog =
323                 new ConfirmationDialogFragment.Builder(getContext())
324                         .setTitle(R.string.storage_clear_user_data_text)
325                         .setMessage(getString(R.string.storage_clear_data_dlg_text))
326                         .setPositiveButton(R.string.okay, mConfirmClearStorageDialog)
327                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
328                         .build();
329         showDialog(confirmClearStorageDialog, CONFIRM_CLEAR_STORAGE_DIALOG_TAG);
330     }
331 
showCannotClearDataDialog()332     private void showCannotClearDataDialog() {
333         ConfirmationDialogFragment dialogFragment =
334                 new ConfirmationDialogFragment.Builder(getContext())
335                         .setTitle(R.string.storage_clear_data_dlg_title)
336                         .setMessage(getString(R.string.storage_clear_failed_dlg_text))
337                         .setPositiveButton(R.string.okay, mConfirmCannotClearStorageDialog)
338                         .build();
339         showDialog(dialogFragment, CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG);
340     }
341 
342     private final Handler mHandler = new Handler() {
343         public void handleMessage(Message msg) {
344             if (getView() == null) {
345                 return;
346             }
347             switch (msg.what) {
348                 case MSG_CLEAR_USER_DATA:
349                     mDataCleared = true;
350                     mCacheCleared = true;
351                     processClearMsg(msg);
352                     break;
353                 case MSG_CLEAR_CACHE:
354                     mCacheCleared = true;
355                     // Refresh size info
356                     updateSize();
357                     break;
358             }
359         }
360     };
361 
362     class ClearCacheObserver extends IPackageDataObserver.Stub {
onRemoveCompleted(final String packageName, final boolean succeeded)363         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
364             Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE);
365             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
366             mHandler.sendMessage(msg);
367         }
368     }
369 
370     class ClearUserDataObserver extends IPackageDataObserver.Stub {
onRemoveCompleted(final String packageName, final boolean succeeded)371         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
372             Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA);
373             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
374             mHandler.sendMessage(msg);
375         }
376     }
377 }
378