blob: fda8f78d54196ce036695ed56e2312c6bdb769a5 [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
Christine Chen0ce0e852013-08-09 18:26:31 -070076 private Intent mIntent;
77
78 private ArrayList<ComponentName> mComponentsWithPermission = new ArrayList<ComponentName>();
79
80 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
Christine Chenee09a492013-08-06 16:02:29 -070081 // Since (for now at least) the number of messages is fixed at 4, and since
82 // SharedPreferences can't deal with arrays anyway, just store the messages
83 // as 4 separate strings.
84 private static final int NUM_CANNED_RESPONSES = 4;
85 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
86 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
87 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
88 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
Christine Chen0ce0e852013-08-09 18:26:31 -070089 /* package */ static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT =
90 "instant_text_def_component";
Christine Chenee09a492013-08-06 16:02:29 -070091
Christine Chen0ce0e852013-08-09 18:26:31 -070092 /* package */ static final String TAG_ALL_SMS_SERVICES = "com.android.phone.AvailablePackages";
93 /* package */ static final String TAG_SEND_SMS = "com.android.phone.MessageIntent";
Christine Chenee09a492013-08-06 16:02:29 -070094
95 /**
96 * Read the (customizable) canned responses from SharedPreferences,
97 * or from defaults if the user has never actually brought up
98 * the Settings UI.
99 *
100 * This method does disk I/O (reading the SharedPreferences file)
101 * so don't call it from the main thread.
102 *
103 * @see com.android.phone.RejectWithTextMessageManager.Settings
104 */
Chiao Cheng6c6b2722013-08-22 18:35:54 -0700105 public static ArrayList<String> loadCannedResponses() {
Christine Chenee09a492013-08-06 16:02:29 -0700106 if (DBG) log("loadCannedResponses()...");
107
108 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
109 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
110 final Resources res = PhoneGlobals.getInstance().getResources();
111
112 final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
113
114 // Note the default values here must agree with the corresponding
115 // android:defaultValue attributes in respond_via_sms_settings.xml.
116
117 responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
118 res.getString(R.string.respond_via_sms_canned_response_1)));
119 responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
120 res.getString(R.string.respond_via_sms_canned_response_2)));
121 responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
122 res.getString(R.string.respond_via_sms_canned_response_3)));
123 responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
124 res.getString(R.string.respond_via_sms_canned_response_4)));
125 return responses;
126 }
127
Christine Chen0ce0e852013-08-09 18:26:31 -0700128 private void sendTextAndExit() {
Christine Chenee09a492013-08-06 16:02:29 -0700129 // Send the selected message immediately with no user interaction.
Christine Chen0ce0e852013-08-09 18:26:31 -0700130 if (mIntent.getComponent() != null) {
131 PhoneGlobals.getInstance().startService(mIntent);
Christine Chenee09a492013-08-06 16:02:29 -0700132 }
133
134 // ...and show a brief confirmation to the user (since
135 // otherwise it's hard to be sure that anything actually
136 // happened.)
137 // TODO(klp): Ask the InCallUI to show a confirmation
138
139
140 // TODO: If the device is locked, this toast won't actually ever
141 // be visible! (That's because we're about to dismiss the call
142 // screen, which means that the device will return to the
143 // keyguard. But toasts aren't visible on top of the keyguard.)
144 // Possible fixes:
145 // (1) Is it possible to allow a specific Toast to be visible
146 // on top of the keyguard?
147 // (2) Artifically delay the dismissCallScreen() call by 3
148 // seconds to allow the toast to be seen?
149 // (3) Don't use a toast at all; instead use a transient state
150 // of the InCallScreen (perhaps via the InCallUiState
151 // progressIndication feature), and have that state be
152 // visible for 3 seconds before calling dismissCallScreen().
153 }
154
155 /**
156 * Queries the System to determine what packages contain services that can handle the instant
157 * text response Action AND have permissions to do so.
158 */
Yorke Lee814da302013-08-30 16:01:07 -0700159 private static ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
Christine Chenee09a492013-08-06 16:02:29 -0700160 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
161
162 final ArrayList<ComponentName> componentsWithPermission = new ArrayList<ComponentName>();
163
164 // Get list of all services set up to handle the Instant Text intent.
165 final List<ResolveInfo> infos = packageManager.queryIntentServices(
166 getInstantTextIntent("", null, null), 0);
167
168 // Collect all the valid services
169 for (ResolveInfo resolveInfo : infos) {
170 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
171 if (serviceInfo == null) {
172 Log.w(TAG, "Ignore package without proper service.");
173 continue;
174 }
175
176 // A Service is valid only if it requires the permission
177 // PERMISSION_SEND_RESPOND_VIA_MESSAGE
178 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
179 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
180 serviceInfo.name));
181 }
182 }
183
184 return componentsWithPermission;
185 }
186
187 /**
188 * @param phoneNumber Must not be null.
189 * @param message Can be null. If message is null, the returned Intent will be configured to
190 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
191 * to be sent with no interaction from the user.
192 * @param component The component that should handle this intent.
193 * @return Service Intent for the instant response.
194 */
195 private static Intent getInstantTextIntent(String phoneNumber, String message,
196 ComponentName component) {
197 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
198 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
199 if (message != null) {
200 intent.putExtra(Intent.EXTRA_TEXT, message);
201 } else {
202 intent.putExtra("exit_on_sent", true);
203 intent.putExtra("showUI", true);
204 }
205 if (component != null) {
206 intent.setComponent(component);
207 }
208 return intent;
209 }
210
Christine Chen0ce0e852013-08-09 18:26:31 -0700211 private boolean getSmsService() {
Christine Chenee09a492013-08-06 16:02:29 -0700212 if (DBG) log("sendTextToDefaultActivity()...");
213 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
214
215 // Check to see if the default component to receive this intent is already saved
216 // and check to see if it still has the corrent permissions.
217 final SharedPreferences prefs = PhoneGlobals.getInstance().
218 getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
219 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
220 if (flattenedName != null) {
221 if (DBG) log("Default package was found." + flattenedName);
222
223 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
224 ServiceInfo serviceInfo = null;
225 try {
226 serviceInfo = packageManager.getServiceInfo(componentName, 0);
227 } catch (PackageManager.NameNotFoundException e) {
228 Log.w(TAG, "Default service does not have permission.");
229 }
230
231 if (serviceInfo != null &&
232 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
Christine Chen0ce0e852013-08-09 18:26:31 -0700233 mIntent.setComponent(componentName);
234 return true;
Christine Chenee09a492013-08-06 16:02:29 -0700235 } else {
236 SharedPreferences.Editor editor = prefs.edit();
237 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
238 editor.apply();
239 }
240 }
241
Christine Chen0ce0e852013-08-09 18:26:31 -0700242 mComponentsWithPermission = getPackagesWithInstantTextPermission();
Christine Chenee09a492013-08-06 16:02:29 -0700243
Christine Chen0ce0e852013-08-09 18:26:31 -0700244 final int size = mComponentsWithPermission.size();
Christine Chenee09a492013-08-06 16:02:29 -0700245 if (size == 0) {
246 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
Christine Chen0ce0e852013-08-09 18:26:31 -0700247 return false;
Christine Chenee09a492013-08-06 16:02:29 -0700248 } else if (size == 1) {
Christine Chen0ce0e852013-08-09 18:26:31 -0700249 mIntent.setComponent(mComponentsWithPermission.get(0));
250 return true;
Christine Chenee09a492013-08-06 16:02:29 -0700251 } else {
252 Log.v(TAG, "Choosing from one of the apps");
253 // TODO(klp): Add an app picker.
Christine Chen0ce0e852013-08-09 18:26:31 -0700254 final Intent intent = new Intent(Intent.ACTION_VIEW, null);
255 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
256 Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS |
257 Intent.FLAG_ACTIVITY_NO_ANIMATION |
258 Intent.FLAG_ACTIVITY_NO_HISTORY |
259 Intent.FLAG_FROM_BACKGROUND);
260 intent.setClass(PhoneGlobals.getInstance(), TextMessagePackageChooser.class);
261 intent.putExtra(TAG_ALL_SMS_SERVICES, mComponentsWithPermission);
262 intent.putExtra(TAG_SEND_SMS, mIntent);
263 PhoneGlobals.getInstance().startActivity(intent);
264 return false;
265 // return componentsWithPermission.get(0);
Christine Chenee09a492013-08-06 16:02:29 -0700266 }
267 }
268
Christine Chen0ce0e852013-08-09 18:26:31 -0700269 public void rejectCallWithMessage(Call call, String message) {
270 mComponentsWithPermission.clear();
271 mIntent = getInstantTextIntent(call.getLatestConnection().getAddress(), message, null);
272 if (getSmsService()) {
273 sendTextAndExit();
Christine Chenee09a492013-08-06 16:02:29 -0700274 }
275 }
276
Yorke Lee814da302013-08-30 16:01:07 -0700277 /**
278 * @return true if the "Respond via SMS" feature should be enabled
279 * for the specified incoming call.
280 *
281 * The general rule is that we *do* allow "Respond via SMS" except for
282 * the few (relatively rare) cases where we know for sure it won't
283 * work, namely:
284 * - a bogus or blank incoming number
285 * - a call from a SIP address
286 * - a "call presentation" that doesn't allow the number to be revealed
287 *
288 * In all other cases, we allow the user to respond via SMS.
289 *
290 * Note that this behavior isn't perfect; for example we have no way
291 * to detect whether the incoming call is from a landline (with most
292 * networks at least), so we still enable this feature even though
293 * SMSes to that number will silently fail.
294 */
295 public static boolean allowRespondViaSmsForCall(
296 com.android.services.telephony.common.Call call, Connection conn) {
297 if (DBG) log("allowRespondViaSmsForCall(" + call + ")...");
298
299 // First some basic sanity checks:
300 if (call == null) {
301 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
302 return false;
303 }
304 if (!(call.getState() == com.android.services.telephony.common.Call.State.INCOMING) &&
305 !(call.getState() ==
306 com.android.services.telephony.common.Call.State.CALL_WAITING)) {
307 // The call is in some state other than INCOMING or WAITING!
308 // (This should almost never happen, but it *could*
309 // conceivably happen if the ringing call got disconnected by
310 // the network just *after* we got it from the CallManager.)
311 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
312 + call.getState());
313 return false;
314 }
315
316 if (conn == null) {
317 // The call doesn't have any connections! (Again, this can
318 // happen if the ringing call disconnects at the exact right
319 // moment, but should almost never happen in practice.)
320 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
321 return false;
322 }
323
324 // Check the incoming number:
325 final String number = conn.getAddress();
326 if (DBG) log("- number: '" + number + "'");
327 if (TextUtils.isEmpty(number)) {
328 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
329 return false;
330 }
331 if (PhoneNumberUtils.isUriNumber(number)) {
332 // The incoming number is actually a URI (i.e. a SIP address),
333 // not a regular PSTN phone number, and we can't send SMSes to
334 // SIP addresses.
335 // (TODO: That might still be possible eventually, though. Is
336 // there some SIP-specific equivalent to sending a text message?)
337 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
338 return false;
339 }
340
341 // Finally, check the "call presentation":
342 int presentation = conn.getNumberPresentation();
343 if (DBG) log("- presentation: " + presentation);
344 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
345 // PRESENTATION_RESTRICTED means "caller-id blocked".
346 // The user isn't allowed to see the number in the first
347 // place, so obviously we can't let you send an SMS to it.
348 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
349 return false;
350 }
351
352 // Allow the feature only when there's a destination for it.
353 if (getPackagesWithInstantTextPermission().size() < 1) {
354 return false;
355 }
356
357 // TODO: with some carriers (in certain countries) you *can* actually
358 // tell whether a given number is a mobile phone or not. So in that
359 // case we could potentially return false here if the incoming call is
360 // from a land line.
361
362 // If none of the above special cases apply, it's OK to enable the
363 // "Respond via SMS" feature.
364 return true;
365 }
366
Christine Chenee09a492013-08-06 16:02:29 -0700367 private static void log(String msg) {
368 Log.d(TAG, msg);
369 }
370}