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