1 /*
2  * Copyright (C) 2020 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 static com.android.car.settings.common.ActionButtonsPreference.ActionButtons;
20 
21 import android.app.ActivityManager;
22 import android.car.drivingstate.CarUxRestrictions;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.IPackageDataObserver;
27 import android.content.pm.PackageManager;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.os.UserHandle;
31 import android.os.UserManager;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.loader.app.LoaderManager;
35 
36 import com.android.car.settings.R;
37 import com.android.car.settings.common.ActionButtonInfo;
38 import com.android.car.settings.common.ActionButtonsPreference;
39 import com.android.car.settings.common.ConfirmationDialogFragment;
40 import com.android.car.settings.common.FragmentController;
41 import com.android.car.settings.common.Logger;
42 import com.android.car.settings.common.PreferenceController;
43 import com.android.settingslib.RestrictedLockUtils;
44 import com.android.settingslib.RestrictedLockUtilsInternal;
45 import com.android.settingslib.applications.ApplicationsState;
46 import com.android.settingslib.applications.StorageStatsSource;
47 
48 /**
49  * Displays the action buttons to clear an applications cache and user data.
50  */
51 public class StorageApplicationActionButtonsPreferenceController extends
52         PreferenceController<ActionButtonsPreference> implements
53         AppsStorageStatsManager.Callback {
54     private static final Logger LOG = new Logger(
55             StorageApplicationActionButtonsPreferenceController.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     private ActionButtonInfo mClearStorageButton;
79     private ActionButtonInfo mClearCacheButton;
80 
81     private ApplicationsState.AppEntry mAppEntry;
82     private String mPackageName;
83     private ApplicationInfo mInfo;
84     private AppsStorageStatsManager mAppsStorageStatsManager;
85     private LoaderManager mLoaderManager;
86 
87     //  An observer callback to get notified when the cache file deletion is complete.
88     private ClearCacheObserver mClearCacheObserver;
89     //  An observer callback to get notified when the user data deletion is complete.
90     private ClearUserDataObserver mClearDataObserver;
91 
92     private PackageManager mPackageManager;
93     private RestrictedLockUtils.EnforcedAdmin mAppsControlDisallowedAdmin;
94     private boolean mAppsControlDisallowedBySystem;
95     private int mUserId;
96 
97     private boolean mCacheCleared;
98     private boolean mDataCleared;
99 
100     private final ConfirmationDialogFragment.ConfirmListener mConfirmClearStorageDialog =
101             arguments -> initiateClearUserData();
102 
103     private final ConfirmationDialogFragment.ConfirmListener mConfirmCannotClearStorageDialog =
104             arguments -> mClearStorageButton.setEnabled(false);
105 
StorageApplicationActionButtonsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)106     public StorageApplicationActionButtonsPreferenceController(Context context,
107             String preferenceKey, FragmentController fragmentController,
108             CarUxRestrictions uxRestrictions) {
109         super(context, preferenceKey, fragmentController, uxRestrictions);
110         mUserId = UserHandle.myUserId();
111         mPackageManager = context.getPackageManager();
112     }
113 
114     @Override
getPreferenceType()115     protected Class<ActionButtonsPreference> getPreferenceType() {
116         return ActionButtonsPreference.class;
117     }
118 
119     /** Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. */
setAppEntry( ApplicationsState.AppEntry appEntry)120     public StorageApplicationActionButtonsPreferenceController setAppEntry(
121             ApplicationsState.AppEntry appEntry) {
122         mAppEntry = appEntry;
123         return this;
124     }
125 
126     /**
127      * Set the packageName, which is used to perform actions on a particular package.
128      */
setPackageName(String packageName)129     public StorageApplicationActionButtonsPreferenceController setPackageName(String packageName) {
130         mPackageName = packageName;
131         return this;
132     }
133 
134     /**
135      * Sets the {@link AppsStorageStatsManager} which will be used to register the controller to the
136      * Listener {@link AppsStorageStatsManager.Callback}.
137      */
setAppsStorageStatsManager( AppsStorageStatsManager appsStorageStatsManager)138     public StorageApplicationActionButtonsPreferenceController setAppsStorageStatsManager(
139             AppsStorageStatsManager appsStorageStatsManager) {
140         mAppsStorageStatsManager = appsStorageStatsManager;
141         return this;
142     }
143 
144     /**
145      * Sets the {@link LoaderManager} used to load app storage stats.
146      */
setLoaderManager( LoaderManager loaderManager)147     public StorageApplicationActionButtonsPreferenceController setLoaderManager(
148             LoaderManager loaderManager) {
149         mLoaderManager = loaderManager;
150         return this;
151     }
152 
153     @Override
checkInitialized()154     protected void checkInitialized() {
155         if (mAppEntry == null || mPackageName == null || mAppsStorageStatsManager == null
156                 || mLoaderManager == null) {
157             throw new IllegalStateException(
158                     "AppEntry, PackageName, AppStorageStatsManager, and LoaderManager should be "
159                             + "set before calling this function");
160         }
161     }
162 
163     @Override
onCreateInternal()164     protected void onCreateInternal() {
165         mAppsStorageStatsManager.registerListener(this);
166 
167         mClearStorageButton = getPreference().getButton(ActionButtons.BUTTON1);
168         mClearCacheButton = getPreference().getButton(ActionButtons.BUTTON2);
169 
170         ConfirmationDialogFragment.resetListeners(
171                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
172                         CONFIRM_CLEAR_STORAGE_DIALOG_TAG),
173                 mConfirmClearStorageDialog,
174                 /* rejectListener= */ null,
175                 /* neutralListener= */ null);
176         ConfirmationDialogFragment.resetListeners(
177                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
178                         CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG),
179                 mConfirmCannotClearStorageDialog,
180                 /* rejectListener= */ null,
181                 /* neutralListener= */ null);
182 
183         mClearStorageButton
184                 .setText(R.string.storage_clear_user_data_text)
185                 .setIcon(R.drawable.ic_delete)
186                 .setOnClickListener(i -> handleClearDataClick())
187                 .setEnabled(false);
188         mClearCacheButton
189                 .setText(R.string.storage_clear_cache_btn_text)
190                 .setIcon(R.drawable.ic_delete)
191                 .setOnClickListener(i -> handleClearCacheClick())
192                 .setEnabled(false);
193     }
194 
195     @Override
onStartInternal()196     protected void onStartInternal() {
197         mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
198                 getContext(), UserManager.DISALLOW_APPS_CONTROL, mUserId);
199         mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction(
200                 getContext(), UserManager.DISALLOW_APPS_CONTROL, mUserId);
201     }
202 
203     @Override
updateState(ActionButtonsPreference preference)204     protected void updateState(ActionButtonsPreference preference) {
205         try {
206             mInfo = mPackageManager.getApplicationInfo(mPackageName, 0);
207         } catch (PackageManager.NameNotFoundException e) {
208             LOG.e("Could not find package", e);
209         }
210         if (mInfo == null) {
211             return;
212         }
213         mAppsStorageStatsManager.startLoading(mLoaderManager, mInfo, mUserId, mCacheCleared,
214                 mDataCleared);
215     }
216 
217     @Override
onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, boolean dataCleared)218     public void onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared,
219             boolean dataCleared) {
220         if (data == null || mAppsControlDisallowedBySystem) {
221             mClearStorageButton.setEnabled(false);
222             mClearCacheButton.setEnabled(false);
223         } else {
224             long cacheSize = data.getCacheBytes();
225             long dataSize = data.getDataBytes() - cacheSize;
226 
227             mClearStorageButton.setEnabled(dataSize > 0 && !mDataCleared);
228             mClearCacheButton.setEnabled(cacheSize > 0 && !mCacheCleared);
229         }
230     }
231 
232     @VisibleForTesting
setPackageManager(PackageManager packageManager)233     void setPackageManager(PackageManager packageManager) {
234         mPackageManager = packageManager;
235     }
236 
237     @VisibleForTesting
setAppsControlDisallowedAdmin(RestrictedLockUtils.EnforcedAdmin admin)238     void setAppsControlDisallowedAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
239         mAppsControlDisallowedAdmin = admin;
240     }
241 
242     @VisibleForTesting
setAppsControlDisallowedBySystem(boolean disallowed)243     void setAppsControlDisallowedBySystem(boolean disallowed) {
244         mAppsControlDisallowedBySystem = disallowed;
245     }
246 
handleClearCacheClick()247     private void handleClearCacheClick() {
248         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
249             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
250                     getContext(), mAppsControlDisallowedAdmin);
251             return;
252         }
253         // Lazy initialization of observer.
254         if (mClearCacheObserver == null) {
255             mClearCacheObserver = new ClearCacheObserver();
256         }
257         mPackageManager.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver);
258     }
259 
handleClearDataClick()260     private void handleClearDataClick() {
261         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
262             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
263                     getContext(), mAppsControlDisallowedAdmin);
264         } else {
265             Intent intent = new Intent(Intent.ACTION_DEFAULT);
266             boolean isManageSpaceActivityAvailable = false;
267             if (mAppEntry.info.manageSpaceActivityName != null) {
268                 intent.setClassName(mAppEntry.info.packageName,
269                         mAppEntry.info.manageSpaceActivityName);
270                 isManageSpaceActivityAvailable = mPackageManager.resolveActivity(
271                         intent, /* flags= */ 0) != null;
272             }
273 
274             if (isManageSpaceActivityAvailable) {
275                 getFragmentController().startActivityForResult(intent,
276                         REQUEST_MANAGE_SPACE, /* callback= */ null);
277             } else {
278                 showClearDataDialog();
279             }
280         }
281     }
282 
283     /*
284      * Private method to initiate clearing user data when the user clicks the clear data
285      * button for a system package
286      */
initiateClearUserData()287     private void initiateClearUserData() {
288         mClearStorageButton.setEnabled(false);
289         // Invoke uninstall or clear user data based on sysPackage
290         String packageName = mAppEntry.info.packageName;
291         LOG.i("Clearing user data for package : " + packageName);
292         if (mClearDataObserver == null) {
293             mClearDataObserver = new ClearUserDataObserver();
294         }
295         ActivityManager am = (ActivityManager)
296                 getContext().getSystemService(Context.ACTIVITY_SERVICE);
297         boolean res = am.clearApplicationUserData(packageName, mClearDataObserver);
298         if (!res) {
299             // Clearing data failed for some obscure reason. Just log error for now
300             LOG.i("Couldn't clear application user data for package:" + packageName);
301             showCannotClearDataDialog();
302         }
303     }
304 
305     /*
306      * Private method to handle clear message notification from observer when
307      * the async operation from PackageManager is complete
308      */
processClearMsg(Message msg)309     private void processClearMsg(Message msg) {
310         int result = msg.arg1;
311         String packageName = mAppEntry.info.packageName;
312         if (result == OP_SUCCESSFUL) {
313             LOG.i("Cleared user data for package : " + packageName);
314             refreshUi();
315         } else {
316             mClearStorageButton.setEnabled(true);
317         }
318     }
319 
showClearDataDialog()320     private void showClearDataDialog() {
321         ConfirmationDialogFragment confirmClearStorageDialog =
322                 new ConfirmationDialogFragment.Builder(getContext())
323                         .setTitle(R.string.storage_clear_user_data_text)
324                         .setMessage(getContext().getString(R.string.storage_clear_data_dlg_text))
325                         .setPositiveButton(R.string.okay, mConfirmClearStorageDialog)
326                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
327                         .build();
328         getFragmentController().showDialog(confirmClearStorageDialog,
329                 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(getContext().getString(R.string.storage_clear_failed_dlg_text))
337                         .setPositiveButton(R.string.okay, mConfirmCannotClearStorageDialog)
338                         .build();
339         getFragmentController().showDialog(dialogFragment, CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG);
340     }
341 
342     private final Handler mHandler = new Handler() {
343         public void handleMessage(Message msg) {
344             switch (msg.what) {
345                 case MSG_CLEAR_USER_DATA:
346                     mDataCleared = true;
347                     mCacheCleared = true;
348                     processClearMsg(msg);
349                     break;
350                 case MSG_CLEAR_CACHE:
351                     mCacheCleared = true;
352                     // Refresh info
353                     refreshUi();
354                     break;
355             }
356         }
357     };
358 
359     class ClearCacheObserver extends IPackageDataObserver.Stub {
onRemoveCompleted(final String packageName, final boolean succeeded)360         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
361             Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE);
362             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
363             mHandler.sendMessage(msg);
364         }
365     }
366 
367     class ClearUserDataObserver extends IPackageDataObserver.Stub {
onRemoveCompleted(final String packageName, final boolean succeeded)368         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
369             Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA);
370             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
371             mHandler.sendMessage(msg);
372         }
373     }
374 }
375