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