blob: f0321697d6ca22693e587e1eff6c62e84e177af3 [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;
35import android.telephony.TelephonyManager;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.BaseAdapter;
41import android.widget.CheckBox;
42import android.widget.CompoundButton;
43import android.widget.ImageView;
44import android.widget.TextView;
45
46import com.android.internal.telephony.Call;
47import com.android.internal.telephony.Connection;
48import com.android.internal.telephony.PhoneConstants;
49import com.google.android.collect.Lists;
50
51import java.util.ArrayList;
52import java.util.List;
53
54/**
55 * Helper class to manage the "Respond via Message" feature for incoming calls.
56 *
57 * @see com.android.phone.InCallScreen.internalRespondViaSms()
58 */
59public class RejectWithTextMessageManager {
60
61 private static final String TAG = RejectWithTextMessageManager.class.getSimpleName();
62 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
63
64 private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
65 "android.permission.SEND_RESPOND_VIA_MESSAGE";
66
67 /** The array of "canned responses"; see loadCannedResponses(). */
68 private String[] mCannedResponses;
69
70 /** SharedPreferences file name for our persistent settings. */
71 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
72
73 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
74 // Since (for now at least) the number of messages is fixed at 4, and since
75 // SharedPreferences can't deal with arrays anyway, just store the messages
76 // as 4 separate strings.
77 private static final int NUM_CANNED_RESPONSES = 4;
78 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
79 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
80 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
81 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
82 private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
83 private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
84
85 /**
86 * Brings up the standard SMS compose UI.
87 */
88 private void launchSmsCompose(String phoneNumber) {
89 if (DBG) log("launchSmsCompose: number " + phoneNumber);
90
91 final Intent intent = getInstantTextIntent(phoneNumber, null, getSmsService());
92
93 if (DBG) log("- Launching SMS compose UI: " + intent);
94 PhoneGlobals.getInstance().startService(intent);
95 }
96
97 /**
98 * Read the (customizable) canned responses from SharedPreferences,
99 * or from defaults if the user has never actually brought up
100 * the Settings UI.
101 *
102 * This method does disk I/O (reading the SharedPreferences file)
103 * so don't call it from the main thread.
104 *
105 * @see com.android.phone.RejectWithTextMessageManager.Settings
106 */
107 public ArrayList<String> loadCannedResponses() {
108 if (DBG) log("loadCannedResponses()...");
109
110 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
111 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
112 final Resources res = PhoneGlobals.getInstance().getResources();
113
114 final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
115
116 // Note the default values here must agree with the corresponding
117 // android:defaultValue attributes in respond_via_sms_settings.xml.
118
119 responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
120 res.getString(R.string.respond_via_sms_canned_response_1)));
121 responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
122 res.getString(R.string.respond_via_sms_canned_response_2)));
123 responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
124 res.getString(R.string.respond_via_sms_canned_response_3)));
125 responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
126 res.getString(R.string.respond_via_sms_canned_response_4)));
127 return responses;
128 }
129
130 /**
131 * @return true if the "Respond via SMS" feature should be enabled
132 * for the specified incoming call.
133 *
134 * The general rule is that we *do* allow "Respond via SMS" except for
135 * the few (relatively rare) cases where we know for sure it won't
136 * work, namely:
137 * - a bogus or blank incoming number
138 * - a call from a SIP address
139 * - a "call presentation" that doesn't allow the number to be revealed
140 *
141 * In all other cases, we allow the user to respond via SMS.
142 *
143 * Note that this behavior isn't perfect; for example we have no way
144 * to detect whether the incoming call is from a landline (with most
145 * networks at least), so we still enable this feature even though
146 * SMSes to that number will silently fail.
147 */
148 public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
149 // TODO(klp) implement this!
150 return true;
151 }
152
153 /**
154 * Sends a text message without any interaction from the user.
155 */
156 private void sendText(String phoneNumber, String message, ComponentName component) {
157 if (DBG) log("sendText: number "
158 + phoneNumber + ", message '" + message + "'");
159
160 PhoneGlobals.getInstance().startService(getInstantTextIntent(phoneNumber, message,
161 component));
162 }
163
164 private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
165 boolean setDefaultComponent) {
166 // Send the selected message immediately with no user interaction.
167 sendText(phoneNumber, message, component);
168
169 if (setDefaultComponent) {
170 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
171 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
172 prefs.edit()
173 .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
174 .apply();
175 }
176
177 // ...and show a brief confirmation to the user (since
178 // otherwise it's hard to be sure that anything actually
179 // happened.)
180 // TODO(klp): Ask the InCallUI to show a confirmation
181
182
183 // TODO: If the device is locked, this toast won't actually ever
184 // be visible! (That's because we're about to dismiss the call
185 // screen, which means that the device will return to the
186 // keyguard. But toasts aren't visible on top of the keyguard.)
187 // Possible fixes:
188 // (1) Is it possible to allow a specific Toast to be visible
189 // on top of the keyguard?
190 // (2) Artifically delay the dismissCallScreen() call by 3
191 // seconds to allow the toast to be seen?
192 // (3) Don't use a toast at all; instead use a transient state
193 // of the InCallScreen (perhaps via the InCallUiState
194 // progressIndication feature), and have that state be
195 // visible for 3 seconds before calling dismissCallScreen().
196 }
197
198 /**
199 * Queries the System to determine what packages contain services that can handle the instant
200 * text response Action AND have permissions to do so.
201 */
202 private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
203 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
204
205 final ArrayList<ComponentName> componentsWithPermission = new ArrayList<ComponentName>();
206
207 // Get list of all services set up to handle the Instant Text intent.
208 final List<ResolveInfo> infos = packageManager.queryIntentServices(
209 getInstantTextIntent("", null, null), 0);
210
211 // Collect all the valid services
212 for (ResolveInfo resolveInfo : infos) {
213 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
214 if (serviceInfo == null) {
215 Log.w(TAG, "Ignore package without proper service.");
216 continue;
217 }
218
219 // A Service is valid only if it requires the permission
220 // PERMISSION_SEND_RESPOND_VIA_MESSAGE
221 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
222 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
223 serviceInfo.name));
224 }
225 }
226
227 return componentsWithPermission;
228 }
229
230 /**
231 * @param phoneNumber Must not be null.
232 * @param message Can be null. If message is null, the returned Intent will be configured to
233 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
234 * to be sent with no interaction from the user.
235 * @param component The component that should handle this intent.
236 * @return Service Intent for the instant response.
237 */
238 private static Intent getInstantTextIntent(String phoneNumber, String message,
239 ComponentName component) {
240 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
241 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
242 if (message != null) {
243 intent.putExtra(Intent.EXTRA_TEXT, message);
244 } else {
245 intent.putExtra("exit_on_sent", true);
246 intent.putExtra("showUI", true);
247 }
248 if (component != null) {
249 intent.setComponent(component);
250 }
251 return intent;
252 }
253
254 public void rejectCallWithNewMessage(Call call) {
255 launchSmsCompose(call.getLatestConnection().getAddress());
256 }
257
258 private ComponentName getSmsService() {
259 if (DBG) log("sendTextToDefaultActivity()...");
260 final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
261
262 // Check to see if the default component to receive this intent is already saved
263 // and check to see if it still has the corrent permissions.
264 final SharedPreferences prefs = PhoneGlobals.getInstance().
265 getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
266 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
267 if (flattenedName != null) {
268 if (DBG) log("Default package was found." + flattenedName);
269
270 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
271 ServiceInfo serviceInfo = null;
272 try {
273 serviceInfo = packageManager.getServiceInfo(componentName, 0);
274 } catch (PackageManager.NameNotFoundException e) {
275 Log.w(TAG, "Default service does not have permission.");
276 }
277
278 if (serviceInfo != null &&
279 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
280 return componentName;
281 } else {
282 SharedPreferences.Editor editor = prefs.edit();
283 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
284 editor.apply();
285 }
286 }
287
288 final ArrayList<ComponentName> componentsWithPermission =
289 getPackagesWithInstantTextPermission();
290
291 final int size = componentsWithPermission.size();
292 if (size == 0) {
293 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
294 return null;
295 } else if (size == 1) {
296 return componentsWithPermission.get(0);
297 } else {
298 Log.v(TAG, "Choosing from one of the apps");
299 // TODO(klp): Add an app picker.
300 return componentsWithPermission.get(0);
301 }
302 }
303
304
305 public void rejectCallWithMessage(Call call, String message) {
306 final ComponentName componentName = getSmsService();
307
308 if (componentName != null) {
309 sendTextAndExit(call.getLatestConnection().getAddress(), message, componentName,
310 false);
311 }
312 }
313
314 private static void log(String msg) {
315 Log.d(TAG, msg);
316 }
317}