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