1 /*
2  * Copyright (C) 2015 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.applications;
18 
19 import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
20 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
21 
22 import android.app.ActivityManager;
23 import android.app.AppGlobals;
24 import android.app.GrantedUriPermission;
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.IPackageDataObserver;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ProviderInfo;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Message;
36 import android.os.RemoteException;
37 import android.os.UserHandle;
38 import android.os.storage.StorageManager;
39 import android.os.storage.VolumeInfo;
40 import android.util.Log;
41 import android.util.MutableInt;
42 import android.view.View;
43 import android.view.View.OnClickListener;
44 import android.widget.Button;
45 
46 import androidx.annotation.VisibleForTesting;
47 import androidx.appcompat.app.AlertDialog;
48 import androidx.loader.app.LoaderManager;
49 import androidx.loader.content.Loader;
50 import androidx.preference.Preference;
51 import androidx.preference.PreferenceCategory;
52 
53 import com.android.settings.R;
54 import com.android.settings.Utils;
55 import com.android.settings.deviceinfo.StorageWizardMoveConfirm;
56 import com.android.settingslib.RestrictedLockUtils;
57 import com.android.settingslib.applications.ApplicationsState.Callbacks;
58 import com.android.settingslib.applications.StorageStatsSource;
59 import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;
60 import com.android.settingslib.widget.ActionButtonsPreference;
61 import com.android.settingslib.widget.LayoutPreference;
62 
63 import java.util.Collections;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Objects;
67 import java.util.TreeMap;
68 
69 public class AppStorageSettings extends AppInfoWithHeader
70         implements OnClickListener, Callbacks, DialogInterface.OnClickListener,
71         LoaderManager.LoaderCallbacks<AppStorageStats> {
72     private static final String TAG = AppStorageSettings.class.getSimpleName();
73 
74     //internal constants used in Handler
75     private static final int OP_SUCCESSFUL = 1;
76     private static final int OP_FAILED = 2;
77     private static final int MSG_CLEAR_USER_DATA = 1;
78     private static final int MSG_CLEAR_CACHE = 3;
79 
80     // invalid size value used initially and also when size retrieval through PackageManager
81     // fails for whatever reason
82     private static final int SIZE_INVALID = -1;
83 
84     // Result code identifiers
85     public static final int REQUEST_MANAGE_SPACE = 2;
86 
87     private static final int DLG_CLEAR_DATA = DLG_BASE + 1;
88     private static final int DLG_CANNOT_CLEAR_DATA = DLG_BASE + 2;
89 
90     private static final String KEY_STORAGE_USED = "storage_used";
91     private static final String KEY_CHANGE_STORAGE = "change_storage_button";
92     private static final String KEY_STORAGE_SPACE = "storage_space";
93     private static final String KEY_STORAGE_CATEGORY = "storage_category";
94 
95     private static final String KEY_TOTAL_SIZE = "total_size";
96     private static final String KEY_APP_SIZE = "app_size";
97     private static final String KEY_DATA_SIZE = "data_size";
98     private static final String KEY_CACHE_SIZE = "cache_size";
99 
100     private static final String KEY_HEADER_BUTTONS = "header_view";
101 
102     private static final String KEY_URI_CATEGORY = "uri_category";
103     private static final String KEY_CLEAR_URI = "clear_uri_button";
104 
105     private static final String KEY_CACHE_CLEARED = "cache_cleared";
106     private static final String KEY_DATA_CLEARED = "data_cleared";
107 
108     // Views related to cache info
109     @VisibleForTesting
110     ActionButtonsPreference mButtonsPref;
111 
112     private Preference mStorageUsed;
113     private Button mChangeStorageButton;
114 
115     // Views related to URI permissions
116     private Button mClearUriButton;
117     private LayoutPreference mClearUri;
118     private PreferenceCategory mUri;
119 
120     private boolean mCanClearData = true;
121     private boolean mCacheCleared;
122     private boolean mDataCleared;
123 
124     @VisibleForTesting
125     AppStorageSizesController mSizeController;
126 
127     private ClearCacheObserver mClearCacheObserver;
128     private ClearUserDataObserver mClearDataObserver;
129 
130     private VolumeInfo[] mCandidates;
131     private AlertDialog.Builder mDialogBuilder;
132     private ApplicationInfo mInfo;
133 
134     @Override
onCreate(Bundle savedInstanceState)135     public void onCreate(Bundle savedInstanceState) {
136         super.onCreate(savedInstanceState);
137         if (savedInstanceState != null) {
138             mCacheCleared = savedInstanceState.getBoolean(KEY_CACHE_CLEARED, false);
139             mDataCleared = savedInstanceState.getBoolean(KEY_DATA_CLEARED, false);
140             mCacheCleared = mCacheCleared || mDataCleared;
141         }
142 
143         addPreferencesFromResource(R.xml.app_storage_settings);
144         setupViews();
145         initMoveDialog();
146     }
147 
148     @Override
onResume()149     public void onResume() {
150         super.onResume();
151         updateSize();
152     }
153 
154     @Override
onSaveInstanceState(Bundle outState)155     public void onSaveInstanceState(Bundle outState) {
156         super.onSaveInstanceState(outState);
157         outState.putBoolean(KEY_CACHE_CLEARED, mCacheCleared);
158         outState.putBoolean(KEY_DATA_CLEARED, mDataCleared);
159     }
160 
setupViews()161     private void setupViews() {
162         // Set default values on sizes
163         mSizeController = new AppStorageSizesController.Builder()
164                 .setTotalSizePreference(findPreference(KEY_TOTAL_SIZE))
165                 .setAppSizePreference(findPreference(KEY_APP_SIZE))
166                 .setDataSizePreference(findPreference(KEY_DATA_SIZE))
167                 .setCacheSizePreference(findPreference(KEY_CACHE_SIZE))
168                 .setComputingString(R.string.computing_size)
169                 .setErrorString(R.string.invalid_size_value)
170                 .build();
171         mButtonsPref = ((ActionButtonsPreference) findPreference(KEY_HEADER_BUTTONS));
172         mStorageUsed = findPreference(KEY_STORAGE_USED);
173         mChangeStorageButton = (Button) ((LayoutPreference) findPreference(KEY_CHANGE_STORAGE))
174                 .findViewById(R.id.button);
175         mChangeStorageButton.setText(R.string.change);
176         mChangeStorageButton.setOnClickListener(this);
177 
178         // Cache section
179         mButtonsPref
180                 .setButton2Text(R.string.clear_cache_btn_text)
181                 .setButton2Icon(R.drawable.ic_settings_delete);
182 
183         // URI permissions section
184         mUri = (PreferenceCategory) findPreference(KEY_URI_CATEGORY);
185         mClearUri = (LayoutPreference) mUri.findPreference(KEY_CLEAR_URI);
186         mClearUriButton = (Button) mClearUri.findViewById(R.id.button);
187         mClearUriButton.setText(R.string.clear_uri_btn_text);
188         mClearUriButton.setOnClickListener(this);
189     }
190 
191     @VisibleForTesting
handleClearCacheClick()192     void handleClearCacheClick() {
193         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
194             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
195                     getActivity(), mAppsControlDisallowedAdmin);
196             return;
197         } else if (mClearCacheObserver == null) { // Lazy initialization of observer
198             mClearCacheObserver = new ClearCacheObserver();
199         }
200         mMetricsFeatureProvider.action(getContext(),
201                 SettingsEnums.ACTION_SETTINGS_CLEAR_APP_CACHE);
202         mPm.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver);
203     }
204 
205     @VisibleForTesting
handleClearDataClick()206     void handleClearDataClick() {
207         if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
208             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
209                     getActivity(), mAppsControlDisallowedAdmin);
210         } else if (mAppEntry.info.manageSpaceActivityName != null) {
211             if (!Utils.isMonkeyRunning()) {
212                 Intent intent = new Intent(Intent.ACTION_DEFAULT);
213                 intent.setClassName(mAppEntry.info.packageName,
214                         mAppEntry.info.manageSpaceActivityName);
215                 startActivityForResult(intent, REQUEST_MANAGE_SPACE);
216             }
217         } else {
218             showDialogInner(DLG_CLEAR_DATA, 0);
219         }
220     }
221 
222     @Override
onClick(View v)223     public void onClick(View v) {
224         if (v == mChangeStorageButton && mDialogBuilder != null && !isMoveInProgress()) {
225             mDialogBuilder.show();
226         } else if (v == mClearUriButton) {
227             if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
228                 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
229                         getActivity(), mAppsControlDisallowedAdmin);
230             } else {
231                 clearUriPermissions();
232             }
233         }
234     }
235 
isMoveInProgress()236     private boolean isMoveInProgress() {
237         try {
238             // TODO: define a cleaner API for this
239             AppGlobals.getPackageManager().checkPackageStartable(mPackageName,
240                     UserHandle.myUserId());
241             return false;
242         } catch (RemoteException | SecurityException e) {
243             return true;
244         }
245     }
246 
247     @Override
onClick(DialogInterface dialog, int which)248     public void onClick(DialogInterface dialog, int which) {
249         final Context context = getActivity();
250 
251         // If not current volume, kick off move wizard
252         final VolumeInfo targetVol = mCandidates[which];
253         final VolumeInfo currentVol = context.getPackageManager().getPackageCurrentVolume(
254                 mAppEntry.info);
255         if (!Objects.equals(targetVol, currentVol)) {
256             final Intent intent = new Intent(context, StorageWizardMoveConfirm.class);
257             intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, targetVol.getId());
258             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mAppEntry.info.packageName);
259             startActivity(intent);
260         }
261         dialog.dismiss();
262     }
263 
264     @Override
refreshUi()265     protected boolean refreshUi() {
266         retrieveAppEntry();
267         if (mAppEntry == null) {
268             return false;
269         }
270         updateUiWithSize(mSizeController.getLastResult());
271         refreshGrantedUriPermissions();
272 
273         final VolumeInfo currentVol = getActivity().getPackageManager()
274                 .getPackageCurrentVolume(mAppEntry.info);
275         final StorageManager storage = getContext().getSystemService(StorageManager.class);
276         mStorageUsed.setSummary(storage.getBestVolumeDescription(currentVol));
277 
278         refreshButtons();
279 
280         return true;
281     }
282 
refreshButtons()283     private void refreshButtons() {
284         initMoveDialog();
285         initDataButtons();
286     }
287 
initDataButtons()288     private void initDataButtons() {
289         final boolean appHasSpaceManagementUI = mAppEntry.info.manageSpaceActivityName != null;
290         final boolean appHasActiveAdmins = mDpm.packageHasActiveAdmins(mPackageName);
291         // Check that SYSTEM_APP flag is set, and ALLOW_CLEAR_USER_DATA is not set.
292         final boolean isNonClearableSystemApp =
293                 (mAppEntry.info.flags & (FLAG_SYSTEM | FLAG_ALLOW_CLEAR_USER_DATA)) == FLAG_SYSTEM;
294         final boolean appRestrictsClearingData = isNonClearableSystemApp || appHasActiveAdmins;
295 
296         final Intent intent = new Intent(Intent.ACTION_DEFAULT);
297         if (appHasSpaceManagementUI) {
298             intent.setClassName(mAppEntry.info.packageName, mAppEntry.info.manageSpaceActivityName);
299         }
300         final boolean isManageSpaceActivityAvailable =
301                 getPackageManager().resolveActivity(intent, 0) != null;
302 
303         if ((!appHasSpaceManagementUI && appRestrictsClearingData)
304                 || !isManageSpaceActivityAvailable) {
305             mButtonsPref
306                     .setButton1Text(R.string.clear_user_data_text)
307                     .setButton1Icon(R.drawable.ic_settings_delete)
308                     .setButton1Enabled(false);
309             mCanClearData = false;
310         } else {
311             if (appHasSpaceManagementUI) {
312                 mButtonsPref.setButton1Text(R.string.manage_space_text);
313             } else {
314                 mButtonsPref.setButton1Text(R.string.clear_user_data_text);
315             }
316             mButtonsPref.setButton1Icon(R.drawable.ic_settings_delete)
317                     .setButton1OnClickListener(v -> handleClearDataClick());
318         }
319 
320         if (mAppsControlDisallowedBySystem) {
321             mButtonsPref.setButton1Enabled(false);
322         }
323     }
324 
initMoveDialog()325     private void initMoveDialog() {
326         final Context context = getActivity();
327         final StorageManager storage = context.getSystemService(StorageManager.class);
328 
329         final List<VolumeInfo> candidates = context.getPackageManager()
330                 .getPackageCandidateVolumes(mAppEntry.info);
331         if (candidates.size() > 1) {
332             Collections.sort(candidates, VolumeInfo.getDescriptionComparator());
333 
334             CharSequence[] labels = new CharSequence[candidates.size()];
335             int current = -1;
336             for (int i = 0; i < candidates.size(); i++) {
337                 final String volDescrip = storage.getBestVolumeDescription(candidates.get(i));
338                 if (Objects.equals(volDescrip, mStorageUsed.getSummary())) {
339                     current = i;
340                 }
341                 labels[i] = volDescrip;
342             }
343             mCandidates = candidates.toArray(new VolumeInfo[candidates.size()]);
344             mDialogBuilder = new AlertDialog.Builder(getContext())
345                     .setTitle(R.string.change_storage)
346                     .setSingleChoiceItems(labels, current, this)
347                     .setNegativeButton(R.string.cancel, null);
348         } else {
349             removePreference(KEY_STORAGE_USED);
350             removePreference(KEY_CHANGE_STORAGE);
351             removePreference(KEY_STORAGE_SPACE);
352         }
353     }
354 
355     /*
356      * Private method to initiate clearing user data when the user clicks the clear data
357      * button for a system package
358      */
initiateClearUserData()359     private void initiateClearUserData() {
360         mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_SETTINGS_CLEAR_APP_DATA);
361         mButtonsPref.setButton1Enabled(false);
362         // Invoke uninstall or clear user data based on sysPackage
363         String packageName = mAppEntry.info.packageName;
364         Log.i(TAG, "Clearing user data for package : " + packageName);
365         if (mClearDataObserver == null) {
366             mClearDataObserver = new ClearUserDataObserver();
367         }
368         ActivityManager am = (ActivityManager)
369                 getActivity().getSystemService(Context.ACTIVITY_SERVICE);
370         boolean res = am.clearApplicationUserData(packageName, mClearDataObserver);
371         if (!res) {
372             // Clearing data failed for some obscure reason. Just log error for now
373             Log.i(TAG, "Couldn't clear application user data for package:" + packageName);
374             showDialogInner(DLG_CANNOT_CLEAR_DATA, 0);
375         } else {
376             mButtonsPref.setButton1Text(R.string.recompute_size);
377         }
378     }
379 
380     /*
381      * Private method to handle clear message notification from observer when
382      * the async operation from PackageManager is complete
383      */
processClearMsg(Message msg)384     private void processClearMsg(Message msg) {
385         int result = msg.arg1;
386         String packageName = mAppEntry.info.packageName;
387         mButtonsPref
388                 .setButton1Text(R.string.clear_user_data_text)
389                 .setButton1Icon(R.drawable.ic_settings_delete);
390         if (result == OP_SUCCESSFUL) {
391             Log.i(TAG, "Cleared user data for package : " + packageName);
392             updateSize();
393         } else {
394             mButtonsPref.setButton1Enabled(true);
395         }
396     }
397 
refreshGrantedUriPermissions()398     private void refreshGrantedUriPermissions() {
399         // Clear UI first (in case the activity has been resumed)
400         removeUriPermissionsFromUi();
401 
402         // Gets all URI permissions from am.
403         ActivityManager am = (ActivityManager) getActivity().getSystemService(
404                 Context.ACTIVITY_SERVICE);
405         List<GrantedUriPermission> perms =
406                 am.getGrantedUriPermissions(mAppEntry.info.packageName).getList();
407 
408         if (perms.isEmpty()) {
409             mClearUriButton.setVisibility(View.GONE);
410             return;
411         }
412 
413         PackageManager pm = getActivity().getPackageManager();
414 
415         // Group number of URIs by app.
416         Map<CharSequence, MutableInt> uriCounters = new TreeMap<>();
417         for (GrantedUriPermission perm : perms) {
418             String authority = perm.uri.getAuthority();
419             ProviderInfo provider = pm.resolveContentProvider(authority, 0);
420             if (provider == null) {
421                 continue;
422             }
423 
424             CharSequence app = provider.applicationInfo.loadLabel(pm);
425             MutableInt count = uriCounters.get(app);
426             if (count == null) {
427                 uriCounters.put(app, new MutableInt(1));
428             } else {
429                 count.value++;
430             }
431         }
432 
433         // Dynamically add the preferences, one per app.
434         int order = 0;
435         for (Map.Entry<CharSequence, MutableInt> entry : uriCounters.entrySet()) {
436             int numberResources = entry.getValue().value;
437             Preference pref = new Preference(getPrefContext());
438             pref.setTitle(entry.getKey());
439             pref.setSummary(getPrefContext().getResources()
440                     .getQuantityString(R.plurals.uri_permissions_text, numberResources,
441                             numberResources));
442             pref.setSelectable(false);
443             pref.setLayoutResource(R.layout.horizontal_preference);
444             pref.setOrder(order);
445             Log.v(TAG, "Adding preference '" + pref + "' at order " + order);
446             mUri.addPreference(pref);
447         }
448 
449         if (mAppsControlDisallowedBySystem) {
450             mClearUriButton.setEnabled(false);
451         }
452 
453         mClearUri.setOrder(order);
454         mClearUriButton.setVisibility(View.VISIBLE);
455 
456     }
457 
clearUriPermissions()458     private void clearUriPermissions() {
459         final Context context = getActivity();
460         final String packageName = mAppEntry.info.packageName;
461         // Synchronously revoke the permissions.
462         final ActivityManager am = (ActivityManager) context.getSystemService(
463                 Context.ACTIVITY_SERVICE);
464         am.clearGrantedUriPermissions(packageName);
465 
466         // Update UI
467         refreshGrantedUriPermissions();
468     }
469 
removeUriPermissionsFromUi()470     private void removeUriPermissionsFromUi() {
471         // Remove all preferences but the clear button.
472         int count = mUri.getPreferenceCount();
473         for (int i = count - 1; i >= 0; i--) {
474             Preference pref = mUri.getPreference(i);
475             if (pref != mClearUri) {
476                 mUri.removePreference(pref);
477             }
478         }
479     }
480 
481     @Override
createDialog(int id, int errorCode)482     protected AlertDialog createDialog(int id, int errorCode) {
483         switch (id) {
484             case DLG_CLEAR_DATA:
485                 return new AlertDialog.Builder(getActivity())
486                         .setTitle(getActivity().getText(R.string.clear_data_dlg_title))
487                         .setMessage(getActivity().getText(R.string.clear_data_dlg_text))
488                         .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() {
489                             public void onClick(DialogInterface dialog, int which) {
490                                 // Clear user data here
491                                 initiateClearUserData();
492                             }
493                         })
494                         .setNegativeButton(R.string.dlg_cancel, null)
495                         .create();
496             case DLG_CANNOT_CLEAR_DATA:
497                 return new AlertDialog.Builder(getActivity())
498                         .setTitle(getActivity().getText(R.string.clear_user_data_text))
499                         .setMessage(getActivity().getText(R.string.clear_failed_dlg_text))
500                         .setNeutralButton(R.string.dlg_ok, new DialogInterface.OnClickListener() {
501                             public void onClick(DialogInterface dialog, int which) {
502                                 mButtonsPref.setButton1Enabled(false);
503                                 //force to recompute changed value
504                                 setIntentAndFinish(false  /* appChanged */);
505                             }
506                         })
507                         .create();
508         }
509         return null;
510     }
511 
512     @Override
513     public void onPackageSizeChanged(String packageName) {
514     }
515 
516     @Override
517     public Loader<AppStorageStats> onCreateLoader(int id, Bundle args) {
518         Context context = getContext();
519         return new FetchPackageStorageAsyncLoader(
520                 context, new StorageStatsSource(context), mInfo, UserHandle.of(mUserId));
521     }
522 
523     @Override
524     public void onLoadFinished(Loader<AppStorageStats> loader, AppStorageStats result) {
525         mSizeController.setResult(result);
526         updateUiWithSize(result);
527     }
528 
529     @Override
530     public void onLoaderReset(Loader<AppStorageStats> loader) {
531     }
532 
533     private void updateSize() {
534         PackageManager packageManager = getPackageManager();
535         try {
536             mInfo = packageManager.getApplicationInfo(mPackageName, 0);
537         } catch (PackageManager.NameNotFoundException e) {
538             Log.e(TAG, "Could not find package", e);
539         }
540 
541         if (mInfo == null) {
542             return;
543         }
544 
545         getLoaderManager().restartLoader(1, Bundle.EMPTY, this);
546     }
547 
548     @VisibleForTesting
549     void updateUiWithSize(AppStorageStats result) {
550         if (mCacheCleared) {
551             mSizeController.setCacheCleared(true);
552         }
553         if (mDataCleared) {
554             mSizeController.setDataCleared(true);
555         }
556 
557         mSizeController.updateUi(getContext());
558 
559         if (result == null) {
560             mButtonsPref.setButton1Enabled(false).setButton2Enabled(false);
561         } else {
562             long cacheSize = result.getCacheBytes();
563             long dataSize = result.getDataBytes() - cacheSize;
564 
565             if (dataSize <= 0 || !mCanClearData || mDataCleared) {
566                 mButtonsPref.setButton1Enabled(false);
567             } else {
568                 mButtonsPref.setButton1Enabled(true)
569                         .setButton1OnClickListener(v -> handleClearDataClick());
570             }
571             if (cacheSize <= 0 || mCacheCleared) {
572                 mButtonsPref.setButton2Enabled(false);
573             } else {
574                 mButtonsPref.setButton2Enabled(true)
575                         .setButton2OnClickListener(v -> handleClearCacheClick());
576             }
577         }
578         if (mAppsControlDisallowedBySystem) {
579             mButtonsPref.setButton1Enabled(false).setButton2Enabled(false);
580         }
581     }
582 
583     private final Handler mHandler = new Handler() {
584         public void handleMessage(Message msg) {
585             if (getView() == null) {
586                 return;
587             }
588             switch (msg.what) {
589                 case MSG_CLEAR_USER_DATA:
590                     mDataCleared = true;
591                     mCacheCleared = true;
592                     processClearMsg(msg);
593                     break;
594                 case MSG_CLEAR_CACHE:
595                     mCacheCleared = true;
596                     // Refresh size info
597                     updateSize();
598                     break;
599             }
600         }
601     };
602 
603     @Override
604     public int getMetricsCategory() {
605         return SettingsEnums.APPLICATIONS_APP_STORAGE;
606     }
607 
608     class ClearCacheObserver extends IPackageDataObserver.Stub {
609         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
610             final Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE);
611             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
612             mHandler.sendMessage(msg);
613         }
614     }
615 
616     class ClearUserDataObserver extends IPackageDataObserver.Stub {
617         public void onRemoveCompleted(final String packageName, final boolean succeeded) {
618             final Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA);
619             msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
620             mHandler.sendMessage(msg);
621         }
622     }
623 }
624