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