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