Home | History | Annotate | Download | only in phone
      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      */
     99     public RespondViaSmsManager() {
    100     }
    101 
    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      */
    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      */
    185     public void dismissPopup() {
    186         if (mPopup != null) {
    187             mPopup.dismiss();  // safe even if already dismissed
    188             mPopup = null;
    189         }
    190     }
    191 
    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 
    203         public RespondViaSmsItemClickListener(String phoneNumber) {
    204             mPhoneNumber = phoneNumber;
    205         }
    206 
    207         /**
    208          * Handles the user selecting an item from the popup.
    209          */
    210         @Override
    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 {
    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
    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      */
    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      */
    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      */
    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
    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
    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
    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      */
    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      */
    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 
    563     private static void log(String msg) {
    564         Log.d(TAG, msg);
    565     }
    566 }
    567