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 android.app.ActivityManager;
     20 import android.app.ActionBar;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.Intent;
     27 import android.content.SharedPreferences;
     28 import android.content.pm.ApplicationInfo;
     29 import android.content.pm.PackageInfo;
     30 import android.content.pm.PackageManager;
     31 import android.content.pm.PackageManager;
     32 import android.content.pm.ResolveInfo;
     33 import android.content.pm.ServiceInfo;
     34 import android.content.res.Resources;
     35 import android.graphics.drawable.Drawable;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.os.SystemProperties;
     39 import android.preference.EditTextPreference;
     40 import android.preference.Preference;
     41 import android.preference.PreferenceActivity;
     42 import android.telephony.PhoneNumberUtils;
     43 import android.telephony.TelephonyManager;
     44 import android.text.TextUtils;
     45 import android.util.Log;
     46 import android.view.LayoutInflater;
     47 import android.view.Menu;
     48 import android.view.MenuItem;
     49 import android.view.View;
     50 import android.view.ViewGroup;
     51 import android.widget.AdapterView;
     52 import android.widget.ArrayAdapter;
     53 import android.widget.BaseAdapter;
     54 import android.widget.CheckBox;
     55 import android.widget.CompoundButton;
     56 import android.widget.ImageView;
     57 import android.widget.ListView;
     58 import android.widget.TextView;
     59 import android.widget.Toast;
     60 
     61 import com.android.internal.telephony.Call;
     62 import com.android.internal.telephony.Connection;
     63 import com.android.internal.telephony.PhoneConstants;
     64 import com.google.android.collect.Lists;
     65 
     66 import java.util.ArrayList;
     67 import java.util.Arrays;
     68 import java.util.List;
     69 
     70 /**
     71  * Helper class to manage the "Respond via Message" feature for incoming calls.
     72  *
     73  * @see InCallScreen.internalRespondViaSms()
     74  */
     75 public class RespondViaSmsManager {
     76     private static final String TAG = "RespondViaSmsManager";
     77     private static final boolean DBG =
     78             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
     79     // Do not check in with VDBG = true, since that may write PII to the system log.
     80     private static final boolean VDBG = false;
     81 
     82     private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
     83             "android.permission.SEND_RESPOND_VIA_MESSAGE";
     84 
     85     private int mIconSize = -1;
     86 
     87     /**
     88      * Reference to the InCallScreen activity that owns us.  This may be
     89      * null if we haven't been initialized yet *or* after the InCallScreen
     90      * activity has been destroyed.
     91      */
     92     private InCallScreen mInCallScreen;
     93 
     94     /**
     95      * The popup showing the list of canned responses.
     96      *
     97      * This is an AlertDialog containing a ListView showing the possible
     98      * choices.  This may be null if the InCallScreen hasn't ever called
     99      * showRespondViaSmsPopup() yet, or if the popup was visible once but
    100      * then got dismissed.
    101      */
    102     private Dialog mCannedResponsePopup;
    103 
    104     /**
    105      * The popup dialog allowing the user to chose which app handles respond-via-sms.
    106      *
    107      * An AlertDialog showing the Resolve-App UI resource from the framework wchih we then fill in
    108      * with the appropriate data set. Can be null when not visible.
    109      */
    110     private Dialog mPackageSelectionPopup;
    111 
    112     /** The array of "canned responses"; see loadCannedResponses(). */
    113     private String[] mCannedResponses;
    114 
    115     /** SharedPreferences file name for our persistent settings. */
    116     private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
    117 
    118     // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
    119     // Since (for now at least) the number of messages is fixed at 4, and since
    120     // SharedPreferences can't deal with arrays anyway, just store the messages
    121     // as 4 separate strings.
    122     private static final int NUM_CANNED_RESPONSES = 4;
    123     private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
    124     private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
    125     private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
    126     private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
    127     private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
    128     private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
    129 
    130     /**
    131      * RespondViaSmsManager constructor.
    132      */
    133     public RespondViaSmsManager() {
    134     }
    135 
    136     public void setInCallScreenInstance(InCallScreen inCallScreen) {
    137         mInCallScreen = inCallScreen;
    138 
    139         if (mInCallScreen != null) {
    140             // Prefetch shared preferences to make the first canned response lookup faster
    141             // (and to prevent StrictMode violation)
    142             mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    143         }
    144     }
    145 
    146     /**
    147      * Brings up the "Respond via SMS" popup for an incoming call.
    148      *
    149      * @param ringingCall the current incoming call
    150      */
    151     public void showRespondViaSmsPopup(Call ringingCall) {
    152         if (DBG) log("showRespondViaSmsPopup()...");
    153 
    154         // Very quick succession of clicks can cause this to run twice.
    155         // Stop here to avoid creating more than one popup.
    156         if (isShowingPopup()) {
    157             if (DBG) log("Skip showing popup when one is already shown.");
    158             return;
    159         }
    160 
    161         ListView lv = new ListView(mInCallScreen);
    162 
    163         // Refresh the array of "canned responses".
    164         mCannedResponses = loadCannedResponses();
    165 
    166         // Build the list: start with the canned responses, but manually add
    167         // the write-your-own option as the last choice.
    168         int numPopupItems = mCannedResponses.length + 1;
    169         String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems);
    170         popupItems[numPopupItems - 1] = mInCallScreen.getResources()
    171                 .getString(R.string.respond_via_sms_custom_message);
    172 
    173         ArrayAdapter<String> adapter =
    174                 new ArrayAdapter<String>(mInCallScreen,
    175                                          android.R.layout.simple_list_item_1,
    176                                          android.R.id.text1,
    177                                          popupItems);
    178         lv.setAdapter(adapter);
    179 
    180         // Create a RespondViaSmsItemClickListener instance to handle item
    181         // clicks from the popup.
    182         // (Note we create a fresh instance for each incoming call, and
    183         // stash away the call's phone number, since we can't necessarily
    184         // assume this call will still be ringing when the user finally
    185         // chooses a response.)
    186 
    187         Connection c = ringingCall.getLatestConnection();
    188         if (VDBG) log("- connection: " + c);
    189 
    190         if (c == null) {
    191             // Uh oh -- the "ringingCall" doesn't have any connections any more.
    192             // (In other words, it's no longer ringing.)  This is rare, but can
    193             // happen if the caller hangs up right at the exact moment the user
    194             // selects the "Respond via SMS" option.
    195             // There's nothing to do here (since the incoming call is gone),
    196             // so just bail out.
    197             Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out...");
    198             return;
    199         }
    200 
    201         // TODO: at this point we probably should re-check c.getAddress()
    202         // and c.getNumberPresentation() for validity.  (i.e. recheck the
    203         // same cases in InCallTouchUi.showIncomingCallWidget() where we
    204         // should have disallowed the "respond via SMS" feature in the
    205         // first place.)
    206 
    207         String phoneNumber = c.getAddress();
    208         if (VDBG) log("- phoneNumber: " + phoneNumber);
    209         lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber));
    210 
    211         AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
    212                 .setCancelable(true)
    213                 .setOnCancelListener(new RespondViaSmsCancelListener())
    214                 .setView(lv);
    215         mCannedResponsePopup = builder.create();
    216         mCannedResponsePopup.show();
    217     }
    218 
    219     /**
    220      * Dismiss currently visible popups.
    221      *
    222      * This is safe to call even if the popup is already dismissed, and
    223      * even if you never called showRespondViaSmsPopup() in the first
    224      * place.
    225      */
    226     public void dismissPopup() {
    227         if (mCannedResponsePopup != null) {
    228             mCannedResponsePopup.dismiss();  // safe even if already dismissed
    229             mCannedResponsePopup = null;
    230         }
    231         if (mPackageSelectionPopup != null) {
    232             mPackageSelectionPopup.dismiss();
    233             mPackageSelectionPopup = null;
    234         }
    235     }
    236 
    237     public boolean isShowingPopup() {
    238         return (mCannedResponsePopup != null && mCannedResponsePopup.isShowing())
    239                 || (mPackageSelectionPopup != null && mPackageSelectionPopup.isShowing());
    240     }
    241 
    242     /**
    243      * OnItemClickListener for the "Respond via SMS" popup.
    244      */
    245     public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
    246         // Phone number to send the SMS to.
    247         private String mPhoneNumber;
    248 
    249         public RespondViaSmsItemClickListener(String phoneNumber) {
    250             mPhoneNumber = phoneNumber;
    251         }
    252 
    253         /**
    254          * Handles the user selecting an item from the popup.
    255          */
    256         @Override
    257         public void onItemClick(AdapterView<?> parent,  // The ListView
    258                                 View view,  // The TextView that was clicked
    259                                 int position,
    260                                 long id) {
    261             if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
    262             String message = (String) parent.getItemAtPosition(position);
    263             if (VDBG) log("- message: '" + message + "'");
    264 
    265             // The "Custom" choice is a special case.
    266             // (For now, it's guaranteed to be the last item.)
    267             if (position == (parent.getCount() - 1)) {
    268                 // Take the user to the standard SMS compose UI.
    269                 launchSmsCompose(mPhoneNumber);
    270                 onPostMessageSent();
    271             } else {
    272                 sendTextToDefaultActivity(mPhoneNumber, message);
    273             }
    274         }
    275     }
    276 
    277 
    278     /**
    279      * OnCancelListener for the "Respond via SMS" popup.
    280      */
    281     public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener {
    282         public RespondViaSmsCancelListener() {
    283         }
    284 
    285         /**
    286          * Handles the user canceling the popup, either by touching
    287          * outside the popup or by pressing Back.
    288          */
    289         @Override
    290         public void onCancel(DialogInterface dialog) {
    291             if (DBG) log("RespondViaSmsCancelListener.onCancel()...");
    292 
    293             dismissPopup();
    294 
    295             final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
    296             if (state == PhoneConstants.State.IDLE) {
    297                 // This means the incoming call is already hung up when the user chooses not to
    298                 // use "Respond via SMS" feature. Let's just exit the whole in-call screen.
    299                 PhoneGlobals.getInstance().dismissCallScreen();
    300             } else {
    301 
    302                 // If the user cancels the popup, this presumably means that
    303                 // they didn't actually mean to bring up the "Respond via SMS"
    304                 // UI in the first place (and instead want to go back to the
    305                 // state where they can either answer or reject the call.)
    306                 // So restart the ringer and bring back the regular incoming
    307                 // call UI.
    308 
    309                 // This will have no effect if the incoming call isn't still ringing.
    310                 PhoneGlobals.getInstance().notifier.restartRinger();
    311 
    312                 // We hid the GlowPadView widget way back in
    313                 // InCallTouchUi.onTrigger(), when the user first selected
    314                 // the "SMS" trigger.
    315                 //
    316                 // To bring it back, just force the entire InCallScreen to
    317                 // update itself based on the current telephony state.
    318                 // (Assuming the incoming call is still ringing, this will
    319                 // cause the incoming call widget to reappear.)
    320                 mInCallScreen.requestUpdateScreen();
    321             }
    322         }
    323     }
    324 
    325     private void sendTextToDefaultActivity(String phoneNumber, String message) {
    326         if (DBG) log("sendTextToDefaultActivity()...");
    327         final PackageManager packageManager = mInCallScreen.getPackageManager();
    328 
    329         // Check to see if the default component to receive this intent is already saved
    330         // and check to see if it still has the corrent permissions.
    331         final SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
    332                 Context.MODE_PRIVATE);
    333         final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
    334         if (flattenedName != null) {
    335             if (DBG) log("Default package was found." + flattenedName);
    336 
    337             final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
    338             ServiceInfo serviceInfo = null;
    339             try {
    340                 serviceInfo = packageManager.getServiceInfo(componentName, 0);
    341             } catch (PackageManager.NameNotFoundException e) {
    342                 Log.w(TAG, "Default service does not have permission.");
    343             }
    344 
    345             if (serviceInfo != null &&
    346                     PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
    347                 sendTextAndExit(phoneNumber, message, componentName, false);
    348                 return;
    349             } else {
    350                 SharedPreferences.Editor editor = prefs.edit();
    351                 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
    352                 editor.apply();
    353             }
    354         }
    355 
    356         final ArrayList<ComponentName> componentsWithPermission =
    357             getPackagesWithInstantTextPermission();
    358 
    359         final int size = componentsWithPermission.size();
    360         if (size == 0) {
    361             Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
    362             onPostMessageSent();
    363         } else if (size == 1) {
    364             sendTextAndExit(phoneNumber, message, componentsWithPermission.get(0), false);
    365         } else {
    366             showPackageSelectionDialog(phoneNumber, message, componentsWithPermission);
    367         }
    368     }
    369 
    370     /**
    371      * Queries the System to determine what packages contain services that can handle the instant
    372      * text response Action AND have permissions to do so.
    373      */
    374     private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
    375         PackageManager packageManager = mInCallScreen.getPackageManager();
    376 
    377         ArrayList<ComponentName> componentsWithPermission = Lists.newArrayList();
    378 
    379         // Get list of all services set up to handle the Instant Text intent.
    380         final List<ResolveInfo> infos = packageManager.queryIntentServices(
    381                 getInstantTextIntent("", null, null), 0);
    382 
    383         // Collect all the valid services
    384         for (ResolveInfo resolveInfo : infos) {
    385             final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
    386             if (serviceInfo == null) {
    387                 Log.w(TAG, "Ignore package without proper service.");
    388                 continue;
    389             }
    390 
    391             // A Service is valid only if it requires the permission
    392             // PERMISSION_SEND_RESPOND_VIA_MESSAGE
    393             if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
    394                 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
    395                     serviceInfo.name));
    396             }
    397         }
    398 
    399         return componentsWithPermission;
    400     }
    401 
    402     private void showPackageSelectionDialog(String phoneNumber, String message,
    403             List<ComponentName> components) {
    404         if (DBG) log("showPackageSelectionDialog()...");
    405 
    406         dismissPopup();
    407 
    408         BaseAdapter adapter = new PackageSelectionAdapter(mInCallScreen, components);
    409 
    410         PackageClickListener clickListener =
    411                 new PackageClickListener(phoneNumber, message, components);
    412 
    413         final CharSequence title = mInCallScreen.getResources().getText(
    414                 com.android.internal.R.string.whichApplication);
    415         LayoutInflater inflater =
    416                 (LayoutInflater) mInCallScreen.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    417 
    418         final View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
    419         final CheckBox alwaysUse = (CheckBox) view.findViewById(
    420                 com.android.internal.R.id.alwaysUse);
    421         alwaysUse.setText(com.android.internal.R.string.alwaysUse);
    422         alwaysUse.setOnCheckedChangeListener(clickListener);
    423 
    424         AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
    425                 .setTitle(title)
    426                 .setCancelable(true)
    427                 .setOnCancelListener(new RespondViaSmsCancelListener())
    428                 .setAdapter(adapter, clickListener)
    429                 .setView(view);
    430         mPackageSelectionPopup = builder.create();
    431         mPackageSelectionPopup.show();
    432     }
    433 
    434     private class PackageSelectionAdapter extends BaseAdapter {
    435         private final LayoutInflater mInflater;
    436         private final List<ComponentName> mComponents;
    437 
    438         public PackageSelectionAdapter(Context context, List<ComponentName> components) {
    439             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    440             mComponents = components;
    441         }
    442 
    443         @Override
    444         public int getCount() {
    445             return mComponents.size();
    446         }
    447 
    448         @Override
    449         public Object getItem(int position) {
    450             return mComponents.get(position);
    451         }
    452 
    453         @Override
    454         public long getItemId(int position) {
    455             return position;
    456         }
    457 
    458         @Override
    459         public View getView(int position, View convertView, ViewGroup parent) {
    460             if (convertView == null) {
    461                 convertView = mInflater.inflate(
    462                         com.android.internal.R.layout.resolve_list_item, parent, false);
    463             }
    464 
    465             final ComponentName component = mComponents.get(position);
    466             final String packageName = component.getPackageName();
    467             final PackageManager packageManager = mInCallScreen.getPackageManager();
    468 
    469             // Set the application label
    470             final TextView text = (TextView) convertView.findViewById(
    471                     com.android.internal.R.id.text1);
    472             final TextView text2 = (TextView) convertView.findViewById(
    473                     com.android.internal.R.id.text2);
    474 
    475             // Reset any previous values
    476             text.setText("");
    477             text2.setVisibility(View.GONE);
    478             try {
    479                 final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
    480                 final CharSequence label = packageManager.getApplicationLabel(appInfo);
    481                 if (label != null) {
    482                     text.setText(label);
    483                 }
    484             } catch (PackageManager.NameNotFoundException e) {
    485                 Log.w(TAG, "Failed to load app label because package was not found.");
    486             }
    487 
    488             // Set the application icon
    489             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
    490             Drawable drawable = null;
    491             try {
    492                 drawable = mInCallScreen.getPackageManager().getApplicationIcon(packageName);
    493             } catch (PackageManager.NameNotFoundException e) {
    494                 Log.w(TAG, "Failed to load icon because it wasn't found.");
    495             }
    496             if (drawable == null) {
    497                 drawable = mInCallScreen.getPackageManager().getDefaultActivityIcon();
    498             }
    499             icon.setImageDrawable(drawable);
    500             ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) icon.getLayoutParams();
    501             lp.width = lp.height = getIconSize();
    502 
    503             return convertView;
    504         }
    505 
    506     }
    507 
    508     private class PackageClickListener implements DialogInterface.OnClickListener,
    509             CompoundButton.OnCheckedChangeListener {
    510         /** Phone number to send the SMS to. */
    511         final private String mPhoneNumber;
    512         final private String mMessage;
    513         final private List<ComponentName> mComponents;
    514         private boolean mMakeDefault = false;
    515 
    516         public PackageClickListener(String phoneNumber, String message,
    517                 List<ComponentName> components) {
    518             mPhoneNumber = phoneNumber;
    519             mMessage = message;
    520             mComponents = components;
    521         }
    522 
    523         @Override
    524         public void onClick(DialogInterface dialog, int which) {
    525             ComponentName component = mComponents.get(which);
    526             sendTextAndExit(mPhoneNumber, mMessage, component, mMakeDefault);
    527         }
    528 
    529         @Override
    530         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    531             Log.i(TAG, "mMakeDefault : " + isChecked);
    532             mMakeDefault = isChecked;
    533         }
    534     }
    535 
    536     private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
    537             boolean setDefaultComponent) {
    538         // Send the selected message immediately with no user interaction.
    539         sendText(phoneNumber, message, component);
    540 
    541         if (setDefaultComponent) {
    542             final SharedPreferences prefs = mInCallScreen.getSharedPreferences(
    543                     SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    544             prefs.edit()
    545                     .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
    546                     .apply();
    547         }
    548 
    549         // ...and show a brief confirmation to the user (since
    550         // otherwise it's hard to be sure that anything actually
    551         // happened.)
    552         final Resources res = mInCallScreen.getResources();
    553         final String formatString = res.getString(R.string.respond_via_sms_confirmation_format);
    554         final String confirmationMsg = String.format(formatString, phoneNumber);
    555         Toast.makeText(mInCallScreen,
    556                        confirmationMsg,
    557                        Toast.LENGTH_LONG).show();
    558 
    559         // TODO: If the device is locked, this toast won't actually ever
    560         // be visible!  (That's because we're about to dismiss the call
    561         // screen, which means that the device will return to the
    562         // keyguard.  But toasts aren't visible on top of the keyguard.)
    563         // Possible fixes:
    564         // (1) Is it possible to allow a specific Toast to be visible
    565         //     on top of the keyguard?
    566         // (2) Artifically delay the dismissCallScreen() call by 3
    567         //     seconds to allow the toast to be seen?
    568         // (3) Don't use a toast at all; instead use a transient state
    569         //     of the InCallScreen (perhaps via the InCallUiState
    570         //     progressIndication feature), and have that state be
    571         //     visible for 3 seconds before calling dismissCallScreen().
    572 
    573         onPostMessageSent();
    574     }
    575 
    576     /**
    577      * Sends a text message without any interaction from the user.
    578      */
    579     private void sendText(String phoneNumber, String message, ComponentName component) {
    580         if (VDBG) log("sendText: number "
    581                       + phoneNumber + ", message '" + message + "'");
    582 
    583         mInCallScreen.startService(getInstantTextIntent(phoneNumber, message, component));
    584     }
    585 
    586     private void onPostMessageSent() {
    587         // At this point the user is done dealing with the incoming call, so
    588         // there's no reason to keep it around.  (It's also confusing for
    589         // the "incoming call" icon in the status bar to still be visible.)
    590         // So reject the call now.
    591         mInCallScreen.hangupRingingCall();
    592 
    593         dismissPopup();
    594 
    595         final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
    596         if (state == PhoneConstants.State.IDLE) {
    597             // There's no other phone call to interact. Exit the entire in-call screen.
    598             PhoneGlobals.getInstance().dismissCallScreen();
    599         } else {
    600             // The user is still in the middle of other phone calls, so we should keep the
    601             // in-call screen.
    602             mInCallScreen.requestUpdateScreen();
    603         }
    604     }
    605 
    606     /**
    607      * Brings up the standard SMS compose UI.
    608      */
    609     private void launchSmsCompose(String phoneNumber) {
    610         if (VDBG) log("launchSmsCompose: number " + phoneNumber);
    611 
    612         Intent intent = getInstantTextIntent(phoneNumber, null, null);
    613 
    614         if (VDBG) log("- Launching SMS compose UI: " + intent);
    615         mInCallScreen.startService(intent);
    616     }
    617 
    618     /**
    619      * @param phoneNumber Must not be null.
    620      * @param message Can be null. If message is null, the returned Intent will be configured to
    621      * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
    622      * to be sent with no interaction from the user.
    623      * @param component The component that should handle this intent.
    624      * @return Service Intent for the instant response.
    625      */
    626     private static Intent getInstantTextIntent(String phoneNumber, String message,
    627             ComponentName component) {
    628         final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
    629         Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
    630         if (message != null) {
    631             intent.putExtra(Intent.EXTRA_TEXT, message);
    632         } else {
    633             intent.putExtra("exit_on_sent", true);
    634             intent.putExtra("showUI", true);
    635         }
    636         if (component != null) {
    637             intent.setComponent(component);
    638         }
    639         return intent;
    640     }
    641 
    642     /**
    643      * Settings activity under "Call settings" to let you manage the
    644      * canned responses; see respond_via_sms_settings.xml
    645      */
    646     public static class Settings extends PreferenceActivity
    647             implements Preference.OnPreferenceChangeListener {
    648         @Override
    649         protected void onCreate(Bundle icicle) {
    650             super.onCreate(icicle);
    651             if (DBG) log("Settings: onCreate()...");
    652 
    653             getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
    654 
    655             // This preference screen is ultra-simple; it's just 4 plain
    656             // <EditTextPreference>s, one for each of the 4 "canned responses".
    657             //
    658             // The only nontrivial thing we do here is copy the text value of
    659             // each of those EditTextPreferences and use it as the preference's
    660             // "title" as well, so that the user will immediately see all 4
    661             // strings when they arrive here.
    662             //
    663             // Also, listen for change events (since we'll need to update the
    664             // title any time the user edits one of the strings.)
    665 
    666             addPreferencesFromResource(R.xml.respond_via_sms_settings);
    667 
    668             EditTextPreference pref;
    669             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
    670             pref.setTitle(pref.getText());
    671             pref.setOnPreferenceChangeListener(this);
    672 
    673             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
    674             pref.setTitle(pref.getText());
    675             pref.setOnPreferenceChangeListener(this);
    676 
    677             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
    678             pref.setTitle(pref.getText());
    679             pref.setOnPreferenceChangeListener(this);
    680 
    681             pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
    682             pref.setTitle(pref.getText());
    683             pref.setOnPreferenceChangeListener(this);
    684 
    685             ActionBar actionBar = getActionBar();
    686             if (actionBar != null) {
    687                 // android.R.id.home will be triggered in onOptionsItemSelected()
    688                 actionBar.setDisplayHomeAsUpEnabled(true);
    689             }
    690         }
    691 
    692         // Preference.OnPreferenceChangeListener implementation
    693         @Override
    694         public boolean onPreferenceChange(Preference preference, Object newValue) {
    695             if (DBG) log("onPreferenceChange: key = " + preference.getKey());
    696             if (VDBG) log("  preference = '" + preference + "'");
    697             if (VDBG) log("  newValue = '" + newValue + "'");
    698 
    699             EditTextPreference pref = (EditTextPreference) preference;
    700 
    701             // Copy the new text over to the title, just like in onCreate().
    702             // (Watch out: onPreferenceChange() is called *before* the
    703             // Preference itself gets updated, so we need to use newValue here
    704             // rather than pref.getText().)
    705             pref.setTitle((String) newValue);
    706 
    707             return true;  // means it's OK to update the state of the Preference with the new value
    708         }
    709 
    710         @Override
    711         public boolean onOptionsItemSelected(MenuItem item) {
    712             final int itemId = item.getItemId();
    713             switch (itemId) {
    714                 case android.R.id.home:
    715                     // See ActionBar#setDisplayHomeAsUpEnabled()
    716                     CallFeaturesSetting.goUpToTopLevelSetting(this);
    717                     return true;
    718                 case R.id.respond_via_message_reset:
    719                     // Reset the preferences settings
    720                     SharedPreferences prefs = getSharedPreferences(
    721                             SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    722                     SharedPreferences.Editor editor = prefs.edit();
    723                     editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
    724                     editor.apply();
    725 
    726                     return true;
    727                 default:
    728             }
    729             return super.onOptionsItemSelected(item);
    730         }
    731 
    732         @Override
    733         public boolean onCreateOptionsMenu(Menu menu) {
    734             getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
    735             return super.onCreateOptionsMenu(menu);
    736         }
    737     }
    738 
    739     /**
    740      * Read the (customizable) canned responses from SharedPreferences,
    741      * or from defaults if the user has never actually brought up
    742      * the Settings UI.
    743      *
    744      * This method does disk I/O (reading the SharedPreferences file)
    745      * so don't call it from the main thread.
    746      *
    747      * @see RespondViaSmsManager.Settings
    748      */
    749     private String[] loadCannedResponses() {
    750         if (DBG) log("loadCannedResponses()...");
    751 
    752         SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
    753                 Context.MODE_PRIVATE);
    754         final Resources res = mInCallScreen.getResources();
    755 
    756         String[] responses = new String[NUM_CANNED_RESPONSES];
    757 
    758         // Note the default values here must agree with the corresponding
    759         // android:defaultValue attributes in respond_via_sms_settings.xml.
    760 
    761         responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
    762                                        res.getString(R.string.respond_via_sms_canned_response_1));
    763         responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
    764                                        res.getString(R.string.respond_via_sms_canned_response_2));
    765         responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
    766                                        res.getString(R.string.respond_via_sms_canned_response_3));
    767         responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
    768                                        res.getString(R.string.respond_via_sms_canned_response_4));
    769         return responses;
    770     }
    771 
    772     /**
    773      * @return true if the "Respond via SMS" feature should be enabled
    774      * for the specified incoming call.
    775      *
    776      * The general rule is that we *do* allow "Respond via SMS" except for
    777      * the few (relatively rare) cases where we know for sure it won't
    778      * work, namely:
    779      *   - a bogus or blank incoming number
    780      *   - a call from a SIP address
    781      *   - a "call presentation" that doesn't allow the number to be revealed
    782      *
    783      * In all other cases, we allow the user to respond via SMS.
    784      *
    785      * Note that this behavior isn't perfect; for example we have no way
    786      * to detect whether the incoming call is from a landline (with most
    787      * networks at least), so we still enable this feature even though
    788      * SMSes to that number will silently fail.
    789      */
    790     public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
    791         if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")...");
    792 
    793         // First some basic sanity checks:
    794         if (ringingCall == null) {
    795             Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
    796             return false;
    797         }
    798         if (!ringingCall.isRinging()) {
    799             // The call is in some state other than INCOMING or WAITING!
    800             // (This should almost never happen, but it *could*
    801             // conceivably happen if the ringing call got disconnected by
    802             // the network just *after* we got it from the CallManager.)
    803             Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
    804                   + ringingCall.getState());
    805             return false;
    806         }
    807         Connection conn = ringingCall.getLatestConnection();
    808         if (conn == null) {
    809             // The call doesn't have any connections!  (Again, this can
    810             // happen if the ringing call disconnects at the exact right
    811             // moment, but should almost never happen in practice.)
    812             Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
    813             return false;
    814         }
    815 
    816         // Check the incoming number:
    817         final String number = conn.getAddress();
    818         if (DBG) log("- number: '" + number + "'");
    819         if (TextUtils.isEmpty(number)) {
    820             Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
    821             return false;
    822         }
    823         if (PhoneNumberUtils.isUriNumber(number)) {
    824             // The incoming number is actually a URI (i.e. a SIP address),
    825             // not a regular PSTN phone number, and we can't send SMSes to
    826             // SIP addresses.
    827             // (TODO: That might still be possible eventually, though.  Is
    828             // there some SIP-specific equivalent to sending a text message?)
    829             Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
    830             return false;
    831         }
    832 
    833         // Finally, check the "call presentation":
    834         int presentation = conn.getNumberPresentation();
    835         if (DBG) log("- presentation: " + presentation);
    836         if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
    837             // PRESENTATION_RESTRICTED means "caller-id blocked".
    838             // The user isn't allowed to see the number in the first
    839             // place, so obviously we can't let you send an SMS to it.
    840             Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
    841             return false;
    842         }
    843 
    844         // Allow the feature only when there's a destination for it.
    845         if (context.getPackageManager().resolveService(getInstantTextIntent(number, null, null) , 0)
    846                 == null) {
    847             return false;
    848         }
    849 
    850         // TODO: with some carriers (in certain countries) you *can* actually
    851         // tell whether a given number is a mobile phone or not.  So in that
    852         // case we could potentially return false here if the incoming call is
    853         // from a land line.
    854 
    855         // If none of the above special cases apply, it's OK to enable the
    856         // "Respond via SMS" feature.
    857         return true;
    858     }
    859 
    860     private int getIconSize() {
    861       if (mIconSize < 0) {
    862           final ActivityManager am =
    863               (ActivityManager) mInCallScreen.getSystemService(Context.ACTIVITY_SERVICE);
    864           mIconSize = am.getLauncherLargeIconSize();
    865       }
    866 
    867       return mIconSize;
    868     }
    869 
    870 
    871     private static void log(String msg) {
    872         Log.d(TAG, msg);
    873     }
    874 }
    875