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 android.app.settings.SettingsEnums; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.PixelFormat; 27 import android.os.AsyncResult; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.telephony.SubscriptionInfo; 32 import android.telephony.SubscriptionManager; 33 import android.telephony.TelephonyManager; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.view.Gravity; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.WindowInsets.Type; 41 import android.view.WindowManager; 42 import android.widget.EditText; 43 import android.widget.ListView; 44 import android.widget.TabHost; 45 import android.widget.TabHost.OnTabChangeListener; 46 import android.widget.TabHost.TabContentFactory; 47 import android.widget.TabHost.TabSpec; 48 import android.widget.TabWidget; 49 import android.widget.TextView; 50 import android.widget.Toast; 51 52 import androidx.preference.Preference; 53 import androidx.preference.SwitchPreference; 54 55 import com.android.internal.telephony.CommandException; 56 import com.android.internal.telephony.Phone; 57 import com.android.internal.telephony.PhoneFactory; 58 import com.android.internal.telephony.TelephonyIntents; 59 60 /** 61 * Implements the preference screen to enable/disable ICC lock and 62 * also the dialogs to change the ICC PIN. In the former case, enabling/disabling 63 * the ICC lock will prompt the user for the current PIN. 64 * In the Change PIN case, it prompts the user for old pin, new pin and new pin 65 * again before attempting to change it. Calls the SimCard interface to execute 66 * these operations. 67 * 68 */ 69 public class IccLockSettings extends SettingsPreferenceFragment 70 implements EditPinPreference.OnPinEnteredListener { 71 private static final String TAG = "IccLockSettings"; 72 private static final boolean DBG = true; 73 74 private static final int OFF_MODE = 0; 75 // State when enabling/disabling ICC lock 76 private static final int ICC_LOCK_MODE = 1; 77 // State when entering the old pin 78 private static final int ICC_OLD_MODE = 2; 79 // State when entering the new pin - first time 80 private static final int ICC_NEW_MODE = 3; 81 // State when entering the new pin - second time 82 private static final int ICC_REENTER_MODE = 4; 83 84 // Keys in xml file 85 private static final String PIN_DIALOG = "sim_pin"; 86 private static final String PIN_TOGGLE = "sim_toggle"; 87 // Keys in icicle 88 private static final String DIALOG_STATE = "dialogState"; 89 private static final String DIALOG_PIN = "dialogPin"; 90 private static final String DIALOG_ERROR = "dialogError"; 91 private static final String ENABLE_TO_STATE = "enableState"; 92 private static final String CURRENT_TAB = "currentTab"; 93 94 // Save and restore inputted PIN code when configuration changed 95 // (ex. portrait<-->landscape) during change PIN code 96 private static final String OLD_PINCODE = "oldPinCode"; 97 private static final String NEW_PINCODE = "newPinCode"; 98 99 private static final int MIN_PIN_LENGTH = 4; 100 private static final int MAX_PIN_LENGTH = 8; 101 // Which dialog to show next when popped up 102 private int mDialogState = OFF_MODE; 103 104 private String mPin; 105 private String mOldPin; 106 private String mNewPin; 107 private String mError; 108 // Are we trying to enable or disable ICC lock? 109 private boolean mToState; 110 111 private TabHost mTabHost; 112 private TabWidget mTabWidget; 113 private ListView mListView; 114 115 private Phone mPhone; 116 117 private EditPinPreference mPinDialog; 118 private SwitchPreference mPinToggle; 119 120 private Resources mRes; 121 122 // For async handler to identify request type 123 private static final int MSG_ENABLE_ICC_PIN_COMPLETE = 100; 124 private static final int MSG_CHANGE_ICC_PIN_COMPLETE = 101; 125 private static final int MSG_SIM_STATE_CHANGED = 102; 126 127 // @see android.widget.Toast$TN 128 private static final long LONG_DURATION_TIMEOUT = 7000; 129 130 // For replies from IccCard interface 131 private Handler mHandler = new Handler() { 132 public void handleMessage(Message msg) { 133 AsyncResult ar = (AsyncResult) msg.obj; 134 switch (msg.what) { 135 case MSG_ENABLE_ICC_PIN_COMPLETE: 136 iccLockChanged(ar.exception == null, msg.arg1, ar.exception); 137 break; 138 case MSG_CHANGE_ICC_PIN_COMPLETE: 139 iccPinChanged(ar.exception == null, msg.arg1); 140 break; 141 case MSG_SIM_STATE_CHANGED: 142 updatePreferences(); 143 break; 144 } 145 146 return; 147 } 148 }; 149 150 private final BroadcastReceiver mSimStateReceiver = new BroadcastReceiver() { 151 public void onReceive(Context context, Intent intent) { 152 final String action = intent.getAction(); 153 if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)) { 154 mHandler.sendMessage(mHandler.obtainMessage(MSG_SIM_STATE_CHANGED)); 155 } 156 } 157 }; 158 159 // For top-level settings screen to query isIccLockEnabled()160 static boolean isIccLockEnabled() { 161 return PhoneFactory.getDefaultPhone().getIccCard().getIccLockEnabled(); 162 } 163 getSummary(Context context)164 static String getSummary(Context context) { 165 Resources res = context.getResources(); 166 String summary = isIccLockEnabled() 167 ? res.getString(R.string.sim_lock_on) 168 : res.getString(R.string.sim_lock_off); 169 return summary; 170 } 171 172 @Override onCreate(Bundle savedInstanceState)173 public void onCreate(Bundle savedInstanceState) { 174 super.onCreate(savedInstanceState); 175 176 if (Utils.isMonkeyRunning()) { 177 finish(); 178 return; 179 } 180 181 addPreferencesFromResource(R.xml.sim_lock_settings); 182 183 mPinDialog = (EditPinPreference) findPreference(PIN_DIALOG); 184 mPinToggle = (SwitchPreference) findPreference(PIN_TOGGLE); 185 if (savedInstanceState != null && savedInstanceState.containsKey(DIALOG_STATE)) { 186 mDialogState = savedInstanceState.getInt(DIALOG_STATE); 187 mPin = savedInstanceState.getString(DIALOG_PIN); 188 mError = savedInstanceState.getString(DIALOG_ERROR); 189 mToState = savedInstanceState.getBoolean(ENABLE_TO_STATE); 190 191 // Restore inputted PIN code 192 switch (mDialogState) { 193 case ICC_NEW_MODE: 194 mOldPin = savedInstanceState.getString(OLD_PINCODE); 195 break; 196 197 case ICC_REENTER_MODE: 198 mOldPin = savedInstanceState.getString(OLD_PINCODE); 199 mNewPin = savedInstanceState.getString(NEW_PINCODE); 200 break; 201 202 case ICC_LOCK_MODE: 203 case ICC_OLD_MODE: 204 default: 205 break; 206 } 207 } 208 209 mPinDialog.setOnPinEnteredListener(this); 210 211 // Don't need any changes to be remembered 212 getPreferenceScreen().setPersistent(false); 213 214 mRes = getResources(); 215 } 216 217 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)218 public View onCreateView(LayoutInflater inflater, ViewGroup container, 219 Bundle savedInstanceState) { 220 221 final TelephonyManager tm = 222 (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE); 223 final int numSims = tm.getSimCount(); 224 if (numSims > 1) { 225 View view = inflater.inflate(R.layout.icc_lock_tabs, container, false); 226 final ViewGroup prefs_container = (ViewGroup) view.findViewById(R.id.prefs_container); 227 Utils.prepareCustomPreferencesList(container, view, prefs_container, false); 228 View prefs = super.onCreateView(inflater, prefs_container, savedInstanceState); 229 prefs_container.addView(prefs); 230 231 mTabHost = (TabHost) view.findViewById(android.R.id.tabhost); 232 mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs); 233 mListView = (ListView) view.findViewById(android.R.id.list); 234 235 mTabHost.setup(); 236 mTabHost.setOnTabChangedListener(mTabListener); 237 mTabHost.clearAllTabs(); 238 239 SubscriptionManager sm = SubscriptionManager.from(getContext()); 240 for (int i = 0; i < numSims; ++i) { 241 final SubscriptionInfo subInfo = sm.getActiveSubscriptionInfoForSimSlotIndex(i); 242 mTabHost.addTab(buildTabSpec(String.valueOf(i), 243 String.valueOf(subInfo == null 244 ? getContext().getString(R.string.sim_editor_title, i + 1) 245 : subInfo.getDisplayName()))); 246 } 247 final SubscriptionInfo sir = sm.getActiveSubscriptionInfoForSimSlotIndex(0); 248 249 mPhone = (sir == null) ? null 250 : PhoneFactory.getPhone(SubscriptionManager.getPhoneId(sir.getSubscriptionId())); 251 252 if (savedInstanceState != null && savedInstanceState.containsKey(CURRENT_TAB)) { 253 mTabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB)); 254 } 255 return view; 256 } else { 257 mPhone = PhoneFactory.getDefaultPhone(); 258 return super.onCreateView(inflater, container, savedInstanceState); 259 } 260 } 261 262 @Override onViewCreated(View view, Bundle savedInstanceState)263 public void onViewCreated(View view, Bundle savedInstanceState) { 264 super.onViewCreated(view, savedInstanceState); 265 updatePreferences(); 266 } 267 updatePreferences()268 private void updatePreferences() { 269 if (mPinDialog != null) { 270 mPinDialog.setEnabled(mPhone != null); 271 } 272 if (mPinToggle != null) { 273 mPinToggle.setEnabled(mPhone != null); 274 275 if (mPhone != null) { 276 mPinToggle.setChecked(mPhone.getIccCard().getIccLockEnabled()); 277 } 278 } 279 } 280 281 @Override getMetricsCategory()282 public int getMetricsCategory() { 283 return SettingsEnums.ICC_LOCK; 284 } 285 286 @Override onResume()287 public void onResume() { 288 super.onResume(); 289 290 // ACTION_SIM_STATE_CHANGED is sticky, so we'll receive current state after this call, 291 // which will call updatePreferences(). 292 final IntentFilter filter = new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED); 293 getContext().registerReceiver(mSimStateReceiver, filter); 294 295 if (mDialogState != OFF_MODE) { 296 showPinDialog(); 297 } else { 298 // Prep for standard click on "Change PIN" 299 resetDialogState(); 300 } 301 } 302 303 @Override onPause()304 public void onPause() { 305 super.onPause(); 306 getContext().unregisterReceiver(mSimStateReceiver); 307 } 308 309 @Override getHelpResource()310 public int getHelpResource() { 311 return R.string.help_url_icc_lock; 312 } 313 314 @Override onSaveInstanceState(Bundle out)315 public void onSaveInstanceState(Bundle out) { 316 // Need to store this state for slider open/close 317 // There is one case where the dialog is popped up by the preference 318 // framework. In that case, let the preference framework store the 319 // dialog state. In other cases, where this activity manually launches 320 // the dialog, store the state of the dialog. 321 if (mPinDialog.isDialogOpen()) { 322 out.putInt(DIALOG_STATE, mDialogState); 323 out.putString(DIALOG_PIN, mPinDialog.getEditText().getText().toString()); 324 out.putString(DIALOG_ERROR, mError); 325 out.putBoolean(ENABLE_TO_STATE, mToState); 326 327 // Save inputted PIN code 328 switch (mDialogState) { 329 case ICC_NEW_MODE: 330 out.putString(OLD_PINCODE, mOldPin); 331 break; 332 333 case ICC_REENTER_MODE: 334 out.putString(OLD_PINCODE, mOldPin); 335 out.putString(NEW_PINCODE, mNewPin); 336 break; 337 338 case ICC_LOCK_MODE: 339 case ICC_OLD_MODE: 340 default: 341 break; 342 } 343 } else { 344 super.onSaveInstanceState(out); 345 } 346 347 if (mTabHost != null) { 348 out.putString(CURRENT_TAB, mTabHost.getCurrentTabTag()); 349 } 350 } 351 showPinDialog()352 private void showPinDialog() { 353 if (mDialogState == OFF_MODE) { 354 return; 355 } 356 setDialogValues(); 357 358 mPinDialog.showPinDialog(); 359 360 final EditText editText = mPinDialog.getEditText(); 361 if (!TextUtils.isEmpty(mPin) && editText != null) { 362 editText.setSelection(mPin.length()); 363 } 364 } 365 setDialogValues()366 private void setDialogValues() { 367 mPinDialog.setText(mPin); 368 String message = ""; 369 switch (mDialogState) { 370 case ICC_LOCK_MODE: 371 message = mRes.getString(R.string.sim_enter_pin); 372 mPinDialog.setDialogTitle(mToState 373 ? mRes.getString(R.string.sim_enable_sim_lock) 374 : mRes.getString(R.string.sim_disable_sim_lock)); 375 break; 376 case ICC_OLD_MODE: 377 message = mRes.getString(R.string.sim_enter_old); 378 mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); 379 break; 380 case ICC_NEW_MODE: 381 message = mRes.getString(R.string.sim_enter_new); 382 mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); 383 break; 384 case ICC_REENTER_MODE: 385 message = mRes.getString(R.string.sim_reenter_new); 386 mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); 387 break; 388 } 389 if (mError != null) { 390 message = mError + "\n" + message; 391 mError = null; 392 } 393 mPinDialog.setDialogMessage(message); 394 } 395 396 @Override onPinEntered(EditPinPreference preference, boolean positiveResult)397 public void onPinEntered(EditPinPreference preference, boolean positiveResult) { 398 if (!positiveResult) { 399 resetDialogState(); 400 return; 401 } 402 403 mPin = preference.getText(); 404 if (!reasonablePin(mPin)) { 405 // inject error message and display dialog again 406 mError = mRes.getString(R.string.sim_bad_pin); 407 showPinDialog(); 408 return; 409 } 410 switch (mDialogState) { 411 case ICC_LOCK_MODE: 412 tryChangeIccLockState(); 413 break; 414 case ICC_OLD_MODE: 415 mOldPin = mPin; 416 mDialogState = ICC_NEW_MODE; 417 mError = null; 418 mPin = null; 419 showPinDialog(); 420 break; 421 case ICC_NEW_MODE: 422 mNewPin = mPin; 423 mDialogState = ICC_REENTER_MODE; 424 mPin = null; 425 showPinDialog(); 426 break; 427 case ICC_REENTER_MODE: 428 if (!mPin.equals(mNewPin)) { 429 mError = mRes.getString(R.string.sim_pins_dont_match); 430 mDialogState = ICC_NEW_MODE; 431 mPin = null; 432 showPinDialog(); 433 } else { 434 mError = null; 435 tryChangePin(); 436 } 437 break; 438 } 439 } 440 441 @Override onPreferenceTreeClick(Preference preference)442 public boolean onPreferenceTreeClick(Preference preference) { 443 if (preference == mPinToggle) { 444 // Get the new, preferred state 445 mToState = mPinToggle.isChecked(); 446 // Flip it back and pop up pin dialog 447 mPinToggle.setChecked(!mToState); 448 mDialogState = ICC_LOCK_MODE; 449 showPinDialog(); 450 } else if (preference == mPinDialog) { 451 mDialogState = ICC_OLD_MODE; 452 return false; 453 } 454 return true; 455 } 456 tryChangeIccLockState()457 private void tryChangeIccLockState() { 458 // Try to change icc lock. If it succeeds, toggle the lock state and 459 // reset dialog state. Else inject error message and show dialog again. 460 Message callback = Message.obtain(mHandler, MSG_ENABLE_ICC_PIN_COMPLETE); 461 mPhone.getIccCard().setIccLockEnabled(mToState, mPin, callback); 462 // Disable the setting till the response is received. 463 mPinToggle.setEnabled(false); 464 } 465 iccLockChanged(boolean success, int attemptsRemaining, Throwable exception)466 private void iccLockChanged(boolean success, int attemptsRemaining, Throwable exception) { 467 if (success) { 468 mPinToggle.setChecked(mToState); 469 } else { 470 if (exception instanceof CommandException) { 471 CommandException.Error err = ((CommandException)(exception)).getCommandError(); 472 if (err == CommandException.Error.PASSWORD_INCORRECT) { 473 createCustomTextToast(getPinPasswordErrorMessage(attemptsRemaining)); 474 } else { 475 if (mToState) { 476 Toast.makeText(getContext(), mRes.getString 477 (R.string.sim_pin_enable_failed), Toast.LENGTH_LONG).show(); 478 } else { 479 Toast.makeText(getContext(), mRes.getString 480 (R.string.sim_pin_disable_failed), Toast.LENGTH_LONG).show(); 481 } 482 } 483 } 484 } 485 mPinToggle.setEnabled(true); 486 resetDialogState(); 487 } 488 createCustomTextToast(CharSequence errorMessage)489 private void createCustomTextToast(CharSequence errorMessage) { 490 // Cannot overlay Toast on PUK unlock screen. 491 // The window type of Toast is set by NotificationManagerService. 492 // It can't be overwritten by LayoutParams.type. 493 // Ovarlay a custom window with LayoutParams (TYPE_STATUS_BAR_PANEL) on PUK unlock screen. 494 View v = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)) 495 .inflate(com.android.internal.R.layout.transient_notification, null); 496 TextView tv = (TextView) v.findViewById(com.android.internal.R.id.message); 497 tv.setText(errorMessage); 498 499 final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); 500 final Configuration config = v.getContext().getResources().getConfiguration(); 501 final int gravity = Gravity.getAbsoluteGravity( 502 getContext().getResources().getInteger( 503 com.android.internal.R.integer.config_toastDefaultGravity), 504 config.getLayoutDirection()); 505 params.gravity = gravity; 506 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { 507 params.horizontalWeight = 1.0f; 508 } 509 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { 510 params.verticalWeight = 1.0f; 511 } 512 params.y = getContext().getResources().getDimensionPixelSize( 513 com.android.internal.R.dimen.toast_y_offset); 514 515 params.height = WindowManager.LayoutParams.WRAP_CONTENT; 516 params.width = WindowManager.LayoutParams.WRAP_CONTENT; 517 params.format = PixelFormat.TRANSLUCENT; 518 params.windowAnimations = com.android.internal.R.style.Animation_Toast; 519 params.type = WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; 520 params.setFitInsetsTypes(params.getFitInsetsTypes() & ~Type.statusBars()); 521 params.setTitle(errorMessage); 522 params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 523 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 524 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 525 526 WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 527 wm.addView(v, params); 528 529 mHandler.postDelayed(new Runnable() { 530 @Override 531 public void run() { 532 wm.removeViewImmediate(v); 533 } 534 }, LONG_DURATION_TIMEOUT); 535 } 536 iccPinChanged(boolean success, int attemptsRemaining)537 private void iccPinChanged(boolean success, int attemptsRemaining) { 538 if (!success) { 539 createCustomTextToast(getPinPasswordErrorMessage(attemptsRemaining)); 540 } else { 541 Toast.makeText(getContext(), mRes.getString(R.string.sim_change_succeeded), 542 Toast.LENGTH_SHORT) 543 .show(); 544 545 } 546 resetDialogState(); 547 } 548 tryChangePin()549 private void tryChangePin() { 550 Message callback = Message.obtain(mHandler, MSG_CHANGE_ICC_PIN_COMPLETE); 551 mPhone.getIccCard().changeIccLockPassword(mOldPin, 552 mNewPin, callback); 553 } 554 getPinPasswordErrorMessage(int attemptsRemaining)555 private String getPinPasswordErrorMessage(int attemptsRemaining) { 556 String displayMessage; 557 558 if (attemptsRemaining == 0) { 559 displayMessage = mRes.getString(R.string.wrong_pin_code_pukked); 560 } else if (attemptsRemaining > 0) { 561 displayMessage = mRes 562 .getQuantityString(R.plurals.wrong_pin_code, attemptsRemaining, 563 attemptsRemaining); 564 } else { 565 displayMessage = mRes.getString(R.string.pin_failed); 566 } 567 if (DBG) Log.d(TAG, "getPinPasswordErrorMessage:" 568 + " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage); 569 return displayMessage; 570 } 571 reasonablePin(String pin)572 private boolean reasonablePin(String pin) { 573 if (pin == null || pin.length() < MIN_PIN_LENGTH || pin.length() > MAX_PIN_LENGTH) { 574 return false; 575 } else { 576 return true; 577 } 578 } 579 resetDialogState()580 private void resetDialogState() { 581 mError = null; 582 mDialogState = ICC_OLD_MODE; // Default for when Change PIN is clicked 583 mPin = ""; 584 setDialogValues(); 585 mDialogState = OFF_MODE; 586 } 587 588 private OnTabChangeListener mTabListener = new OnTabChangeListener() { 589 @Override 590 public void onTabChanged(String tabId) { 591 final int slotId = Integer.parseInt(tabId); 592 final SubscriptionInfo sir = SubscriptionManager.from(getActivity().getBaseContext()) 593 .getActiveSubscriptionInfoForSimSlotIndex(slotId); 594 595 mPhone = (sir == null) ? null 596 : PhoneFactory.getPhone(SubscriptionManager.getPhoneId(sir.getSubscriptionId())); 597 598 // The User has changed tab; update the body. 599 updatePreferences(); 600 } 601 }; 602 603 private TabContentFactory mEmptyTabContent = new TabContentFactory() { 604 @Override 605 public View createTabContent(String tag) { 606 return new View(mTabHost.getContext()); 607 } 608 }; 609 buildTabSpec(String tag, String title)610 private TabSpec buildTabSpec(String tag, String title) { 611 return mTabHost.newTabSpec(tag).setIndicator(title).setContent( 612 mEmptyTabContent); 613 } 614 } 615