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
17package com.android.phone;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.content.res.Resources;
24import android.net.Uri;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.Message;
28import android.telephony.PhoneNumberUtils;
29import android.telephony.TelephonyManager;
30import android.text.TextUtils;
31import android.util.Log;
32import android.widget.Toast;
33
34import com.android.internal.telephony.Call;
35import com.android.internal.telephony.Connection;
36import com.android.internal.telephony.PhoneConstants;
37import com.android.internal.telephony.SmsApplication;
38
39import 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 */
46public 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