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.providers.media; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.content.DialogInterface; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.graphics.Typeface; 26 import android.os.AsyncTask; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.text.BidiFormatter; 30 import android.text.SpannableString; 31 import android.text.TextPaint; 32 import android.text.TextUtils; 33 import android.text.style.StyleSpan; 34 import android.util.Log; 35 import android.view.View; 36 import android.view.Window; 37 import android.view.WindowManager; 38 import android.widget.ProgressBar; 39 import android.widget.TextView; 40 41 import com.android.providers.media.util.FileUtils; 42 43 public class CacheClearingActivity extends Activity implements DialogInterface.OnClickListener { 44 private static final String TAG = "CacheClearingActivity"; 45 private static final float MAX_APP_NAME_SIZE_PX = 500f; 46 private static final float TEXT_SIZE = 42f; 47 private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L; 48 49 private AlertDialog mActionDialog; 50 private Dialog mLoadingDialog; 51 52 @Override onCreate(Bundle savedInstanceState)53 public void onCreate(Bundle savedInstanceState) { 54 super.onCreate(savedInstanceState); 55 final String packageName = getCallingPackage(); 56 setResult(RESULT_CANCELED); 57 58 if (packageName == null) { 59 finish(); 60 return; 61 } 62 63 final PackageManager packageManager = getPackageManager(); 64 final ApplicationInfo aInfo; 65 try { 66 aInfo = packageManager.getApplicationInfo(packageName, 0); 67 } catch (PackageManager.NameNotFoundException e) { 68 Log.e(TAG, "unable to look up package name", e); 69 finish(); 70 return; 71 } 72 73 if (!MediaProvider.hasPermissionToClearCaches(this, aInfo)) { 74 Log.i(TAG, "Calling package " + packageName + " has no permission clear app caches"); 75 finish(); 76 return; 77 } 78 79 // If the label contains new line characters it may push the security 80 // message below the fold of the dialog. Labels shouldn't have new line 81 // characters anyways, so we just delete all of the newlines (if there are any). 82 final String label = aInfo.loadSafeLabel(packageManager, MAX_APP_NAME_SIZE_PX, 83 TextUtils.SAFE_STRING_FLAG_SINGLE_LINE).toString(); 84 85 createActionDialog(label); 86 mActionDialog.show(); 87 } 88 89 @Override onDestroy()90 protected void onDestroy() { 91 super.onDestroy(); 92 dismissDialogs(mActionDialog, mLoadingDialog); 93 } 94 95 @Override onClick(DialogInterface dialog, int which)96 public void onClick(DialogInterface dialog, int which) { 97 dismissDialogs(mActionDialog); 98 99 if (which == AlertDialog.BUTTON_POSITIVE) { 100 new CacheClearingTask().execute(); 101 } else { 102 finish(); 103 } 104 } 105 106 private class CacheClearingTask extends AsyncTask<Void, Void, Integer> { 107 private long mStartTime; 108 109 @Override onPreExecute()110 protected void onPreExecute() { 111 dismissDialogs(mActionDialog); 112 createLoadingDialog(); 113 mLoadingDialog.show(); 114 mStartTime = System.currentTimeMillis(); 115 } 116 117 @Override doInBackground(Void... unused)118 public Integer doInBackground(Void... unused) { 119 return FileUtils.clearAppCacheDirectories(); 120 } 121 122 @Override onPostExecute(Integer result)123 protected void onPostExecute(Integer result) { 124 // We take the convention of not using primitive wrapper pretty seriously 125 int status = result.intValue(); 126 127 if (result == 0) { 128 setResult(RESULT_OK); 129 } else { 130 setResult(status); 131 } 132 133 // Don't dismiss the progress dialog too quick, it will cause bad UX. 134 final long duration = System.currentTimeMillis() - mStartTime; 135 if (duration > LEAST_SHOW_PROGRESS_TIME_MS) { 136 dismissDialogs(mLoadingDialog); 137 finish(); 138 } else { 139 Handler handler = new Handler(getMainLooper()); 140 handler.postDelayed(() -> { 141 dismissDialogs(mLoadingDialog); 142 finish(); 143 }, LEAST_SHOW_PROGRESS_TIME_MS - duration); 144 } 145 } 146 } 147 createLoadingDialog()148 private void createLoadingDialog() { 149 final CharSequence dialogTitle = getString(R.string.cache_clearing_in_progress_title); 150 final View dialogTitleView = View.inflate(this, R.layout.cache_clearing_dialog, null); 151 final TextView titleText = dialogTitleView.findViewById(R.id.dialog_title); 152 final ProgressBar progressBar = new ProgressBar(CacheClearingActivity.this); 153 final int padding = getResources().getDimensionPixelOffset(R.dimen.dialog_space); 154 155 progressBar.setIndeterminate(true); 156 progressBar.setPadding(0, padding / 2, 0, padding); 157 titleText.setText(dialogTitle); 158 mLoadingDialog = new AlertDialog.Builder(this) 159 .setCustomTitle(dialogTitleView) 160 .setView(progressBar) 161 .setCancelable(false) 162 .create(); 163 164 dialogTitleView.findViewById(R.id.dialog_icon).setVisibility(View.GONE); 165 mLoadingDialog.create(); 166 setDialogOverlaySettings(mActionDialog); 167 } 168 createActionDialog(CharSequence appLabel)169 private void createActionDialog(CharSequence appLabel) { 170 final TextPaint paint = new TextPaint(); 171 paint.setTextSize(TEXT_SIZE); 172 173 final String unsanitizedAppName = TextUtils.ellipsize(appLabel, 174 paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); 175 final String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); 176 final String actionText = getString(R.string.cache_clearing_dialog_text, appName); 177 final CharSequence dialogTitle = getString(R.string.cache_clearing_dialog_title); 178 179 final View dialogTitleView = View.inflate(this, R.layout.cache_clearing_dialog, null); 180 final TextView titleText = dialogTitleView.findViewById(R.id.dialog_title); 181 titleText.setText(dialogTitle); 182 mActionDialog = new AlertDialog.Builder(this) 183 .setCustomTitle(dialogTitleView) 184 .setMessage(actionText) 185 .setPositiveButton(R.string.clear, this) 186 .setNegativeButton(android.R.string.cancel, this) 187 .setCancelable(false) 188 .create(); 189 190 mActionDialog.create(); 191 mActionDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); 192 193 setDialogOverlaySettings(mActionDialog); 194 } 195 setDialogOverlaySettings(Dialog d)196 private static void setDialogOverlaySettings(Dialog d) { 197 final Window w = d.getWindow(); 198 w.addSystemFlags(WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 199 } 200 dismissDialogs(Dialog... dialogs)201 private static void dismissDialogs(Dialog... dialogs) { 202 for (Dialog d : dialogs) { 203 if (d != null) { 204 d.dismiss(); 205 } 206 } 207 } 208 } 209