blob: b816eb0879ef8d090c955eb82ede52450e00850c [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;
Christine Chen32f4a8d2013-09-18 21:11:17 -070035import android.os.Looper;
36import android.os.Message;
Yorke Lee814da302013-08-30 16:01:07 -070037import android.telephony.PhoneNumberUtils;
Christine Chenee09a492013-08-06 16:02:29 -070038import android.telephony.TelephonyManager;
Yorke Lee814da302013-08-30 16:01:07 -070039import android.text.TextUtils;
Christine Chenee09a492013-08-06 16:02:29 -070040import android.util.Log;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.ViewGroup;
44import android.widget.BaseAdapter;
45import android.widget.CheckBox;
46import android.widget.CompoundButton;
47import android.widget.ImageView;
48import android.widget.TextView;
Christine Chen32f4a8d2013-09-18 21:11:17 -070049import android.widget.Toast;
Christine Chenee09a492013-08-06 16:02:29 -070050
51import com.android.internal.telephony.Call;
52import com.android.internal.telephony.Connection;
53import com.android.internal.telephony.PhoneConstants;
Yorke Lee814da302013-08-30 16:01:07 -070054
Christine Chenee09a492013-08-06 16:02:29 -070055import com.google.android.collect.Lists;
56
57import java.util.ArrayList;
58import java.util.List;
59
60/**
61 * Helper class to manage the "Respond via Message" feature for incoming calls.
62 *
63 * @see com.android.phone.InCallScreen.internalRespondViaSms()
64 */
65public class RejectWithTextMessageManager {
66
67 private static final String TAG = RejectWithTextMessageManager.class.getSimpleName();
68 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
69
70 private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
71 "android.permission.SEND_RESPOND_VIA_MESSAGE";
72
73 /** The array of "canned responses"; see loadCannedResponses(). */
74 private String[] mCannedResponses;
75
76 /** SharedPreferences file name for our persistent settings. */
77 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
78
Christine Chen0ce0e852013-08-09 18:26:31 -070079 private Intent mIntent;
80
81 private ArrayList<ComponentName> mComponentsWithPermission = new ArrayList<ComponentName>();
82
83 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
Christine Chenee09a492013-08-06 16:02:29 -070084 // Since (for now at least) the number of messages is fixed at 4, and since
85 // SharedPreferences can't deal with arrays anyway, just store the messages
86 // as 4 separate strings.
87 private static final int NUM_CANNED_RESPONSES = 4;
88 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
89 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
90 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
91 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
Christine Chen0ce0e852013-08-09 18:26:31 -070092 /* package */ static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT =
93 "instant_text_def_component";
Christine Chenee09a492013-08-06 16:02:29 -070094
Christine Chen0ce0e852013-08-09 18:26:31 -070095 /* package */ static final String TAG_ALL_SMS_SERVICES = "com.android.phone.AvailablePackages";
96 /* package */ static final String TAG_SEND_SMS = "com.android.phone.MessageIntent";
Christine Chen32f4a8d2013-09-18 21:11:17 -070097 /* package */ static final String TAG_SMS_DESTINATION = "com.android.phone.SmsDestination";
Christine Chenee09a492013-08-06 16:02:29 -070098
99 /**
100 * Read the (customizable) canned responses from SharedPreferences,
101 * or from defaults if the user has never actually brought up
102 * the Settings UI.
103 *
104 * This method does disk I/O (reading the SharedPreferences file)
105 * so don't call it from the main thread.
106 *
107 * @see com.android.phone.RejectWithTextMessageManager.Settings
108 */
Chiao Cheng6c6b2722013-08-22 18:35:54 -0700109 public static ArrayList<String> loadCannedResponses() {
Christine Chenee09a492013-08-06 16:02:29 -0700110 if (DBG) log("loadCannedResponses()...");
111
112 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
113 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
114 final Resources res = PhoneGlobals.getInstance().getResources();
115
116 final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
117
118 // Note the default values here must agree with the corresponding
119 // android:defaultValue attributes in respond_via_sms_settings.xml.
120
121 responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
122 res.getString(R.string.respond_via_sms_canned_response_1)));
123 responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
124 res.getString(R.string.respond_via_sms_canned_response_2)));
125 responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
126 res.getString(R.string.respond_via_sms_canned_response_3)));
127 responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
128 res.getString(R.string.respond_via_sms_canned_response_4)));
129 return responses;
130 }
131
Christine Chen32f4a8d2013-09-18 21:11:17 -0700132 private void sendTextAndExit(final String phoneNumber) {
Christine Chenee09a492013-08-06 16:02:29 -0700133 // Send the selected message immediately with no user interaction.
Christine Chen0ce0e852013-08-09 18:26:31 -0700134 if (mIntent.getComponent() != null) {
135 PhoneGlobals.getInstance().startService(mIntent);
Christine Chen32f4a8d2013-09-18 21:11:17 -0700136
137 // ...and show a brief confirmation to the user (since
138 // otherwise it's hard to be sure that anything actually
139 // happened.)
140 // Ugly hack to show a toaster from a service.
141 (new Thread(new Runnable() {
142 @Override
143 public void run() {
144 Looper.prepare();
145 Handler innerHandler = new Handler() {
146 @Override
147 public void handleMessage(Message message) {
148 final Resources res = PhoneGlobals.getInstance().getResources();
149 final String formatString = res.getString(
150 R.string.respond_via_sms_confirmation_format);
151 final String confirmationMsg = String.format(formatString, phoneNumber);
152 Toast.makeText(PhoneGlobals.getInstance(), confirmationMsg,
153 Toast.LENGTH_LONG).show();
154 }
155
156 @Override
157 public void dispatchMessage(Message message) {
158 handleMessage(message);
159 }
160 };
161
162 Message message = innerHandler.obtainMessage();
163 innerHandler.dispatchMessage(message);
164 Looper.loop();
165 }
166 })).start();
Christine Chenee09a492013-08-06 16:02:29 -0700167 }
168
Christine Chenee09a492013-08-06 16:02:29 -0700169 // TODO: If the device is locked, this toast won't actually ever
170 // be visible! (That's because we're about to dismiss the call
171 // screen, which means that the device will return to the
172 // keyguard. But toasts aren't visible on top of the keyguard.)
173 // Possible fixes:
174 // (1) Is it possible to allow a specific Toast to be visible
175 // on top of the keyguard?
176 // (2) Artifically delay the dismissCallScreen() call by 3
177 // seconds to allow the toast to be seen?
178 // (3) Don't use a toast at all; instead use a transient state
179 // of the InCallScreen (perhaps via the InCallUiState
180 // progressIndication feature), and have that state be
181 // visible for 3 seconds before calling dismissCallScreen().
182 }
183
184 /**
185 * Queries the System to determine what packages contain services that can handle the instant
186 * text response Action AND have permissions to do so.
187 */
Yorke Lee814da302013-08-30 16:01:07 -0700188 private static ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
Christine Chenee09a492013-08-06 16:02:29 -0700189 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
190
191 final ArrayList<ComponentName> componentsWithPermission = new ArrayList<ComponentName>();
192
193 // Get list of all services set up to handle the Instant Text intent.
194 final List<ResolveInfo> infos = packageManager.queryIntentServices(
195 getInstantTextIntent("", null, null), 0);
196
197 // Collect all the valid services
198 for (ResolveInfo resolveInfo : infos) {
199 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
200 if (serviceInfo == null) {
201 Log.w(TAG, "Ignore package without proper service.");
202 continue;
203 }
204
205 // A Service is valid only if it requires the permission
206 // PERMISSION_SEND_RESPOND_VIA_MESSAGE
207 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
208 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
209 serviceInfo.name));
210 }
211 }
212
213 return componentsWithPermission;
214 }
215
216 /**
217 * @param phoneNumber Must not be null.
218 * @param message Can be null. If message is null, the returned Intent will be configured to
219 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
220 * to be sent with no interaction from the user.
221 * @param component The component that should handle this intent.
222 * @return Service Intent for the instant response.
223 */
224 private static Intent getInstantTextIntent(String phoneNumber, String message,
225 ComponentName component) {
226 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
227 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
228 if (message != null) {
229 intent.putExtra(Intent.EXTRA_TEXT, message);
230 } else {
231 intent.putExtra("exit_on_sent", true);
232 intent.putExtra("showUI", true);
233 }
234 if (component != null) {
235 intent.setComponent(component);
236 }
237 return intent;
238 }
239
Christine Chen32f4a8d2013-09-18 21:11:17 -0700240 private boolean getSmsService(String phoneNumber) {
Christine Chenee09a492013-08-06 16:02:29 -0700241 if (DBG) log("sendTextToDefaultActivity()...");
242 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
243
244 // Check to see if the default component to receive this intent is already saved
245 // and check to see if it still has the corrent permissions.
246 final SharedPreferences prefs = PhoneGlobals.getInstance().
247 getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
248 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
249 if (flattenedName != null) {
250 if (DBG) log("Default package was found." + flattenedName);
251
252 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
253 ServiceInfo serviceInfo = null;
254 try {
255 serviceInfo = packageManager.getServiceInfo(componentName, 0);
256 } catch (PackageManager.NameNotFoundException e) {
257 Log.w(TAG, "Default service does not have permission.");
258 }
259
260 if (serviceInfo != null &&
261 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
Christine Chen0ce0e852013-08-09 18:26:31 -0700262 mIntent.setComponent(componentName);
263 return true;
Christine Chenee09a492013-08-06 16:02:29 -0700264 } else {
265 SharedPreferences.Editor editor = prefs.edit();
266 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
267 editor.apply();
268 }
269 }
270
Christine Chen0ce0e852013-08-09 18:26:31 -0700271 mComponentsWithPermission = getPackagesWithInstantTextPermission();
Christine Chenee09a492013-08-06 16:02:29 -0700272
Christine Chen0ce0e852013-08-09 18:26:31 -0700273 final int size = mComponentsWithPermission.size();
Christine Chenee09a492013-08-06 16:02:29 -0700274 if (size == 0) {
275 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
Christine Chen0ce0e852013-08-09 18:26:31 -0700276 return false;
Christine Chenee09a492013-08-06 16:02:29 -0700277 } else if (size == 1) {
Christine Chen0ce0e852013-08-09 18:26:31 -0700278 mIntent.setComponent(mComponentsWithPermission.get(0));
279 return true;
Christine Chenee09a492013-08-06 16:02:29 -0700280 } else {
281 Log.v(TAG, "Choosing from one of the apps");
Christine Chen0ce0e852013-08-09 18:26:31 -0700282 final Intent intent = new Intent(Intent.ACTION_VIEW, null);
283 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
284 Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS |
285 Intent.FLAG_ACTIVITY_NO_ANIMATION |
286 Intent.FLAG_ACTIVITY_NO_HISTORY |
287 Intent.FLAG_FROM_BACKGROUND);
288 intent.setClass(PhoneGlobals.getInstance(), TextMessagePackageChooser.class);
289 intent.putExtra(TAG_ALL_SMS_SERVICES, mComponentsWithPermission);
290 intent.putExtra(TAG_SEND_SMS, mIntent);
Christine Chen32f4a8d2013-09-18 21:11:17 -0700291 intent.putExtra(TAG_SMS_DESTINATION, phoneNumber);
Christine Chen0ce0e852013-08-09 18:26:31 -0700292 PhoneGlobals.getInstance().startActivity(intent);
293 return false;
294 // return componentsWithPermission.get(0);
Christine Chenee09a492013-08-06 16:02:29 -0700295 }
296 }
297
Christine Chen0ce0e852013-08-09 18:26:31 -0700298 public void rejectCallWithMessage(Call call, String message) {
299 mComponentsWithPermission.clear();
Christine Chen32f4a8d2013-09-18 21:11:17 -0700300 final String phoneNumber = call.getLatestConnection().getAddress();
301 mIntent = getInstantTextIntent(phoneNumber, message, null);
302 if (getSmsService(phoneNumber)) {
303 sendTextAndExit(phoneNumber);
Christine Chenee09a492013-08-06 16:02:29 -0700304 }
305 }
306
Yorke Lee814da302013-08-30 16:01:07 -0700307 /**
308 * @return true if the "Respond via SMS" feature should be enabled
309 * for the specified incoming call.
310 *
311 * The general rule is that we *do* allow "Respond via SMS" except for
312 * the few (relatively rare) cases where we know for sure it won't
313 * work, namely:
314 * - a bogus or blank incoming number
315 * - a call from a SIP address
316 * - a "call presentation" that doesn't allow the number to be revealed
317 *
318 * In all other cases, we allow the user to respond via SMS.
319 *
320 * Note that this behavior isn't perfect; for example we have no way
321 * to detect whether the incoming call is from a landline (with most
322 * networks at least), so we still enable this feature even though
323 * SMSes to that number will silently fail.
324 */
325 public static boolean allowRespondViaSmsForCall(
326 com.android.services.telephony.common.Call call, Connection conn) {
327 if (DBG) log("allowRespondViaSmsForCall(" + call + ")...");
328
329 // First some basic sanity checks:
330 if (call == null) {
331 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
332 return false;
333 }
334 if (!(call.getState() == com.android.services.telephony.common.Call.State.INCOMING) &&
335 !(call.getState() ==
336 com.android.services.telephony.common.Call.State.CALL_WAITING)) {
337 // The call is in some state other than INCOMING or WAITING!
338 // (This should almost never happen, but it *could*
339 // conceivably happen if the ringing call got disconnected by
340 // the network just *after* we got it from the CallManager.)
341 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
342 + call.getState());
343 return false;
344 }
345
346 if (conn == null) {
347 // The call doesn't have any connections! (Again, this can
348 // happen if the ringing call disconnects at the exact right
349 // moment, but should almost never happen in practice.)
350 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
351 return false;
352 }
353
354 // Check the incoming number:
355 final String number = conn.getAddress();
356 if (DBG) log("- number: '" + number + "'");
357 if (TextUtils.isEmpty(number)) {
358 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
359 return false;
360 }
361 if (PhoneNumberUtils.isUriNumber(number)) {
362 // The incoming number is actually a URI (i.e. a SIP address),
363 // not a regular PSTN phone number, and we can't send SMSes to
364 // SIP addresses.
365 // (TODO: That might still be possible eventually, though. Is
366 // there some SIP-specific equivalent to sending a text message?)
367 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
368 return false;
369 }
370
371 // Finally, check the "call presentation":
372 int presentation = conn.getNumberPresentation();
373 if (DBG) log("- presentation: " + presentation);
374 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
375 // PRESENTATION_RESTRICTED means "caller-id blocked".
376 // The user isn't allowed to see the number in the first
377 // place, so obviously we can't let you send an SMS to it.
378 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
379 return false;
380 }
381
382 // Allow the feature only when there's a destination for it.
383 if (getPackagesWithInstantTextPermission().size() < 1) {
384 return false;
385 }
386
387 // TODO: with some carriers (in certain countries) you *can* actually
388 // tell whether a given number is a mobile phone or not. So in that
389 // case we could potentially return false here if the incoming call is
390 // from a land line.
391
392 // If none of the above special cases apply, it's OK to enable the
393 // "Respond via SMS" feature.
394 return true;
395 }
396
Christine Chenee09a492013-08-06 16:02:29 -0700397 private static void log(String msg) {
398 Log.d(TAG, msg);
399 }
400}