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