blob: b03211a42b559eeb45dc2fcae65d5756fc5601db [file] [log] [blame]
Christine Chenee09a492013-08-06 16:02:29 -07001/*
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.app.ActivityManager;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.pm.ServiceInfo;
31import android.content.res.Resources;
32import android.graphics.drawable.Drawable;
33import android.net.Uri;
34import android.os.Handler;
Yorke Lee814da302013-08-30 16:01:07 -070035import android.telephony.PhoneNumberUtils;
Christine Chenee09a492013-08-06 16:02:29 -070036import android.telephony.TelephonyManager;
Yorke Lee814da302013-08-30 16:01:07 -070037import android.text.TextUtils;
Christine Chenee09a492013-08-06 16:02:29 -070038import android.util.Log;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.widget.BaseAdapter;
43import android.widget.CheckBox;
44import android.widget.CompoundButton;
45import android.widget.ImageView;
46import android.widget.TextView;
47
48import com.android.internal.telephony.Call;
49import com.android.internal.telephony.Connection;
50import com.android.internal.telephony.PhoneConstants;
Yorke Lee814da302013-08-30 16:01:07 -070051
Christine Chenee09a492013-08-06 16:02:29 -070052import com.google.android.collect.Lists;
53
54import java.util.ArrayList;
55import java.util.List;
56
57/**
58 * Helper class to manage the "Respond via Message" feature for incoming calls.
59 *
60 * @see com.android.phone.InCallScreen.internalRespondViaSms()
61 */
62public class RejectWithTextMessageManager {
63
64 private static final String TAG = RejectWithTextMessageManager.class.getSimpleName();
65 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
66
67 private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
68 "android.permission.SEND_RESPOND_VIA_MESSAGE";
69
70 /** The array of "canned responses"; see loadCannedResponses(). */
71 private String[] mCannedResponses;
72
73 /** SharedPreferences file name for our persistent settings. */
74 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
75
76 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
77 // Since (for now at least) the number of messages is fixed at 4, and since
78 // SharedPreferences can't deal with arrays anyway, just store the messages
79 // as 4 separate strings.
80 private static final int NUM_CANNED_RESPONSES = 4;
81 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
82 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
83 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
84 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
85 private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
86 private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
87
88 /**
89 * Brings up the standard SMS compose UI.
90 */
91 private void launchSmsCompose(String phoneNumber) {
92 if (DBG) log("launchSmsCompose: number " + phoneNumber);
93
94 final Intent intent = getInstantTextIntent(phoneNumber, null, getSmsService());
95
96 if (DBG) log("- Launching SMS compose UI: " + intent);
97 PhoneGlobals.getInstance().startService(intent);
98 }
99
100 /**
101 * Read the (customizable) canned responses from SharedPreferences,
102 * or from defaults if the user has never actually brought up
103 * the Settings UI.
104 *
105 * This method does disk I/O (reading the SharedPreferences file)
106 * so don't call it from the main thread.
107 *
108 * @see com.android.phone.RejectWithTextMessageManager.Settings
109 */
Chiao Cheng6c6b2722013-08-22 18:35:54 -0700110 public static ArrayList<String> loadCannedResponses() {
Christine Chenee09a492013-08-06 16:02:29 -0700111 if (DBG) log("loadCannedResponses()...");
112
113 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
114 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
115 final Resources res = PhoneGlobals.getInstance().getResources();
116
117 final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
118
119 // Note the default values here must agree with the corresponding
120 // android:defaultValue attributes in respond_via_sms_settings.xml.
121
122 responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
123 res.getString(R.string.respond_via_sms_canned_response_1)));
124 responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
125 res.getString(R.string.respond_via_sms_canned_response_2)));
126 responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
127 res.getString(R.string.respond_via_sms_canned_response_3)));
128 responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
129 res.getString(R.string.respond_via_sms_canned_response_4)));
130 return responses;
131 }
132
133 /**
Christine Chenee09a492013-08-06 16:02:29 -0700134 * Sends a text message without any interaction from the user.
135 */
136 private void sendText(String phoneNumber, String message, ComponentName component) {
137 if (DBG) log("sendText: number "
138 + phoneNumber + ", message '" + message + "'");
139
140 PhoneGlobals.getInstance().startService(getInstantTextIntent(phoneNumber, message,
141 component));
142 }
143
144 private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
145 boolean setDefaultComponent) {
146 // Send the selected message immediately with no user interaction.
147 sendText(phoneNumber, message, component);
148
149 if (setDefaultComponent) {
150 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
151 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
152 prefs.edit()
153 .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
154 .apply();
155 }
156
157 // ...and show a brief confirmation to the user (since
158 // otherwise it's hard to be sure that anything actually
159 // happened.)
160 // TODO(klp): Ask the InCallUI to show a confirmation
161
162
163 // TODO: If the device is locked, this toast won't actually ever
164 // be visible! (That's because we're about to dismiss the call
165 // screen, which means that the device will return to the
166 // keyguard. But toasts aren't visible on top of the keyguard.)
167 // Possible fixes:
168 // (1) Is it possible to allow a specific Toast to be visible
169 // on top of the keyguard?
170 // (2) Artifically delay the dismissCallScreen() call by 3
171 // seconds to allow the toast to be seen?
172 // (3) Don't use a toast at all; instead use a transient state
173 // of the InCallScreen (perhaps via the InCallUiState
174 // progressIndication feature), and have that state be
175 // visible for 3 seconds before calling dismissCallScreen().
176 }
177
178 /**
179 * Queries the System to determine what packages contain services that can handle the instant
180 * text response Action AND have permissions to do so.
181 */
Yorke Lee814da302013-08-30 16:01:07 -0700182 private static ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
Christine Chenee09a492013-08-06 16:02:29 -0700183 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
184
185 final ArrayList<ComponentName> componentsWithPermission = new ArrayList<ComponentName>();
186
187 // Get list of all services set up to handle the Instant Text intent.
188 final List<ResolveInfo> infos = packageManager.queryIntentServices(
189 getInstantTextIntent("", null, null), 0);
190
191 // Collect all the valid services
192 for (ResolveInfo resolveInfo : infos) {
193 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
194 if (serviceInfo == null) {
195 Log.w(TAG, "Ignore package without proper service.");
196 continue;
197 }
198
199 // A Service is valid only if it requires the permission
200 // PERMISSION_SEND_RESPOND_VIA_MESSAGE
201 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
202 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
203 serviceInfo.name));
204 }
205 }
206
207 return componentsWithPermission;
208 }
209
210 /**
211 * @param phoneNumber Must not be null.
212 * @param message Can be null. If message is null, the returned Intent will be configured to
213 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
214 * to be sent with no interaction from the user.
215 * @param component The component that should handle this intent.
216 * @return Service Intent for the instant response.
217 */
218 private static Intent getInstantTextIntent(String phoneNumber, String message,
219 ComponentName component) {
220 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
221 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
222 if (message != null) {
223 intent.putExtra(Intent.EXTRA_TEXT, message);
224 } else {
225 intent.putExtra("exit_on_sent", true);
226 intent.putExtra("showUI", true);
227 }
228 if (component != null) {
229 intent.setComponent(component);
230 }
231 return intent;
232 }
233
Yorke Lee814da302013-08-30 16:01:07 -0700234 public void rejectCallWithNewMessage(String number) {
235 launchSmsCompose(number);
Christine Chenee09a492013-08-06 16:02:29 -0700236 }
237
238 private ComponentName getSmsService() {
239 if (DBG) log("sendTextToDefaultActivity()...");
240 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
241
242 // Check to see if the default component to receive this intent is already saved
243 // and check to see if it still has the corrent permissions.
244 final SharedPreferences prefs = PhoneGlobals.getInstance().
245 getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
246 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
247 if (flattenedName != null) {
248 if (DBG) log("Default package was found." + flattenedName);
249
250 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
251 ServiceInfo serviceInfo = null;
252 try {
253 serviceInfo = packageManager.getServiceInfo(componentName, 0);
254 } catch (PackageManager.NameNotFoundException e) {
255 Log.w(TAG, "Default service does not have permission.");
256 }
257
258 if (serviceInfo != null &&
259 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
260 return componentName;
261 } else {
262 SharedPreferences.Editor editor = prefs.edit();
263 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
264 editor.apply();
265 }
266 }
267
268 final ArrayList<ComponentName> componentsWithPermission =
269 getPackagesWithInstantTextPermission();
270
271 final int size = componentsWithPermission.size();
272 if (size == 0) {
273 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
274 return null;
275 } else if (size == 1) {
276 return componentsWithPermission.get(0);
277 } else {
278 Log.v(TAG, "Choosing from one of the apps");
279 // TODO(klp): Add an app picker.
280 return componentsWithPermission.get(0);
281 }
282 }
283
284
Yorke Lee814da302013-08-30 16:01:07 -0700285 public void rejectCallWithMessage(final String number, String message) {
Christine Chenee09a492013-08-06 16:02:29 -0700286 final ComponentName componentName = getSmsService();
287
288 if (componentName != null) {
Yorke Lee814da302013-08-30 16:01:07 -0700289 sendTextAndExit(number, message, componentName,
Christine Chenee09a492013-08-06 16:02:29 -0700290 false);
291 }
292 }
293
Yorke Lee814da302013-08-30 16:01:07 -0700294 /**
295 * @return true if the "Respond via SMS" feature should be enabled
296 * for the specified incoming call.
297 *
298 * The general rule is that we *do* allow "Respond via SMS" except for
299 * the few (relatively rare) cases where we know for sure it won't
300 * work, namely:
301 * - a bogus or blank incoming number
302 * - a call from a SIP address
303 * - a "call presentation" that doesn't allow the number to be revealed
304 *
305 * In all other cases, we allow the user to respond via SMS.
306 *
307 * Note that this behavior isn't perfect; for example we have no way
308 * to detect whether the incoming call is from a landline (with most
309 * networks at least), so we still enable this feature even though
310 * SMSes to that number will silently fail.
311 */
312 public static boolean allowRespondViaSmsForCall(
313 com.android.services.telephony.common.Call call, Connection conn) {
314 if (DBG) log("allowRespondViaSmsForCall(" + call + ")...");
315
316 // First some basic sanity checks:
317 if (call == null) {
318 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
319 return false;
320 }
321 if (!(call.getState() == com.android.services.telephony.common.Call.State.INCOMING) &&
322 !(call.getState() ==
323 com.android.services.telephony.common.Call.State.CALL_WAITING)) {
324 // The call is in some state other than INCOMING or WAITING!
325 // (This should almost never happen, but it *could*
326 // conceivably happen if the ringing call got disconnected by
327 // the network just *after* we got it from the CallManager.)
328 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
329 + call.getState());
330 return false;
331 }
332
333 if (conn == null) {
334 // The call doesn't have any connections! (Again, this can
335 // happen if the ringing call disconnects at the exact right
336 // moment, but should almost never happen in practice.)
337 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
338 return false;
339 }
340
341 // Check the incoming number:
342 final String number = conn.getAddress();
343 if (DBG) log("- number: '" + number + "'");
344 if (TextUtils.isEmpty(number)) {
345 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
346 return false;
347 }
348 if (PhoneNumberUtils.isUriNumber(number)) {
349 // The incoming number is actually a URI (i.e. a SIP address),
350 // not a regular PSTN phone number, and we can't send SMSes to
351 // SIP addresses.
352 // (TODO: That might still be possible eventually, though. Is
353 // there some SIP-specific equivalent to sending a text message?)
354 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
355 return false;
356 }
357
358 // Finally, check the "call presentation":
359 int presentation = conn.getNumberPresentation();
360 if (DBG) log("- presentation: " + presentation);
361 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
362 // PRESENTATION_RESTRICTED means "caller-id blocked".
363 // The user isn't allowed to see the number in the first
364 // place, so obviously we can't let you send an SMS to it.
365 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
366 return false;
367 }
368
369 // Allow the feature only when there's a destination for it.
370 if (getPackagesWithInstantTextPermission().size() < 1) {
371 return false;
372 }
373
374 // TODO: with some carriers (in certain countries) you *can* actually
375 // tell whether a given number is a mobile phone or not. So in that
376 // case we could potentially return false here if the incoming call is
377 // from a land line.
378
379 // If none of the above special cases apply, it's OK to enable the
380 // "Respond via SMS" feature.
381 return true;
382 }
383
Christine Chenee09a492013-08-06 16:02:29 -0700384 private static void log(String msg) {
385 Log.d(TAG, msg);
386 }
387}