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