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 
17 package com.android.dialer;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.DialogFragment;
22 import android.app.KeyguardManager;
23 import android.app.ProgressDialog;
24 import android.content.ActivityNotFoundException;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.database.Cursor;
30 import android.net.Uri;
31 import android.os.Looper;
32 import android.provider.Settings;
33 import android.telecom.PhoneAccount;
34 import android.telecom.PhoneAccountHandle;
35 import android.telephony.PhoneNumberUtils;
36 import android.telephony.TelephonyManager;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.WindowManager;
40 import android.widget.EditText;
41 import android.widget.Toast;
42 
43 import com.android.common.io.MoreCloseables;
44 import com.android.contacts.common.compat.CompatUtils;
45 import com.android.contacts.common.compat.TelephonyManagerCompat;
46 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
47 import com.android.contacts.common.util.ContactDisplayUtils;
48 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
49 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
50 import com.android.dialer.calllog.PhoneAccountUtils;
51 import com.android.dialer.util.TelecomUtil;
52 
53 import java.util.ArrayList;
54 import 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  */
68 public 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 
HandleAdnEntryAccountSelectedCallback(Context context, QueryHandler queryHandler, SimContactQueryCookie cookie)99         public HandleAdnEntryAccountSelectedCallback(Context context,
100                 QueryHandler queryHandler, SimContactQueryCookie cookie) {
101             mContext = context;
102             mQueryHandler = queryHandler;
103             mCookie = cookie;
104         }
105 
106         @Override
onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle, boolean setDefault)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;
HandleMmiAccountSelectedCallback(Context context, String input)119         public HandleMmiAccountSelectedCallback(Context context, String input) {
120             mContext = context.getApplicationContext();
121             mInput = input;
122         }
123 
124         @Override
onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle, boolean setDefault)125         public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
126                 boolean setDefault) {
127             TelecomUtil.handleMmi(mContext, mInput, selectedAccountHandle);
128         }
129     }
130 
131     /** This class is never instantiated. */
SpecialCharSequenceMgr()132     private SpecialCharSequenceMgr() {
133     }
134 
handleChars(Context context, String input, EditText textField)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      */
cleanup()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      */
handleSecretCode(Context context, String input)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      */
handleAdnEntry(Context context, String input, EditText textField)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 
handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri)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 
handlePinEntry(final Context context, final String input)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.
handleDeviceIdDisplay(Context context, String input)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 
handleRegulatoryInfoDisplay(Context context, String input)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 
SimContactQueryCookie(int number, QueryHandler handler, int token)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          */
getTextField()407         public synchronized EditText getTextField() {
408             return textField;
409         }
410 
411         /**
412          * Synchronized setter for the EditText.
413          */
setTextField(EditText text)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          */
onCancel(DialogInterface dialog)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 
QueryHandler(ContentResolver cr)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
onNotNullableQueryComplete(int token, Object cookie, Cursor c)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 
cancel()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