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