1 /* 2 * Copyright (C) 2019 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.car.developeroptions; 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.car.developeroptions.core.InstrumentedFragment; 62 import com.android.car.developeroptions.core.SubSettingLauncher; 63 import com.android.car.developeroptions.enterprise.ActionDisabledByAdminDialogHelper; 64 import com.android.car.developeroptions.password.ChooseLockSettingsHelper; 65 import com.android.car.developeroptions.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, 186 mEsimStorageContainer.getVisibility() == View.VISIBLE && 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 } 348 349 final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); 350 loadAccountList(um); 351 final StringBuffer contentDescription = new StringBuffer(); 352 final View masterClearContainer = mContentView.findViewById(R.id.master_clear_container); 353 getContentDescription(masterClearContainer, contentDescription); 354 masterClearContainer.setContentDescription(contentDescription); 355 356 // Set the status of initiateButton based on scrollview 357 mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() { 358 @Override 359 public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, 360 int oldScrollY) { 361 if (v instanceof ScrollView && hasReachedBottom((ScrollView) v)) { 362 mInitiateButton.setEnabled(true); 363 mScrollView.setOnScrollChangeListener(null); 364 } 365 } 366 }); 367 368 // Set the initial state of the initiateButton 369 mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(this); 370 } 371 372 /** 373 * Whether to show strings indicating that the eUICC will be wiped. 374 * 375 * <p>We show the strings on any device which supports eUICC as long as the eUICC was ever 376 * provisioned (that is, at least one profile was ever downloaded onto it). 377 */ 378 @VisibleForTesting showWipeEuicc()379 boolean showWipeEuicc() { 380 Context context = getContext(); 381 if (!isEuiccEnabled(context)) { 382 return false; 383 } 384 ContentResolver cr = context.getContentResolver(); 385 return Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0 386 || Settings.Global.getInt( 387 cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 388 } 389 390 @VisibleForTesting showWipeEuiccCheckbox()391 boolean showWipeEuiccCheckbox() { 392 return SystemProperties 393 .getBoolean(KEY_SHOW_ESIM_RESET_CHECKBOX, false /* def */); 394 } 395 396 @VisibleForTesting isEuiccEnabled(Context context)397 protected boolean isEuiccEnabled(Context context) { 398 EuiccManager euiccManager = (EuiccManager) context.getSystemService(Context.EUICC_SERVICE); 399 return euiccManager.isEnabled(); 400 } 401 402 @VisibleForTesting hasReachedBottom(final ScrollView scrollView)403 boolean hasReachedBottom(final ScrollView scrollView) { 404 if (scrollView.getChildCount() < 1) { 405 return true; 406 } 407 408 final View view = scrollView.getChildAt(0); 409 final int diff = view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()); 410 411 return diff <= 0; 412 } 413 setUpInitiateButton()414 private void setUpInitiateButton() { 415 if (mInitiateButton != null) { 416 return; 417 } 418 419 final GlifLayout layout = mContentView.findViewById(R.id.setup_wizard_layout); 420 final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); 421 mixin.setPrimaryButton( 422 new FooterButton.Builder(getActivity()) 423 .setText(R.string.master_clear_button_text) 424 .setListener(mInitiateListener) 425 .setButtonType(ButtonType.OTHER) 426 .setTheme(R.style.SudGlifButton_Primary) 427 .build() 428 ); 429 mInitiateButton = mixin.getPrimaryButton(); 430 } 431 getContentDescription(View v, StringBuffer description)432 private void getContentDescription(View v, StringBuffer description) { 433 if (v.getVisibility() != View.VISIBLE) { 434 return; 435 } 436 if (v instanceof ViewGroup) { 437 ViewGroup vGroup = (ViewGroup) v; 438 for (int i = 0; i < vGroup.getChildCount(); i++) { 439 View nextChild = vGroup.getChildAt(i); 440 getContentDescription(nextChild, description); 441 } 442 } else if (v instanceof TextView) { 443 TextView vText = (TextView) v; 444 description.append(vText.getText()); 445 description.append(","); // Allow Talkback to pause between sections. 446 } 447 } 448 isExtStorageEncrypted()449 private boolean isExtStorageEncrypted() { 450 String state = VoldProperties.decrypt().orElse(""); 451 return !"".equals(state); 452 } 453 loadAccountList(final UserManager um)454 private void loadAccountList(final UserManager um) { 455 View accountsLabel = mContentView.findViewById(R.id.accounts_label); 456 LinearLayout contents = (LinearLayout) mContentView.findViewById(R.id.accounts); 457 contents.removeAllViews(); 458 459 Context context = getActivity(); 460 final List<UserInfo> profiles = um.getProfiles(UserHandle.myUserId()); 461 final int profilesSize = profiles.size(); 462 463 AccountManager mgr = AccountManager.get(context); 464 465 LayoutInflater inflater = (LayoutInflater) context.getSystemService( 466 Context.LAYOUT_INFLATER_SERVICE); 467 468 int accountsCount = 0; 469 for (int profileIndex = 0; profileIndex < profilesSize; profileIndex++) { 470 final UserInfo userInfo = profiles.get(profileIndex); 471 final int profileId = userInfo.id; 472 final UserHandle userHandle = new UserHandle(profileId); 473 Account[] accounts = mgr.getAccountsAsUser(profileId); 474 final int N = accounts.length; 475 if (N == 0) { 476 continue; 477 } 478 accountsCount += N; 479 480 AuthenticatorDescription[] descs = AccountManager.get(context) 481 .getAuthenticatorTypesAsUser(profileId); 482 final int M = descs.length; 483 484 if (profilesSize > 1) { 485 View titleView = Utils.inflateCategoryHeader(inflater, contents); 486 final TextView titleText = (TextView) titleView.findViewById(android.R.id.title); 487 titleText.setText(userInfo.isManagedProfile() ? R.string.category_work 488 : R.string.category_personal); 489 contents.addView(titleView); 490 } 491 492 for (int i = 0; i < N; i++) { 493 Account account = accounts[i]; 494 AuthenticatorDescription desc = null; 495 for (int j = 0; j < M; j++) { 496 if (account.type.equals(descs[j].type)) { 497 desc = descs[j]; 498 break; 499 } 500 } 501 if (desc == null) { 502 Log.w(TAG, "No descriptor for account name=" + account.name 503 + " type=" + account.type); 504 continue; 505 } 506 Drawable icon = null; 507 try { 508 if (desc.iconId != 0) { 509 Context authContext = context.createPackageContextAsUser(desc.packageName, 510 0, userHandle); 511 icon = context.getPackageManager().getUserBadgedIcon( 512 authContext.getDrawable(desc.iconId), userHandle); 513 } 514 } catch (PackageManager.NameNotFoundException e) { 515 Log.w(TAG, "Bad package name for account type " + desc.type); 516 } catch (Resources.NotFoundException e) { 517 Log.w(TAG, "Invalid icon id for account type " + desc.type, e); 518 } 519 if (icon == null) { 520 icon = context.getPackageManager().getDefaultActivityIcon(); 521 } 522 523 View child = inflater.inflate(R.layout.master_clear_account, contents, false); 524 ((ImageView) child.findViewById(android.R.id.icon)).setImageDrawable(icon); 525 ((TextView) child.findViewById(android.R.id.title)).setText(account.name); 526 contents.addView(child); 527 } 528 } 529 530 if (accountsCount > 0) { 531 accountsLabel.setVisibility(View.VISIBLE); 532 contents.setVisibility(View.VISIBLE); 533 } 534 // Checking for all other users and their profiles if any. 535 View otherUsers = mContentView.findViewById(R.id.other_users_present); 536 final boolean hasOtherUsers = (um.getUserCount() - profilesSize) > 0; 537 otherUsers.setVisibility(hasOtherUsers ? View.VISIBLE : View.GONE); 538 } 539 540 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)541 public View onCreateView(LayoutInflater inflater, ViewGroup container, 542 Bundle savedInstanceState) { 543 final Context context = getContext(); 544 final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, 545 UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId()); 546 final UserManager um = UserManager.get(context); 547 final boolean disallow = !um.isAdminUser() || RestrictedLockUtilsInternal 548 .hasBaseUserRestriction(context, UserManager.DISALLOW_FACTORY_RESET, 549 UserHandle.myUserId()); 550 if (disallow && !Utils.isDemoUser(context)) { 551 return inflater.inflate(R.layout.master_clear_disallowed_screen, null); 552 } else if (admin != null) { 553 new ActionDisabledByAdminDialogHelper(getActivity()) 554 .prepareDialogBuilder(UserManager.DISALLOW_FACTORY_RESET, admin) 555 .setOnDismissListener(__ -> getActivity().finish()) 556 .show(); 557 return new View(getContext()); 558 } 559 560 mContentView = inflater.inflate(R.layout.master_clear, null); 561 562 establishInitialState(); 563 return mContentView; 564 } 565 566 @Override getMetricsCategory()567 public int getMetricsCategory() { 568 return SettingsEnums.MASTER_CLEAR; 569 } 570 } 571