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.content.ComponentName;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.SharedPreferences;
     23 import android.content.res.Resources;
     24 import android.net.Uri;
     25 import android.os.Handler;
     26 import android.os.Looper;
     27 import android.os.Message;
     28 import android.telephony.PhoneNumberUtils;
     29 import android.telephony.TelephonyManager;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.widget.Toast;
     33 
     34 import com.android.internal.telephony.Call;
     35 import com.android.internal.telephony.Connection;
     36 import com.android.internal.telephony.PhoneConstants;
     37 import com.android.internal.telephony.SmsApplication;
     38 
     39 import java.util.ArrayList;
     40 
     41 /**
     42  * Helper class to manage the "Respond via Message" feature for incoming calls.
     43  *
     44  * @see com.android.phone.InCallScreen.internalRespondViaSms()
     45  */
     46 public class RejectWithTextMessageManager {
     47     private static final String TAG = RejectWithTextMessageManager.class.getSimpleName();
     48     private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
     49 
     50     /** SharedPreferences file name for our persistent settings. */
     51     private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
     52 
     53     // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
     54     // Since (for now at least) the number of messages is fixed at 4, and since
     55     // SharedPreferences can't deal with arrays anyway, just store the messages
     56     // as 4 separate strings.
     57     private static final int NUM_CANNED_RESPONSES = 4;
     58     private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
     59     private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
     60     private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
     61     private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
     62 
     63     /**
     64      * Read the (customizable) canned responses from SharedPreferences,
     65      * or from defaults if the user has never actually brought up
     66      * the Settings UI.
     67      *
     68      * This method does disk I/O (reading the SharedPreferences file)
     69      * so don't call it from the main thread.
     70      *
     71      * @see com.android.phone.RejectWithTextMessageManager.Settings
     72      */
     73     public static ArrayList<String> loadCannedResponses() {
     74         if (DBG) log("loadCannedResponses()...");
     75 
     76         final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
     77                 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
     78         final Resources res = PhoneGlobals.getInstance().getResources();
     79 
     80         final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
     81 
     82         // Note the default values here must agree with the corresponding
     83         // android:defaultValue attributes in respond_via_sms_settings.xml.
     84 
     85         responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
     86                                        res.getString(R.string.respond_via_sms_canned_response_1)));
     87         responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
     88                                        res.getString(R.string.respond_via_sms_canned_response_2)));
     89         responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
     90                                        res.getString(R.string.respond_via_sms_canned_response_3)));
     91         responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
     92                                        res.getString(R.string.respond_via_sms_canned_response_4)));
     93         return responses;
     94     }
     95 
     96     private static void showMessageSentToast(final String phoneNumber) {
     97         // ...and show a brief confirmation to the user (since
     98         // otherwise it's hard to be sure that anything actually
     99         // happened.)
    100         // Ugly hack to show a toaster from a service.
    101         (new Thread(new Runnable() {
    102             @Override
    103             public void run() {
    104                 Looper.prepare();
    105                 Handler innerHandler = new Handler() {
    106                     @Override
    107                     public void handleMessage(Message message) {
    108                         final Resources res = PhoneGlobals.getInstance().getResources();
    109                         final String formatString = res.getString(
    110                                 R.string.respond_via_sms_confirmation_format);
    111                         final String confirmationMsg = String.format(formatString, phoneNumber);
    112                         Toast.makeText(PhoneGlobals.getInstance(), confirmationMsg,
    113                                 Toast.LENGTH_LONG).show();
    114                     }
    115 
    116                     @Override
    117                     public void dispatchMessage(Message message) {
    118                         handleMessage(message);
    119                     }
    120                 };
    121 
    122                 Message message = innerHandler.obtainMessage();
    123                 innerHandler.dispatchMessage(message);
    124                 Looper.loop();
    125             }
    126         })).start();
    127 
    128         // TODO: If the device is locked, this toast won't actually ever
    129         // be visible!  (That's because we're about to dismiss the call
    130         // screen, which means that the device will return to the
    131         // keyguard.  But toasts aren't visible on top of the keyguard.)
    132         // Possible fixes:
    133         // (1) Is it possible to allow a specific Toast to be visible
    134         //     on top of the keyguard?
    135         // (2) Artificially delay the dismissCallScreen() call by 3
    136         //     seconds to allow the toast to be seen?
    137         // (3) Don't use a toast at all; instead use a transient state
    138         //     of the InCallScreen (perhaps via the InCallUiState
    139         //     progressIndication feature), and have that state be
    140         //     visible for 3 seconds before calling dismissCallScreen().
    141     }
    142 
    143     /**
    144      * Reject the call with the specified message. If message is null this call is ignored.
    145      */
    146     public static void rejectCallWithMessage(String phoneNumber, String message) {
    147         if (message != null) {
    148             final ComponentName component =
    149                     SmsApplication.getDefaultRespondViaMessageApplication(
    150                             PhoneGlobals.getInstance(), true /*updateIfNeeded*/);
    151             if (component != null) {
    152                 // Build and send the intent
    153                 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
    154                 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
    155                 intent.putExtra(Intent.EXTRA_TEXT, message);
    156                 showMessageSentToast(phoneNumber);
    157                 intent.setComponent(component);
    158                 PhoneGlobals.getInstance().startService(intent);
    159             }
    160         }
    161     }
    162 
    163     /**
    164      * @return true if the "Respond via SMS" feature should be enabled
    165      * for the specified incoming call.
    166      *
    167      * The general rule is that we *do* allow "Respond via SMS" except for
    168      * the few (relatively rare) cases where we know for sure it won't
    169      * work, namely:
    170      *   - a bogus or blank incoming number
    171      *   - a call from a SIP address
    172      *   - a "call presentation" that doesn't allow the number to be revealed
    173      *
    174      * In all other cases, we allow the user to respond via SMS.
    175      *
    176      * Note that this behavior isn't perfect; for example we have no way
    177      * to detect whether the incoming call is from a landline (with most
    178      * networks at least), so we still enable this feature even though
    179      * SMSes to that number will silently fail.
    180      */
    181     public static boolean allowRespondViaSmsForCall(
    182             com.android.services.telephony.common.Call call, Connection conn) {
    183         if (DBG) log("allowRespondViaSmsForCall(" + call + ")...");
    184 
    185         // First some basic sanity checks:
    186         if (call == null) {
    187             Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
    188             return false;
    189         }
    190         if (!(call.getState() == com.android.services.telephony.common.Call.State.INCOMING) &&
    191                 !(call.getState() ==
    192                         com.android.services.telephony.common.Call.State.CALL_WAITING)) {
    193             // The call is in some state other than INCOMING or WAITING!
    194             // (This should almost never happen, but it *could*
    195             // conceivably happen if the ringing call got disconnected by
    196             // the network just *after* we got it from the CallManager.)
    197             Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
    198                     + call.getState());
    199             return false;
    200         }
    201 
    202         if (conn == null) {
    203             // The call doesn't have any connections! (Again, this can
    204             // happen if the ringing call disconnects at the exact right
    205             // moment, but should almost never happen in practice.)
    206             Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
    207             return false;
    208         }
    209 
    210         // Check the incoming number:
    211         final String number = conn.getAddress();
    212         if (DBG) log("- number: '" + number + "'");
    213         if (TextUtils.isEmpty(number)) {
    214             Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
    215             return false;
    216         }
    217         if (PhoneNumberUtils.isUriNumber(number)) {
    218             // The incoming number is actually a URI (i.e. a SIP address),
    219             // not a regular PSTN phone number, and we can't send SMSes to
    220             // SIP addresses.
    221             // (TODO: That might still be possible eventually, though. Is
    222             // there some SIP-specific equivalent to sending a text message?)
    223             Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
    224             return false;
    225         }
    226 
    227         // Finally, check the "call presentation":
    228         int presentation = conn.getNumberPresentation();
    229         if (DBG) log("- presentation: " + presentation);
    230         if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
    231             // PRESENTATION_RESTRICTED means "caller-id blocked".
    232             // The user isn't allowed to see the number in the first
    233             // place, so obviously we can't let you send an SMS to it.
    234             Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
    235             return false;
    236         }
    237 
    238         // Is there a valid SMS application on the phone?
    239         if (SmsApplication.getDefaultRespondViaMessageApplication(PhoneGlobals.getInstance(),
    240                 true /*updateIfNeeded*/) == null) {
    241             return false;
    242         }
    243 
    244         // TODO: with some carriers (in certain countries) you *can* actually
    245         // tell whether a given number is a mobile phone or not. So in that
    246         // case we could potentially return false here if the incoming call is
    247         // from a land line.
    248 
    249         // If none of the above special cases apply, it's OK to enable the
    250         // "Respond via SMS" feature.
    251         return true;
    252     }
    253 
    254     private static void log(String msg) {
    255         Log.d(TAG, msg);
    256     }
    257 }
    258