• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.phone;
18 
19 import com.android.internal.telephony.Call;
20 import com.android.internal.telephony.Connection;
21 import com.android.internal.telephony.Phone;
22 
23 import android.app.ActionBar;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.res.Resources;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.SystemProperties;
34 import android.preference.EditTextPreference;
35 import android.preference.Preference;
36 import android.preference.PreferenceActivity;
37 import android.telephony.PhoneNumberUtils;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.widget.AdapterView;
43 import android.widget.ArrayAdapter;
44 import android.widget.ListView;
45 import android.widget.Toast;
46 
47 import java.util.Arrays;
48 
49 /**
50  * Helper class to manage the "Respond via SMS" feature for incoming calls.
51  * @see InCallScreen.internalRespondViaSms()
52  */
53 public class RespondViaSmsManager {
54     private static final String TAG = "RespondViaSmsManager";
55     private static final boolean DBG =
56             (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
57     // Do not check in with VDBG = true, since that may write PII to the system log.
58     private static final boolean VDBG = false;
59 
60     /**
61      * Reference to the InCallScreen activity that owns us.  This may be
62      * null if we haven't been initialized yet *or* after the InCallScreen
63      * activity has been destroyed.
64      */
65     private InCallScreen mInCallScreen;
66 
67     /**
68      * The popup showing the list of canned responses.
69      *
70      * This is an AlertDialog containing a ListView showing the possible
71      * choices.  This may be null if the InCallScreen hasn't ever called
72      * showRespondViaSmsPopup() yet, or if the popup was visible once but
73      * then got dismissed.
74      */
75     private Dialog mPopup;
76 
77     /** The array of "canned responses"; see loadCannedResponses(). */
78     private String[] mCannedResponses;
79 
80     /** SharedPreferences file name for our persistent settings. */
81     private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
82 
83     // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
84     // Since (for now at least) the number of messages is fixed at 4, and since
85     // SharedPreferences can't deal with arrays anyway, just store the messages
86     // as 4 separate strings.
87     private static final int NUM_CANNED_RESPONSES = 4;
88     private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
89     private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
90     private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
91     private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
92 
93     private static final String ACTION_SENDTO_NO_CONFIRMATION =
94             "com.android.mms.intent.action.SENDTO_NO_CONFIRMATION";
95 
96     /**
97      * RespondViaSmsManager constructor.
98      */
RespondViaSmsManager()99     public RespondViaSmsManager() {
100     }
101 
setInCallScreenInstance(InCallScreen inCallScreen)102     public void setInCallScreenInstance(InCallScreen inCallScreen) {
103         mInCallScreen = inCallScreen;
104 
105         if (mInCallScreen != null) {
106             // Prefetch shared preferences to make the first canned response lookup faster
107             // (and to prevent StrictMode violation)
108             mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
109         }
110     }
111 
112     /**
113      * Brings up the "Respond via SMS" popup for an incoming call.
114      *
115      * @param ringingCall the current incoming call
116      */
showRespondViaSmsPopup(Call ringingCall)117     public void showRespondViaSmsPopup(Call ringingCall) {
118         if (DBG) log("showRespondViaSmsPopup()...");
119 
120         ListView lv = new ListView(mInCallScreen);
121 
122         // Refresh the array of "canned responses".
123         mCannedResponses = loadCannedResponses();
124 
125         // Build the list: start with the canned responses, but manually add
126         // "Custom message..." as the last choice.
127         int numPopupItems = mCannedResponses.length + 1;
128         String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems);
129         popupItems[numPopupItems - 1] = mInCallScreen.getResources()
130                 .getString(R.string.respond_via_sms_custom_message);
131 
132         ArrayAdapter<String> adapter =
133                 new ArrayAdapter<String>(mInCallScreen,
134                                          android.R.layout.simple_list_item_1,
135                                          android.R.id.text1,
136                                          popupItems);
137         lv.setAdapter(adapter);
138 
139         // Create a RespondViaSmsItemClickListener instance to handle item
140         // clicks from the popup.
141         // (Note we create a fresh instance for each incoming call, and
142         // stash away the call's phone number, since we can't necessarily
143         // assume this call will still be ringing when the user finally
144         // chooses a response.)
145 
146         Connection c = ringingCall.getLatestConnection();
147         if (VDBG) log("- connection: " + c);
148 
149         if (c == null) {
150             // Uh oh -- the "ringingCall" doesn't have any connections any more.
151             // (In other words, it's no longer ringing.)  This is rare, but can
152             // happen if the caller hangs up right at the exact moment the user
153             // selects the "Respond via SMS" option.
154             // There's nothing to do here (since the incoming call is gone),
155             // so just bail out.
156             Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out...");
157             return;
158         }
159 
160         // TODO: at this point we probably should re-check c.getAddress()
161         // and c.getNumberPresentation() for validity.  (i.e. recheck the
162         // same cases in InCallTouchUi.showIncomingCallWidget() where we
163         // should have disallowed the "respond via SMS" feature in the
164         // first place.)
165 
166         String phoneNumber = c.getAddress();
167         if (VDBG) log("- phoneNumber: " + phoneNumber);
168         lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber));
169 
170         AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
171                 .setCancelable(true)
172                 .setOnCancelListener(new RespondViaSmsCancelListener())
173                 .setView(lv);
174         mPopup = builder.create();
175         mPopup.show();
176     }
177 
178     /**
179      * Dismiss the "Respond via SMS" popup if it's visible.
180      *
181      * This is safe to call even if the popup is already dismissed, and
182      * even if you never called showRespondViaSmsPopup() in the first
183      * place.
184      */
dismissPopup()185     public void dismissPopup() {
186         if (mPopup != null) {
187             mPopup.dismiss();  // safe even if already dismissed
188             mPopup = null;
189         }
190     }
191 
isShowingPopup()192     public boolean isShowingPopup() {
193         return mPopup != null && mPopup.isShowing();
194     }
195 
196     /**
197      * OnItemClickListener for the "Respond via SMS" popup.
198      */
199     public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
200         // Phone number to send the SMS to.
201         private String mPhoneNumber;
202 
RespondViaSmsItemClickListener(String phoneNumber)203         public RespondViaSmsItemClickListener(String phoneNumber) {
204             mPhoneNumber = phoneNumber;
205         }
206 
207         /**
208          * Handles the user selecting an item from the popup.
209          */
210         @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)211         public void onItemClick(AdapterView<?> parent,  // The ListView
212                                 View view,  // The TextView that was clicked
213                                 int position,
214                                 long id) {
215             if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
216             String message = (String) parent.getItemAtPosition(position);
217             if (VDBG) log("- message: '" + message + "'");
218 
219             // The "Custom" choice is a special case.
220             // (For now, it's guaranteed to be the last item.)
221             if (position == (parent.getCount() - 1)) {
222                 // Take the user to the standard SMS compose UI.
223                 launchSmsCompose(mPhoneNumber);
224             } else {
225                 // Send the selected message immediately with no user interaction.
226                 sendText(mPhoneNumber, message);
227 
228                 // ...and show a brief confirmation to the user (since
229                 // otherwise it's hard to be sure that anything actually
230                 // happened.)
231                 final Resources res = mInCallScreen.getResources();
232                 String formatString = res.getString(R.string.respond_via_sms_confirmation_format);
233                 String confirmationMsg = String.format(formatString, mPhoneNumber);
234                 Toast.makeText(mInCallScreen,
235                                confirmationMsg,
236                                Toast.LENGTH_LONG).show();
237 
238                 // TODO: If the device is locked, this toast won't actually ever
239                 // be visible!  (That's because we're about to dismiss the call
240                 // screen, which means that the device will return to the
241                 // keyguard.  But toasts aren't visible on top of the keyguard.)
242                 // Possible fixes:
243                 // (1) Is it possible to allow a specific Toast to be visible
244                 //     on top of the keyguard?
245                 // (2) Artifically delay the dismissCallScreen() call by 3
246                 //     seconds to allow the toast to be seen?
247                 // (3) Don't use a toast at all; instead use a transient state
248                 //     of the InCallScreen (perhaps via the InCallUiState
249                 //     progressIndication feature), and have that state be
250                 //     visible for 3 seconds before calling dismissCallScreen().
251             }
252 
253             // At this point the user is done dealing with the incoming call, so
254             // there's no reason to keep it around.  (It's also confusing for
255             // the "incoming call" icon in the status bar to still be visible.)
256             // So reject the call now.
257             mInCallScreen.hangupRingingCall();
258 
259             dismissPopup();
260 
261             final Phone.State state = PhoneApp.getInstance().mCM.getState();
262             if (state == Phone.State.IDLE) {
263                 // There's no other phone call to interact. Exit the entire in-call screen.
264                 PhoneApp.getInstance().dismissCallScreen();
265             } else {
266                 // The user is still in the middle of other phone calls, so we should keep the
267                 // in-call screen.
268                 mInCallScreen.requestUpdateScreen();
269             }
270         }
271     }
272 
273     /**
274      * OnCancelListener for the "Respond via SMS" popup.
275      */
276     public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener {
RespondViaSmsCancelListener()277         public RespondViaSmsCancelListener() {
278         }
279 
280         /**
281          * Handles the user canceling the popup, either by touching
282          * outside the popup or by pressing Back.
283          */
284         @Override
onCancel(DialogInterface dialog)285         public void onCancel(DialogInterface dialog) {
286             if (DBG) log("RespondViaSmsCancelListener.onCancel()...");
287 
288             dismissPopup();
289 
290             final Phone.State state = PhoneApp.getInstance().mCM.getState();
291             if (state == Phone.State.IDLE) {
292                 // This means the incoming call is already hung up when the user chooses not to
293                 // use "Respond via SMS" feature. Let's just exit the whole in-call screen.
294                 PhoneApp.getInstance().dismissCallScreen();
295             } else {
296 
297                 // If the user cancels the popup, this presumably means that
298                 // they didn't actually mean to bring up the "Respond via SMS"
299                 // UI in the first place (and instead want to go back to the
300                 // state where they can either answer or reject the call.)
301                 // So restart the ringer and bring back the regular incoming
302                 // call UI.
303 
304                 // This will have no effect if the incoming call isn't still ringing.
305                 PhoneApp.getInstance().notifier.restartRinger();
306 
307                 // We hid the GlowPadView widget way back in
308                 // InCallTouchUi.onTrigger(), when the user first selected
309                 // the "SMS" trigger.
310                 //
311                 // To bring it back, just force the entire InCallScreen to
312                 // update itself based on the current telephony state.
313                 // (Assuming the incoming call is still ringing, this will
314                 // cause the incoming call widget to reappear.)
315                 mInCallScreen.requestUpdateScreen();
316             }
317         }
318     }
319 
320     /**
321      * Sends a text message without any interaction from the user.
322      */
sendText(String phoneNumber, String message)323     private void sendText(String phoneNumber, String message) {
324         if (VDBG) log("sendText: number "
325                       + phoneNumber + ", message '" + message + "'");
326 
327         mInCallScreen.startService(getInstantTextIntent(phoneNumber, message));
328     }
329 
330     /**
331      * Brings up the standard SMS compose UI.
332      */
launchSmsCompose(String phoneNumber)333     private void launchSmsCompose(String phoneNumber) {
334         if (VDBG) log("launchSmsCompose: number " + phoneNumber);
335 
336         Intent intent = getInstantTextIntent(phoneNumber, null);
337 
338         if (VDBG) log("- Launching SMS compose UI: " + intent);
339         mInCallScreen.startService(intent);
340     }
341 
342     /**
343      * @param phoneNumber Must not be null.
344      * @param message Can be null. If message is null, the returned Intent will be configured to
345      * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
346      * to be sent with no interaction from the user.
347      * @return Service Intent for the instant response.
348      */
getInstantTextIntent(String phoneNumber, String message)349     private static Intent getInstantTextIntent(String phoneNumber, String message) {
350         Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
351         Intent intent = new Intent(ACTION_SENDTO_NO_CONFIRMATION, uri);
352         if (message != null) {
353             intent.putExtra(Intent.EXTRA_TEXT, message);
354         } else {
355             intent.putExtra("exit_on_sent", true);
356             intent.putExtra("showUI", true);
357         }
358         return intent;
359     }
360 
361     /**
362      * Settings activity under "Call settings" to let you manage the
363      * canned responses; see respond_via_sms_settings.xml
364      */
365     public static class Settings extends PreferenceActivity
366             implements Preference.OnPreferenceChangeListener {
367         @Override
onCreate(Bundle icicle)368         protected void onCreate(Bundle icicle) {
369             super.onCreate(icicle);
370             if (DBG) log("Settings: onCreate()...");
371 
372             getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
373 
374             // This preference screen is ultra-simple; it's just 4 plain
375             // <EditTextPreference>s, one for each of the 4 "canned responses".
376             //
377             // The only nontrivial thing we do here is copy the text value of
378             // each of those EditTextPreferences and use it as the preference's
379             // "title" as well, so that the user will immediately see all 4
380             // strings when they arrive here.
381             //
382             // Also, listen for change events (since we'll need to update the
383             // title any time the user edits one of the strings.)
384 
385             addPreferencesFromResource(R.xml.respond_via_sms_settings);
386 
387             EditTextPreference pref;
388             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
389             pref.setTitle(pref.getText());
390             pref.setOnPreferenceChangeListener(this);
391 
392             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
393             pref.setTitle(pref.getText());
394             pref.setOnPreferenceChangeListener(this);
395 
396             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
397             pref.setTitle(pref.getText());
398             pref.setOnPreferenceChangeListener(this);
399 
400             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
401             pref.setTitle(pref.getText());
402             pref.setOnPreferenceChangeListener(this);
403 
404             ActionBar actionBar = getActionBar();
405             if (actionBar != null) {
406                 // android.R.id.home will be triggered in onOptionsItemSelected()
407                 actionBar.setDisplayHomeAsUpEnabled(true);
408             }
409         }
410 
411         // Preference.OnPreferenceChangeListener implementation
412         @Override
onPreferenceChange(Preference preference, Object newValue)413         public boolean onPreferenceChange(Preference preference, Object newValue) {
414             if (DBG) log("onPreferenceChange: key = " + preference.getKey());
415             if (VDBG) log("  preference = '" + preference + "'");
416             if (VDBG) log("  newValue = '" + newValue + "'");
417 
418             EditTextPreference pref = (EditTextPreference) preference;
419 
420             // Copy the new text over to the title, just like in onCreate().
421             // (Watch out: onPreferenceChange() is called *before* the
422             // Preference itself gets updated, so we need to use newValue here
423             // rather than pref.getText().)
424             pref.setTitle((String) newValue);
425 
426             return true;  // means it's OK to update the state of the Preference with the new value
427         }
428 
429         @Override
onOptionsItemSelected(MenuItem item)430         public boolean onOptionsItemSelected(MenuItem item) {
431             final int itemId = item.getItemId();
432             if (itemId == android.R.id.home) {  // See ActionBar#setDisplayHomeAsUpEnabled()
433                 CallFeaturesSetting.goUpToTopLevelSetting(this);
434                 return true;
435             }
436             return super.onOptionsItemSelected(item);
437         }
438     }
439 
440     /**
441      * Read the (customizable) canned responses from SharedPreferences,
442      * or from defaults if the user has never actually brought up
443      * the Settings UI.
444      *
445      * This method does disk I/O (reading the SharedPreferences file)
446      * so don't call it from the main thread.
447      *
448      * @see RespondViaSmsManager.Settings
449      */
loadCannedResponses()450     private String[] loadCannedResponses() {
451         if (DBG) log("loadCannedResponses()...");
452 
453         SharedPreferences prefs =
454                 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
455                                                    Context.MODE_PRIVATE);
456         final Resources res = mInCallScreen.getResources();
457 
458         String[] responses = new String[NUM_CANNED_RESPONSES];
459 
460         // Note the default values here must agree with the corresponding
461         // android:defaultValue attributes in respond_via_sms_settings.xml.
462 
463         responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
464                                        res.getString(R.string.respond_via_sms_canned_response_1));
465         responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
466                                        res.getString(R.string.respond_via_sms_canned_response_2));
467         responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
468                                        res.getString(R.string.respond_via_sms_canned_response_3));
469         responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
470                                        res.getString(R.string.respond_via_sms_canned_response_4));
471         return responses;
472     }
473 
474     /**
475      * @return true if the "Respond via SMS" feature should be enabled
476      * for the specified incoming call.
477      *
478      * The general rule is that we *do* allow "Respond via SMS" except for
479      * the few (relatively rare) cases where we know for sure it won't
480      * work, namely:
481      *   - a bogus or blank incoming number
482      *   - a call from a SIP address
483      *   - a "call presentation" that doesn't allow the number to be revealed
484      *
485      * In all other cases, we allow the user to respond via SMS.
486      *
487      * Note that this behavior isn't perfect; for example we have no way
488      * to detect whether the incoming call is from a landline (with most
489      * networks at least), so we still enable this feature even though
490      * SMSes to that number will silently fail.
491      */
allowRespondViaSmsForCall(Context context, Call ringingCall)492     public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
493         if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")...");
494 
495         // First some basic sanity checks:
496         if (ringingCall == null) {
497             Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
498             return false;
499         }
500         if (!ringingCall.isRinging()) {
501             // The call is in some state other than INCOMING or WAITING!
502             // (This should almost never happen, but it *could*
503             // conceivably happen if the ringing call got disconnected by
504             // the network just *after* we got it from the CallManager.)
505             Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
506                   + ringingCall.getState());
507             return false;
508         }
509         Connection conn = ringingCall.getLatestConnection();
510         if (conn == null) {
511             // The call doesn't have any connections!  (Again, this can
512             // happen if the ringing call disconnects at the exact right
513             // moment, but should almost never happen in practice.)
514             Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
515             return false;
516         }
517 
518         // Check the incoming number:
519         final String number = conn.getAddress();
520         if (DBG) log("- number: '" + number + "'");
521         if (TextUtils.isEmpty(number)) {
522             Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
523             return false;
524         }
525         if (PhoneNumberUtils.isUriNumber(number)) {
526             // The incoming number is actually a URI (i.e. a SIP address),
527             // not a regular PSTN phone number, and we can't send SMSes to
528             // SIP addresses.
529             // (TODO: That might still be possible eventually, though.  Is
530             // there some SIP-specific equivalent to sending a text message?)
531             Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
532             return false;
533         }
534 
535         // Finally, check the "call presentation":
536         int presentation = conn.getNumberPresentation();
537         if (DBG) log("- presentation: " + presentation);
538         if (presentation == Connection.PRESENTATION_RESTRICTED) {
539             // PRESENTATION_RESTRICTED means "caller-id blocked".
540             // The user isn't allowed to see the number in the first
541             // place, so obviously we can't let you send an SMS to it.
542             Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
543             return false;
544         }
545 
546         // Allow the feature only when there's a destination for it.
547         if (context.getPackageManager().resolveService(getInstantTextIntent(number, null) , 0)
548                 == null) {
549             return false;
550         }
551 
552         // TODO: with some carriers (in certain countries) you *can* actually
553         // tell whether a given number is a mobile phone or not.  So in that
554         // case we could potentially return false here if the incoming call is
555         // from a land line.
556 
557         // If none of the above special cases apply, it's OK to enable the
558         // "Respond via SMS" feature.
559         return true;
560     }
561 
562 
log(String msg)563     private static void log(String msg) {
564         Log.d(TAG, msg);
565     }
566 }
567