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