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