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