1 /* 2 * Copyright (C) 2008 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; 18 19 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 20 21 import android.accounts.Account; 22 import android.accounts.AccountManager; 23 import android.accounts.AuthenticatorDescription; 24 import android.app.ActionBar; 25 import android.app.Activity; 26 import android.app.settings.SettingsEnums; 27 import android.content.ComponentName; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.pm.UserInfo; 34 import android.content.res.Resources; 35 import android.graphics.Color; 36 import android.graphics.drawable.Drawable; 37 import android.os.Bundle; 38 import android.os.Environment; 39 import android.os.SystemProperties; 40 import android.os.UserHandle; 41 import android.os.UserManager; 42 import android.provider.Settings; 43 import android.sysprop.VoldProperties; 44 import android.telephony.euicc.EuiccManager; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.view.LayoutInflater; 48 import android.view.View; 49 import android.view.View.OnScrollChangeListener; 50 import android.view.ViewGroup; 51 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 52 import android.widget.Button; 53 import android.widget.CheckBox; 54 import android.widget.ImageView; 55 import android.widget.LinearLayout; 56 import android.widget.ScrollView; 57 import android.widget.TextView; 58 59 import androidx.annotation.VisibleForTesting; 60 61 import com.android.settings.core.InstrumentedFragment; 62 import com.android.settings.core.SubSettingLauncher; 63 import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; 64 import com.android.settings.password.ChooseLockSettingsHelper; 65 import com.android.settings.password.ConfirmLockPattern; 66 import com.android.settingslib.RestrictedLockUtilsInternal; 67 68 import com.google.android.setupcompat.template.FooterBarMixin; 69 import com.google.android.setupcompat.template.FooterButton; 70 import com.google.android.setupcompat.template.FooterButton.ButtonType; 71 import com.google.android.setupdesign.GlifLayout; 72 73 import java.util.List; 74 75 /** 76 * Confirm and execute a reset of the device to a clean "just out of the box" 77 * state. Multiple confirmations are required: first, a general "are you sure 78 * you want to do this?" prompt, followed by a keyguard pattern trace if the user 79 * has defined one, followed by a final strongly-worded "THIS WILL ERASE EVERYTHING 80 * ON THE PHONE" prompt. If at any time the phone is allowed to go to sleep, is 81 * locked, et cetera, then the confirmation sequence is abandoned. 82 * 83 * This is the initial screen. 84 */ 85 public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutListener { 86 private static final String TAG = "MasterClear"; 87 88 @VisibleForTesting 89 static final int KEYGUARD_REQUEST = 55; 90 @VisibleForTesting 91 static final int CREDENTIAL_CONFIRM_REQUEST = 56; 92 93 private static final String KEY_SHOW_ESIM_RESET_CHECKBOX 94 = "masterclear.allow_retain_esim_profiles_after_fdr"; 95 96 static final String ERASE_EXTERNAL_EXTRA = "erase_sd"; 97 static final String ERASE_ESIMS_EXTRA = "erase_esim"; 98 99 private View mContentView; 100 @VisibleForTesting 101 FooterButton mInitiateButton; 102 private View mExternalStorageContainer; 103 @VisibleForTesting 104 CheckBox mExternalStorage; 105 @VisibleForTesting 106 View mEsimStorageContainer; 107 @VisibleForTesting 108 CheckBox mEsimStorage; 109 @VisibleForTesting 110 ScrollView mScrollView; 111 112 @Override onGlobalLayout()113 public void onGlobalLayout() { 114 mInitiateButton.setEnabled(hasReachedBottom(mScrollView)); 115 } 116 setUpActionBarAndTitle()117 private void setUpActionBarAndTitle() { 118 final Activity activity = getActivity(); 119 if (activity == null) { 120 Log.e(TAG, "No activity attached, skipping setUpActionBarAndTitle"); 121 return; 122 } 123 final ActionBar actionBar = activity.getActionBar(); 124 if (actionBar == null) { 125 Log.e(TAG, "No actionbar, skipping setUpActionBarAndTitle"); 126 return; 127 } 128 actionBar.hide(); 129 activity.getWindow().setStatusBarColor(Color.TRANSPARENT); 130 } 131 132 /** 133 * Keyguard validation is run using the standard {@link ConfirmLockPattern} 134 * component as a subactivity 135 * 136 * @param request the request code to be returned once confirmation finishes 137 * @return true if confirmation launched 138 */ runKeyguardConfirmation(int request)139 private boolean runKeyguardConfirmation(int request) { 140 Resources res = getActivity().getResources(); 141 return new ChooseLockSettingsHelper(getActivity(), this).launchConfirmationActivity( 142 request, res.getText(R.string.master_clear_short_title)); 143 } 144 145 @VisibleForTesting isValidRequestCode(int requestCode)146 boolean isValidRequestCode(int requestCode) { 147 return !((requestCode != KEYGUARD_REQUEST) && (requestCode != CREDENTIAL_CONFIRM_REQUEST)); 148 } 149 150 @Override onActivityResult(int requestCode, int resultCode, Intent data)151 public void onActivityResult(int requestCode, int resultCode, Intent data) { 152 super.onActivityResult(requestCode, resultCode, data); 153 onActivityResultInternal(requestCode, resultCode, data); 154 } 155 156 /* 157 * Internal method that allows easy testing without dealing with super references. 158 */ 159 @VisibleForTesting onActivityResultInternal(int requestCode, int resultCode, Intent data)160 void onActivityResultInternal(int requestCode, int resultCode, Intent data) { 161 if (!isValidRequestCode(requestCode)) { 162 return; 163 } 164 165 if (resultCode != Activity.RESULT_OK) { 166 establishInitialState(); 167 return; 168 } 169 170 Intent intent = null; 171 // If returning from a Keyguard request, try to show an account confirmation request if 172 // applciable. 173 if (CREDENTIAL_CONFIRM_REQUEST != requestCode 174 && (intent = getAccountConfirmationIntent()) != null) { 175 showAccountCredentialConfirmation(intent); 176 } else { 177 showFinalConfirmation(); 178 } 179 } 180 181 @VisibleForTesting showFinalConfirmation()182 void showFinalConfirmation() { 183 final Bundle args = new Bundle(); 184 args.putBoolean(ERASE_EXTERNAL_EXTRA, mExternalStorage.isChecked()); 185 args.putBoolean(ERASE_ESIMS_EXTRA, mEsimStorage.isChecked()); 186 new SubSettingLauncher(getContext()) 187 .setDestination(MasterClearConfirm.class.getName()) 188 .setArguments(args) 189 .setTitleRes(R.string.master_clear_confirm_title) 190 .setSourceMetricsCategory(getMetricsCategory()) 191 .launch(); 192 } 193 194 @VisibleForTesting showAccountCredentialConfirmation(Intent intent)195 void showAccountCredentialConfirmation(Intent intent) { 196 startActivityForResult(intent, CREDENTIAL_CONFIRM_REQUEST); 197 } 198 199 @VisibleForTesting getAccountConfirmationIntent()200 Intent getAccountConfirmationIntent() { 201 final Context context = getActivity(); 202 final String accountType = context.getString(R.string.account_type); 203 final String packageName = context.getString(R.string.account_confirmation_package); 204 final String className = context.getString(R.string.account_confirmation_class); 205 if (TextUtils.isEmpty(accountType) 206 || TextUtils.isEmpty(packageName) 207 || TextUtils.isEmpty(className)) { 208 Log.i(TAG, "Resources not set for account confirmation."); 209 return null; 210 } 211 final AccountManager am = AccountManager.get(context); 212 Account[] accounts = am.getAccountsByType(accountType); 213 if (accounts != null && accounts.length > 0) { 214 final Intent requestAccountConfirmation = new Intent() 215 .setPackage(packageName) 216 .setComponent(new ComponentName(packageName, className)); 217 // Check to make sure that the intent is supported. 218 final PackageManager pm = context.getPackageManager(); 219 final ResolveInfo resolution = pm.resolveActivity(requestAccountConfirmation, 0); 220 if (resolution != null 221 && resolution.activityInfo != null 222 && packageName.equals(resolution.activityInfo.packageName)) { 223 // Note that we need to check the packagename to make sure that an Activity resolver 224 // wasn't returned. 225 return requestAccountConfirmation; 226 } else { 227 Log.i(TAG, "Unable to resolve Activity: " + packageName + "/" + className); 228 } 229 } else { 230 Log.d(TAG, "No " + accountType + " accounts installed!"); 231 } 232 return null; 233 } 234 235 /** 236 * If the user clicks to begin the reset sequence, we next require a 237 * keyguard confirmation if the user has currently enabled one. If there 238 * is no keyguard available, we simply go to the final confirmation prompt. 239 * 240 * If the user is in demo mode, route to the demo mode app for confirmation. 241 */ 242 @VisibleForTesting 243 protected final Button.OnClickListener mInitiateListener = new Button.OnClickListener() { 244 245 public void onClick(View view) { 246 final Context context = view.getContext(); 247 if (Utils.isDemoUser(context)) { 248 final ComponentName componentName = Utils.getDeviceOwnerComponent(context); 249 if (componentName != null) { 250 final Intent requestFactoryReset = new Intent() 251 .setPackage(componentName.getPackageName()) 252 .setAction(Intent.ACTION_FACTORY_RESET); 253 context.startActivity(requestFactoryReset); 254 } 255 return; 256 } 257 258 if (runKeyguardConfirmation(KEYGUARD_REQUEST)) { 259 return; 260 } 261 262 Intent intent = getAccountConfirmationIntent(); 263 if (intent != null) { 264 showAccountCredentialConfirmation(intent); 265 } else { 266 showFinalConfirmation(); 267 } 268 } 269 }; 270 271 /** 272 * In its initial state, the activity presents a button for the user to 273 * click in order to initiate a confirmation sequence. This method is 274 * called from various other points in the code to reset the activity to 275 * this base state. 276 * 277 * <p>Reinflating views from resources is expensive and prevents us from 278 * caching widget pointers, so we use a single-inflate pattern: we lazy- 279 * inflate each view, caching all of the widget pointers we'll need at the 280 * time, then simply reuse the inflated views directly whenever we need 281 * to change contents. 282 */ 283 @VisibleForTesting establishInitialState()284 void establishInitialState() { 285 setUpActionBarAndTitle(); 286 setUpInitiateButton(); 287 288 mExternalStorageContainer = mContentView.findViewById(R.id.erase_external_container); 289 mExternalStorage = mContentView.findViewById(R.id.erase_external); 290 mEsimStorageContainer = mContentView.findViewById(R.id.erase_esim_container); 291 mEsimStorage = mContentView.findViewById(R.id.erase_esim); 292 if (mScrollView != null) { 293 mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 294 } 295 mScrollView = mContentView.findViewById(R.id.master_clear_scrollview); 296 297 /* 298 * If the external storage is emulated, it will be erased with a factory 299 * reset at any rate. There is no need to have a separate option until 300 * we have a factory reset that only erases some directories and not 301 * others. Likewise, if it's non-removable storage, it could potentially have been 302 * encrypted, and will also need to be wiped. 303 */ 304 boolean isExtStorageEmulated = Environment.isExternalStorageEmulated(); 305 if (isExtStorageEmulated 306 || (!Environment.isExternalStorageRemovable() && isExtStorageEncrypted())) { 307 mExternalStorageContainer.setVisibility(View.GONE); 308 309 final View externalOption = mContentView.findViewById(R.id.erase_external_option_text); 310 externalOption.setVisibility(View.GONE); 311 312 final View externalAlsoErased = mContentView.findViewById(R.id.also_erases_external); 313 externalAlsoErased.setVisibility(View.VISIBLE); 314 315 // If it's not emulated, it is on a separate partition but it means we're doing 316 // a force wipe due to encryption. 317 mExternalStorage.setChecked(!isExtStorageEmulated); 318 } else { 319 mExternalStorageContainer.setOnClickListener(new View.OnClickListener() { 320 321 @Override 322 public void onClick(View v) { 323 mExternalStorage.toggle(); 324 } 325 }); 326 } 327 328 if (showWipeEuicc()) { 329 if (showWipeEuiccCheckbox()) { 330 mEsimStorageContainer.setVisibility(View.VISIBLE); 331 mEsimStorageContainer.setOnClickListener(new View.OnClickListener() { 332 @Override 333 public void onClick(View v) { 334 mEsimStorage.toggle(); 335 } 336 }); 337 } else { 338 final View esimAlsoErased = mContentView.findViewById(R.id.also_erases_esim); 339 esimAlsoErased.setVisibility(View.VISIBLE); 340 341 final View noCancelMobilePlan = mContentView.findViewById( 342 R.id.no_cancel_mobile_plan); 343 noCancelMobilePlan.setVisibility(View.VISIBLE); 344 mEsimStorage.setChecked(true /* checked */); 345 } 346 } 347 348 final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); 349 loadAccountList(um); 350 final StringBuffer contentDescription = new StringBuffer(); 351 final View masterClearContainer = mContentView.findViewById(R.id.master_clear_container); 352 getContentDescription(masterClearContainer, contentDescription); 353 masterClearContainer.setContentDescription(contentDescription); 354 355 // Set the status of initiateButton based on scrollview 356 mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() { 357 @Override 358 public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, 359 int oldScrollY) { 360 if (v instanceof ScrollView && hasReachedBottom((ScrollView) v)) { 361 mInitiateButton.setEnabled(true); 362 mScrollView.setOnScrollChangeListener(null); 363 } 364 } 365 }); 366 367 // Set the initial state of the initiateButton 368 mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(this); 369 } 370 371 /** 372 * Whether to show strings indicating that the eUICC will be wiped. 373 * 374 * <p>We show the strings on any device which supports eUICC as long as the eUICC was ever 375 * provisioned (that is, at least one profile was ever downloaded onto it). 376 */ 377 @VisibleForTesting showWipeEuicc()378 boolean showWipeEuicc() { 379 Context context = getContext(); 380 if (!isEuiccEnabled(context)) { 381 return false; 382 } 383 ContentResolver cr = context.getContentResolver(); 384 return Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0 385 || Settings.Global.getInt( 386 cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 387 } 388 389 @VisibleForTesting showWipeEuiccCheckbox()390 boolean showWipeEuiccCheckbox() { 391 return SystemProperties 392 .getBoolean(KEY_SHOW_ESIM_RESET_CHECKBOX, false /* def */); 393 } 394 395 @VisibleForTesting isEuiccEnabled(Context context)396 protected boolean isEuiccEnabled(Context context) { 397 EuiccManager euiccManager = (EuiccManager) context.getSystemService(Context.EUICC_SERVICE); 398 return euiccManager.isEnabled(); 399 } 400 401 @VisibleForTesting hasReachedBottom(final ScrollView scrollView)402 boolean hasReachedBottom(final ScrollView scrollView) { 403 if (scrollView.getChildCount() < 1) { 404 return true; 405 } 406 407 final View view = scrollView.getChildAt(0); 408 final int diff = view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()); 409 410 return diff <= 0; 411 } 412 setUpInitiateButton()413 private void setUpInitiateButton() { 414 if (mInitiateButton != null) { 415 return; 416 } 417 418 final GlifLayout layout = mContentView.findViewById(R.id.setup_wizard_layout); 419 final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); 420 mixin.setPrimaryButton( 421 new FooterButton.Builder(getActivity()) 422 .setText(R.string.master_clear_button_text) 423 .setListener(mInitiateListener) 424 .setButtonType(ButtonType.OTHER) 425 .setTheme(R.style.SudGlifButton_Primary) 426 .build() 427 ); 428 mInitiateButton = mixin.getPrimaryButton(); 429 } 430 getContentDescription(View v, StringBuffer description)431 private void getContentDescription(View v, StringBuffer description) { 432 if (v.getVisibility() != View.VISIBLE) { 433 return; 434 } 435 if (v instanceof ViewGroup) { 436 ViewGroup vGroup = (ViewGroup) v; 437 for (int i = 0; i < vGroup.getChildCount(); i++) { 438 View nextChild = vGroup.getChildAt(i); 439 getContentDescription(nextChild, description); 440 } 441 } else if (v instanceof TextView) { 442 TextView vText = (TextView) v; 443 description.append(vText.getText()); 444 description.append(","); // Allow Talkback to pause between sections. 445 } 446 } 447 isExtStorageEncrypted()448 private boolean isExtStorageEncrypted() { 449 String state = VoldProperties.decrypt().orElse(""); 450 return !"".equals(state); 451 } 452 loadAccountList(final UserManager um)453 private void loadAccountList(final UserManager um) { 454 View accountsLabel = mContentView.findViewById(R.id.accounts_label); 455 LinearLayout contents = (LinearLayout) mContentView.findViewById(R.id.accounts); 456 contents.removeAllViews(); 457 458 Context context = getActivity(); 459 final List<UserInfo> profiles = um.getProfiles(UserHandle.myUserId()); 460 final int profilesSize = profiles.size(); 461 462 AccountManager mgr = AccountManager.get(context); 463 464 LayoutInflater inflater = (LayoutInflater) context.getSystemService( 465 Context.LAYOUT_INFLATER_SERVICE); 466 467 int accountsCount = 0; 468 for (int profileIndex = 0; profileIndex < profilesSize; profileIndex++) { 469 final UserInfo userInfo = profiles.get(profileIndex); 470 final int profileId = userInfo.id; 471 final UserHandle userHandle = new UserHandle(profileId); 472 Account[] accounts = mgr.getAccountsAsUser(profileId); 473 final int N = accounts.length; 474 if (N == 0) { 475 continue; 476 } 477 accountsCount += N; 478 479 AuthenticatorDescription[] descs = AccountManager.get(context) 480 .getAuthenticatorTypesAsUser(profileId); 481 final int M = descs.length; 482 483 if (profilesSize > 1) { 484 View titleView = Utils.inflateCategoryHeader(inflater, contents); 485 final TextView titleText = (TextView) titleView.findViewById(android.R.id.title); 486 titleText.setText(userInfo.isManagedProfile() ? R.string.category_work 487 : R.string.category_personal); 488 contents.addView(titleView); 489 } 490 491 for (int i = 0; i < N; i++) { 492 Account account = accounts[i]; 493 AuthenticatorDescription desc = null; 494 for (int j = 0; j < M; j++) { 495 if (account.type.equals(descs[j].type)) { 496 desc = descs[j]; 497 break; 498 } 499 } 500 if (desc == null) { 501 Log.w(TAG, "No descriptor for account name=" + account.name 502 + " type=" + account.type); 503 continue; 504 } 505 Drawable icon = null; 506 try { 507 if (desc.iconId != 0) { 508 Context authContext = context.createPackageContextAsUser(desc.packageName, 509 0, userHandle); 510 icon = context.getPackageManager().getUserBadgedIcon( 511 authContext.getDrawable(desc.iconId), userHandle); 512 } 513 } catch (PackageManager.NameNotFoundException e) { 514 Log.w(TAG, "Bad package name for account type " + desc.type); 515 } catch (Resources.NotFoundException e) { 516 Log.w(TAG, "Invalid icon id for account type " + desc.type, e); 517 } 518 if (icon == null) { 519 icon = context.getPackageManager().getDefaultActivityIcon(); 520 } 521 522 View child = inflater.inflate(R.layout.master_clear_account, contents, false); 523 ((ImageView) child.findViewById(android.R.id.icon)).setImageDrawable(icon); 524 ((TextView) child.findViewById(android.R.id.title)).setText(account.name); 525 contents.addView(child); 526 } 527 } 528 529 if (accountsCount > 0) { 530 accountsLabel.setVisibility(View.VISIBLE); 531 contents.setVisibility(View.VISIBLE); 532 } 533 // Checking for all other users and their profiles if any. 534 View otherUsers = mContentView.findViewById(R.id.other_users_present); 535 final boolean hasOtherUsers = (um.getUserCount() - profilesSize) > 0; 536 otherUsers.setVisibility(hasOtherUsers ? View.VISIBLE : View.GONE); 537 } 538 539 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)540 public View onCreateView(LayoutInflater inflater, ViewGroup container, 541 Bundle savedInstanceState) { 542 final Context context = getContext(); 543 final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, 544 UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId()); 545 final UserManager um = UserManager.get(context); 546 final boolean disallow = !um.isAdminUser() || RestrictedLockUtilsInternal 547 .hasBaseUserRestriction(context, UserManager.DISALLOW_FACTORY_RESET, 548 UserHandle.myUserId()); 549 if (disallow && !Utils.isDemoUser(context)) { 550 return inflater.inflate(R.layout.master_clear_disallowed_screen, null); 551 } else if (admin != null) { 552 new ActionDisabledByAdminDialogHelper(getActivity()) 553 .prepareDialogBuilder(UserManager.DISALLOW_FACTORY_RESET, admin) 554 .setOnDismissListener(__ -> getActivity().finish()) 555 .show(); 556 return new View(getContext()); 557 } 558 559 mContentView = inflater.inflate(R.layout.master_clear, null); 560 561 establishInitialState(); 562 return mContentView; 563 } 564 565 @Override getMetricsCategory()566 public int getMetricsCategory() { 567 return SettingsEnums.MASTER_CLEAR; 568 } 569 } 570