1/*
2 * Copyright (C) 2006 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.dialer;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.DialogFragment;
22import android.app.KeyguardManager;
23import android.app.ProgressDialog;
24import android.content.ActivityNotFoundException;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.database.Cursor;
30import android.net.Uri;
31import android.os.Looper;
32import android.provider.Settings;
33import android.telecom.PhoneAccount;
34import android.telecom.PhoneAccountHandle;
35import android.telephony.PhoneNumberUtils;
36import android.telephony.TelephonyManager;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.WindowManager;
40import android.widget.EditText;
41import android.widget.Toast;
42
43import com.android.common.io.MoreCloseables;
44import com.android.contacts.common.compat.CompatUtils;
45import com.android.contacts.common.compat.TelephonyManagerCompat;
46import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
47import com.android.contacts.common.util.ContactDisplayUtils;
48import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
49import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
50import com.android.dialer.calllog.PhoneAccountUtils;
51import com.android.dialer.util.TelecomUtil;
52
53import java.util.ArrayList;
54import java.util.List;
55
56/**
57 * Helper class to listen for some magic character sequences
58 * that are handled specially by the dialer.
59 *
60 * Note the Phone app also handles these sequences too (in a couple of
61 * relatively obscure places in the UI), so there's a separate version of
62 * this class under apps/Phone.
63 *
64 * TODO: there's lots of duplicated code between this class and the
65 * corresponding class under apps/Phone.  Let's figure out a way to
66 * unify these two classes (in the framework? in a common shared library?)
67 */
68public class SpecialCharSequenceMgr {
69    private static final String TAG = "SpecialCharSequenceMgr";
70
71    private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment";
72
73    private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
74    private static final String MMI_IMEI_DISPLAY = "*#06#";
75    private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#";
76
77    /**
78     * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to
79     * prevent possible crash.
80     *
81     * QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone,
82     * which will cause the app crash. This variable enables the class to prevent the crash
83     * on {@link #cleanup()}.
84     *
85     * TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation.
86     * One complication is that we have SpecialCharSequenceMgr in Phone package too, which has
87     * *slightly* different implementation. Note that Phone package doesn't have this problem,
88     * so the class on Phone side doesn't have this functionality.
89     * Fundamental fix would be to have one shared implementation and resolve this corner case more
90     * gracefully.
91     */
92    private static QueryHandler sPreviousAdnQueryHandler;
93
94    public static class HandleAdnEntryAccountSelectedCallback extends SelectPhoneAccountListener{
95        final private Context mContext;
96        final private QueryHandler mQueryHandler;
97        final private SimContactQueryCookie mCookie;
98
99        public HandleAdnEntryAccountSelectedCallback(Context context,
100                QueryHandler queryHandler, SimContactQueryCookie cookie) {
101            mContext = context;
102            mQueryHandler = queryHandler;
103            mCookie = cookie;
104        }
105
106        @Override
107        public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
108                boolean setDefault) {
109            Uri uri = TelecomUtil.getAdnUriForPhoneAccount(mContext, selectedAccountHandle);
110            handleAdnQuery(mQueryHandler, mCookie, uri);
111            // TODO: Show error dialog if result isn't valid.
112        }
113
114    }
115
116    public static class HandleMmiAccountSelectedCallback extends SelectPhoneAccountListener{
117        final private Context mContext;
118        final private String mInput;
119        public HandleMmiAccountSelectedCallback(Context context, String input) {
120            mContext = context.getApplicationContext();
121            mInput = input;
122        }
123
124        @Override
125        public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
126                boolean setDefault) {
127            TelecomUtil.handleMmi(mContext, mInput, selectedAccountHandle);
128        }
129    }
130
131    /** This class is never instantiated. */
132    private SpecialCharSequenceMgr() {
133    }
134
135    public static boolean handleChars(Context context, String input, EditText textField) {
136        //get rid of the separators so that the string gets parsed correctly
137        String dialString = PhoneNumberUtils.stripSeparators(input);
138
139        if (handleDeviceIdDisplay(context, dialString)
140                || handleRegulatoryInfoDisplay(context, dialString)
141                || handlePinEntry(context, dialString)
142                || handleAdnEntry(context, dialString, textField)
143                || handleSecretCode(context, dialString)) {
144            return true;
145        }
146
147        return false;
148    }
149
150    /**
151     * Cleanup everything around this class. Must be run inside the main thread.
152     *
153     * This should be called when the screen becomes background.
154     */
155    public static void cleanup() {
156        if (Looper.myLooper() != Looper.getMainLooper()) {
157            Log.wtf(TAG, "cleanup() is called outside the main thread");
158            return;
159        }
160
161        if (sPreviousAdnQueryHandler != null) {
162            sPreviousAdnQueryHandler.cancel();
163            sPreviousAdnQueryHandler = null;
164        }
165    }
166
167    /**
168     * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
169     * If a secret code is encountered an Intent is started with the android_secret_code://<code>
170     * URI.
171     *
172     * @param context the context to use
173     * @param input the text to check for a secret code in
174     * @return true if a secret code was encountered
175     */
176    static boolean handleSecretCode(Context context, String input) {
177        // Secret codes are in the form *#*#<code>#*#*
178        int len = input.length();
179        if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
180            final Intent intent = new Intent(SECRET_CODE_ACTION,
181                    Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
182            context.sendBroadcast(intent);
183            return true;
184        }
185
186        return false;
187    }
188
189    /**
190     * Handle ADN requests by filling in the SIM contact number into the requested
191     * EditText.
192     *
193     * This code works alongside the Asynchronous query handler {@link QueryHandler}
194     * and query cancel handler implemented in {@link SimContactQueryCookie}.
195     */
196    static boolean handleAdnEntry(Context context, String input, EditText textField) {
197        /* ADN entries are of the form "N(N)(N)#" */
198        TelephonyManager telephonyManager =
199                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
200        if (telephonyManager == null
201                || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) {
202            return false;
203        }
204
205        // if the phone is keyguard-restricted, then just ignore this
206        // input.  We want to make sure that sim card contacts are NOT
207        // exposed unless the phone is unlocked, and this code can be
208        // accessed from the emergency dialer.
209        KeyguardManager keyguardManager =
210                (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
211        if (keyguardManager.inKeyguardRestrictedInputMode()) {
212            return false;
213        }
214
215        int len = input.length();
216        if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
217            try {
218                // get the ordinal number of the sim contact
219                final int index = Integer.parseInt(input.substring(0, len-1));
220
221                // The original code that navigated to a SIM Contacts list view did not
222                // highlight the requested contact correctly, a requirement for PTCRB
223                // certification.  This behaviour is consistent with the UI paradigm
224                // for touch-enabled lists, so it does not make sense to try to work
225                // around it.  Instead we fill in the the requested phone number into
226                // the dialer text field.
227
228                // create the async query handler
229                final QueryHandler handler = new QueryHandler(context.getContentResolver());
230
231                // create the cookie object
232                final SimContactQueryCookie sc = new SimContactQueryCookie(index - 1, handler,
233                        ADN_QUERY_TOKEN);
234
235                // setup the cookie fields
236                sc.contactNum = index - 1;
237                sc.setTextField(textField);
238
239                // create the progress dialog
240                sc.progressDialog = new ProgressDialog(context);
241                sc.progressDialog.setTitle(R.string.simContacts_title);
242                sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading));
243                sc.progressDialog.setIndeterminate(true);
244                sc.progressDialog.setCancelable(true);
245                sc.progressDialog.setOnCancelListener(sc);
246                sc.progressDialog.getWindow().addFlags(
247                        WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
248
249                List<PhoneAccountHandle> subscriptionAccountHandles =
250                        PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
251                Context applicationContext = context.getApplicationContext();
252                boolean hasUserSelectedDefault = subscriptionAccountHandles.contains(
253                        TelecomUtil.getDefaultOutgoingPhoneAccount(applicationContext,
254                                PhoneAccount.SCHEME_TEL));
255
256                if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
257                    Uri uri = TelecomUtil.getAdnUriForPhoneAccount(applicationContext, null);
258                    handleAdnQuery(handler, sc, uri);
259                } else {
260                    SelectPhoneAccountListener callback = new HandleAdnEntryAccountSelectedCallback(
261                            applicationContext, handler, sc);
262
263                    DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance(
264                            subscriptionAccountHandles, callback);
265                    dialogFragment.show(((Activity) context).getFragmentManager(),
266                            TAG_SELECT_ACCT_FRAGMENT);
267                }
268
269                return true;
270            } catch (NumberFormatException ex) {
271                // Ignore
272            }
273        }
274        return false;
275    }
276
277    private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie,
278            Uri uri) {
279        if (handler == null || cookie == null || uri == null) {
280            Log.w(TAG, "queryAdn parameters incorrect");
281            return;
282        }
283
284        // display the progress dialog
285        cookie.progressDialog.show();
286
287        // run the query.
288        handler.startQuery(ADN_QUERY_TOKEN, cookie, uri, new String[]{ADN_PHONE_NUMBER_COLUMN_NAME},
289                null, null, null);
290
291        if (sPreviousAdnQueryHandler != null) {
292            // It is harmless to call cancel() even after the handler's gone.
293            sPreviousAdnQueryHandler.cancel();
294        }
295        sPreviousAdnQueryHandler = handler;
296    }
297
298    static boolean handlePinEntry(final Context context, final String input) {
299        if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) {
300            List<PhoneAccountHandle> subscriptionAccountHandles =
301                    PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
302            boolean hasUserSelectedDefault = subscriptionAccountHandles.contains(
303                    TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL));
304
305            if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
306                // Don't bring up the dialog for single-SIM or if the default outgoing account is
307                // a subscription account.
308                return TelecomUtil.handleMmi(context, input, null);
309            } else {
310                SelectPhoneAccountListener listener =
311                        new HandleMmiAccountSelectedCallback(context, input);
312
313                DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance(
314                        subscriptionAccountHandles, listener);
315                dialogFragment.show(((Activity) context).getFragmentManager(),
316                        TAG_SELECT_ACCT_FRAGMENT);
317            }
318            return true;
319        }
320        return false;
321    }
322
323    // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a
324    // hard-coded string.
325    static boolean handleDeviceIdDisplay(Context context, String input) {
326        TelephonyManager telephonyManager =
327                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
328
329        if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) {
330            int labelResId = (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) ?
331                    R.string.imei : R.string.meid;
332
333            List<String> deviceIds = new ArrayList<String>();
334            if (TelephonyManagerCompat.getPhoneCount(telephonyManager) > 1 &&
335                    CompatUtils.isMethodAvailable(TelephonyManagerCompat.TELEPHONY_MANAGER_CLASS,
336                            "getDeviceId", Integer.TYPE)) {
337                for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) {
338                    String deviceId = telephonyManager.getDeviceId(slot);
339                    if (!TextUtils.isEmpty(deviceId)) {
340                        deviceIds.add(deviceId);
341                    }
342                }
343            } else {
344                deviceIds.add(telephonyManager.getDeviceId());
345            }
346
347            AlertDialog alert = new AlertDialog.Builder(context)
348                    .setTitle(labelResId)
349                    .setItems(deviceIds.toArray(new String[deviceIds.size()]), null)
350                    .setPositiveButton(android.R.string.ok, null)
351                    .setCancelable(false)
352                    .show();
353            return true;
354        }
355        return false;
356    }
357
358    private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
359        if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
360            Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app");
361            Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
362            try {
363                context.startActivity(showRegInfoIntent);
364            } catch (ActivityNotFoundException e) {
365                Log.e(TAG, "startActivity() failed: " + e);
366            }
367            return true;
368        }
369        return false;
370    }
371
372    /*******
373     * This code is used to handle SIM Contact queries
374     *******/
375    private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number";
376    private static final String ADN_NAME_COLUMN_NAME = "name";
377    private static final int ADN_QUERY_TOKEN = -1;
378
379    /**
380     * Cookie object that contains everything we need to communicate to the
381     * handler's onQuery Complete, as well as what we need in order to cancel
382     * the query (if requested).
383     *
384     * Note, access to the textField field is going to be synchronized, because
385     * the user can request a cancel at any time through the UI.
386     */
387    private static class SimContactQueryCookie implements DialogInterface.OnCancelListener{
388        public ProgressDialog progressDialog;
389        public int contactNum;
390
391        // Used to identify the query request.
392        private int mToken;
393        private QueryHandler mHandler;
394
395        // The text field we're going to update
396        private EditText textField;
397
398        public SimContactQueryCookie(int number, QueryHandler handler, int token) {
399            contactNum = number;
400            mHandler = handler;
401            mToken = token;
402        }
403
404        /**
405         * Synchronized getter for the EditText.
406         */
407        public synchronized EditText getTextField() {
408            return textField;
409        }
410
411        /**
412         * Synchronized setter for the EditText.
413         */
414        public synchronized void setTextField(EditText text) {
415            textField = text;
416        }
417
418        /**
419         * Cancel the ADN query by stopping the operation and signaling
420         * the cookie that a cancel request is made.
421         */
422        public synchronized void onCancel(DialogInterface dialog) {
423            // close the progress dialog
424            if (progressDialog != null) {
425                progressDialog.dismiss();
426            }
427
428            // setting the textfield to null ensures that the UI does NOT get
429            // updated.
430            textField = null;
431
432            // Cancel the operation if possible.
433            mHandler.cancelOperation(mToken);
434        }
435    }
436
437    /**
438     * Asynchronous query handler that services requests to look up ADNs
439     *
440     * Queries originate from {@link #handleAdnEntry}.
441     */
442    private static class QueryHandler extends NoNullCursorAsyncQueryHandler {
443
444        private boolean mCanceled;
445
446        public QueryHandler(ContentResolver cr) {
447            super(cr);
448        }
449
450        /**
451         * Override basic onQueryComplete to fill in the textfield when
452         * we're handed the ADN cursor.
453         */
454        @Override
455        protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) {
456            try {
457                sPreviousAdnQueryHandler = null;
458                if (mCanceled) {
459                    return;
460                }
461
462                SimContactQueryCookie sc = (SimContactQueryCookie) cookie;
463
464                // close the progress dialog.
465                sc.progressDialog.dismiss();
466
467                // get the EditText to update or see if the request was cancelled.
468                EditText text = sc.getTextField();
469
470                // if the TextView is valid, and the cursor is valid and positionable on the
471                // Nth number, then we update the text field and display a toast indicating the
472                // caller name.
473                if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) {
474                    String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME));
475                    String number =
476                            c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME));
477
478                    // fill the text in.
479                    text.getText().replace(0, 0, number);
480
481                    // display the name as a toast
482                    Context context = sc.progressDialog.getContext();
483                    CharSequence msg = ContactDisplayUtils.getTtsSpannedPhoneNumber(
484                            context.getResources(), R.string.menu_callNumber, name);
485                    Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
486                }
487            } finally {
488                MoreCloseables.closeQuietly(c);
489            }
490        }
491
492        public void cancel() {
493            mCanceled = true;
494            // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is
495            // already started.
496            cancelOperation(ADN_QUERY_TOKEN);
497        }
498    }
499}
500