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