blob: c851471f62e6befff5a691b053a5f200dc1ed5e1 [file] [log] [blame]
Santos Cordon7d4ddf62013-07-10 11:58:08 -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.ActionBar;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.content.pm.ApplicationInfo;
29import android.content.pm.PackageInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManager;
32import android.content.pm.ResolveInfo;
33import android.content.pm.ServiceInfo;
34import android.content.res.Resources;
35import android.graphics.drawable.Drawable;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.SystemProperties;
39import android.preference.EditTextPreference;
40import android.preference.Preference;
41import android.preference.PreferenceActivity;
42import android.telephony.PhoneNumberUtils;
43import android.telephony.TelephonyManager;
44import android.text.TextUtils;
45import android.util.Log;
46import android.view.LayoutInflater;
47import android.view.Menu;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.ViewGroup;
51import android.widget.AdapterView;
52import android.widget.ArrayAdapter;
53import android.widget.BaseAdapter;
54import android.widget.CheckBox;
55import android.widget.CompoundButton;
56import android.widget.ImageView;
57import android.widget.ListView;
58import android.widget.TextView;
59import android.widget.Toast;
60
61import com.android.internal.telephony.Call;
62import com.android.internal.telephony.Connection;
63import com.android.internal.telephony.PhoneConstants;
64import com.google.android.collect.Lists;
65
66import java.util.ArrayList;
67import java.util.Arrays;
68import java.util.List;
69
70/**
71 * Helper class to manage the "Respond via Message" feature for incoming calls.
72 *
73 * @see InCallScreen.internalRespondViaSms()
74 */
75public class RespondViaSmsManager {
76 private static final String TAG = "RespondViaSmsManager";
77 private static final boolean DBG =
78 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
79 // Do not check in with VDBG = true, since that may write PII to the system log.
80 private static final boolean VDBG = false;
81
82 private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
83 "android.permission.SEND_RESPOND_VIA_MESSAGE";
84
85 private int mIconSize = -1;
86
87 /**
88 * Reference to the InCallScreen activity that owns us. This may be
89 * null if we haven't been initialized yet *or* after the InCallScreen
90 * activity has been destroyed.
91 */
92 private InCallScreen mInCallScreen;
93
94 /**
95 * The popup showing the list of canned responses.
96 *
97 * This is an AlertDialog containing a ListView showing the possible
98 * choices. This may be null if the InCallScreen hasn't ever called
99 * showRespondViaSmsPopup() yet, or if the popup was visible once but
100 * then got dismissed.
101 */
102 private Dialog mCannedResponsePopup;
103
104 /**
105 * The popup dialog allowing the user to chose which app handles respond-via-sms.
106 *
107 * An AlertDialog showing the Resolve-App UI resource from the framework wchih we then fill in
108 * with the appropriate data set. Can be null when not visible.
109 */
110 private Dialog mPackageSelectionPopup;
111
112 /** The array of "canned responses"; see loadCannedResponses(). */
113 private String[] mCannedResponses;
114
115 /** SharedPreferences file name for our persistent settings. */
116 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
117
118 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
119 // Since (for now at least) the number of messages is fixed at 4, and since
120 // SharedPreferences can't deal with arrays anyway, just store the messages
121 // as 4 separate strings.
122 private static final int NUM_CANNED_RESPONSES = 4;
123 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
124 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
125 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
126 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
127 private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
128 private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
129
130 /**
131 * RespondViaSmsManager constructor.
132 */
133 public RespondViaSmsManager() {
134 }
135
136 public void setInCallScreenInstance(InCallScreen inCallScreen) {
137 mInCallScreen = inCallScreen;
138
139 if (mInCallScreen != null) {
140 // Prefetch shared preferences to make the first canned response lookup faster
141 // (and to prevent StrictMode violation)
142 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
143 }
144 }
145
146 /**
147 * Brings up the "Respond via SMS" popup for an incoming call.
148 *
149 * @param ringingCall the current incoming call
150 */
151 public void showRespondViaSmsPopup(Call ringingCall) {
152 if (DBG) log("showRespondViaSmsPopup()...");
153
154 // Very quick succession of clicks can cause this to run twice.
155 // Stop here to avoid creating more than one popup.
156 if (isShowingPopup()) {
157 if (DBG) log("Skip showing popup when one is already shown.");
158 return;
159 }
160
161 ListView lv = new ListView(mInCallScreen);
162
163 // Refresh the array of "canned responses".
164 mCannedResponses = loadCannedResponses();
165
166 // Build the list: start with the canned responses, but manually add
167 // the write-your-own option as the last choice.
168 int numPopupItems = mCannedResponses.length + 1;
169 String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems);
170 popupItems[numPopupItems - 1] = mInCallScreen.getResources()
171 .getString(R.string.respond_via_sms_custom_message);
172
173 ArrayAdapter<String> adapter =
174 new ArrayAdapter<String>(mInCallScreen,
175 android.R.layout.simple_list_item_1,
176 android.R.id.text1,
177 popupItems);
178 lv.setAdapter(adapter);
179
180 // Create a RespondViaSmsItemClickListener instance to handle item
181 // clicks from the popup.
182 // (Note we create a fresh instance for each incoming call, and
183 // stash away the call's phone number, since we can't necessarily
184 // assume this call will still be ringing when the user finally
185 // chooses a response.)
186
187 Connection c = ringingCall.getLatestConnection();
188 if (VDBG) log("- connection: " + c);
189
190 if (c == null) {
191 // Uh oh -- the "ringingCall" doesn't have any connections any more.
192 // (In other words, it's no longer ringing.) This is rare, but can
193 // happen if the caller hangs up right at the exact moment the user
194 // selects the "Respond via SMS" option.
195 // There's nothing to do here (since the incoming call is gone),
196 // so just bail out.
197 Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out...");
198 return;
199 }
200
201 // TODO: at this point we probably should re-check c.getAddress()
202 // and c.getNumberPresentation() for validity. (i.e. recheck the
203 // same cases in InCallTouchUi.showIncomingCallWidget() where we
204 // should have disallowed the "respond via SMS" feature in the
205 // first place.)
206
207 String phoneNumber = c.getAddress();
208 if (VDBG) log("- phoneNumber: " + phoneNumber);
209 lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber));
210
211 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
212 .setCancelable(true)
213 .setOnCancelListener(new RespondViaSmsCancelListener())
214 .setView(lv);
215 mCannedResponsePopup = builder.create();
216 mCannedResponsePopup.show();
217 }
218
219 /**
220 * Dismiss currently visible popups.
221 *
222 * This is safe to call even if the popup is already dismissed, and
223 * even if you never called showRespondViaSmsPopup() in the first
224 * place.
225 */
226 public void dismissPopup() {
227 if (mCannedResponsePopup != null) {
228 mCannedResponsePopup.dismiss(); // safe even if already dismissed
229 mCannedResponsePopup = null;
230 }
231 if (mPackageSelectionPopup != null) {
232 mPackageSelectionPopup.dismiss();
233 mPackageSelectionPopup = null;
234 }
235 }
236
237 public boolean isShowingPopup() {
238 return (mCannedResponsePopup != null && mCannedResponsePopup.isShowing())
239 || (mPackageSelectionPopup != null && mPackageSelectionPopup.isShowing());
240 }
241
242 /**
243 * OnItemClickListener for the "Respond via SMS" popup.
244 */
245 public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
246 // Phone number to send the SMS to.
247 private String mPhoneNumber;
248
249 public RespondViaSmsItemClickListener(String phoneNumber) {
250 mPhoneNumber = phoneNumber;
251 }
252
253 /**
254 * Handles the user selecting an item from the popup.
255 */
256 @Override
257 public void onItemClick(AdapterView<?> parent, // The ListView
258 View view, // The TextView that was clicked
259 int position,
260 long id) {
261 if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
262 String message = (String) parent.getItemAtPosition(position);
263 if (VDBG) log("- message: '" + message + "'");
264
265 // The "Custom" choice is a special case.
266 // (For now, it's guaranteed to be the last item.)
267 if (position == (parent.getCount() - 1)) {
268 // Take the user to the standard SMS compose UI.
269 launchSmsCompose(mPhoneNumber);
270 onPostMessageSent();
271 } else {
272 sendTextToDefaultActivity(mPhoneNumber, message);
273 }
274 }
275 }
276
277
278 /**
279 * OnCancelListener for the "Respond via SMS" popup.
280 */
281 public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener {
282 public RespondViaSmsCancelListener() {
283 }
284
285 /**
286 * Handles the user canceling the popup, either by touching
287 * outside the popup or by pressing Back.
288 */
289 @Override
290 public void onCancel(DialogInterface dialog) {
291 if (DBG) log("RespondViaSmsCancelListener.onCancel()...");
292
293 dismissPopup();
294
295 final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
296 if (state == PhoneConstants.State.IDLE) {
297 // This means the incoming call is already hung up when the user chooses not to
298 // use "Respond via SMS" feature. Let's just exit the whole in-call screen.
299 PhoneGlobals.getInstance().dismissCallScreen();
300 } else {
301
302 // If the user cancels the popup, this presumably means that
303 // they didn't actually mean to bring up the "Respond via SMS"
304 // UI in the first place (and instead want to go back to the
305 // state where they can either answer or reject the call.)
306 // So restart the ringer and bring back the regular incoming
307 // call UI.
308
309 // This will have no effect if the incoming call isn't still ringing.
310 PhoneGlobals.getInstance().notifier.restartRinger();
311
312 // We hid the GlowPadView widget way back in
313 // InCallTouchUi.onTrigger(), when the user first selected
314 // the "SMS" trigger.
315 //
316 // To bring it back, just force the entire InCallScreen to
317 // update itself based on the current telephony state.
318 // (Assuming the incoming call is still ringing, this will
319 // cause the incoming call widget to reappear.)
320 mInCallScreen.requestUpdateScreen();
321 }
322 }
323 }
324
325 private void sendTextToDefaultActivity(String phoneNumber, String message) {
326 if (DBG) log("sendTextToDefaultActivity()...");
327 final PackageManager packageManager = mInCallScreen.getPackageManager();
328
329 // Check to see if the default component to receive this intent is already saved
330 // and check to see if it still has the corrent permissions.
331 final SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
332 Context.MODE_PRIVATE);
333 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
334 if (flattenedName != null) {
335 if (DBG) log("Default package was found." + flattenedName);
336
337 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
338 ServiceInfo serviceInfo = null;
339 try {
340 serviceInfo = packageManager.getServiceInfo(componentName, 0);
341 } catch (PackageManager.NameNotFoundException e) {
342 Log.w(TAG, "Default service does not have permission.");
343 }
344
345 if (serviceInfo != null &&
346 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
347 sendTextAndExit(phoneNumber, message, componentName, false);
348 return;
349 } else {
350 SharedPreferences.Editor editor = prefs.edit();
351 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
352 editor.apply();
353 }
354 }
355
356 final ArrayList<ComponentName> componentsWithPermission =
357 getPackagesWithInstantTextPermission();
358
359 final int size = componentsWithPermission.size();
360 if (size == 0) {
361 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
362 onPostMessageSent();
363 } else if (size == 1) {
364 sendTextAndExit(phoneNumber, message, componentsWithPermission.get(0), false);
365 } else {
366 showPackageSelectionDialog(phoneNumber, message, componentsWithPermission);
367 }
368 }
369
370 /**
371 * Queries the System to determine what packages contain services that can handle the instant
372 * text response Action AND have permissions to do so.
373 */
374 private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
375 PackageManager packageManager = mInCallScreen.getPackageManager();
376
377 ArrayList<ComponentName> componentsWithPermission = Lists.newArrayList();
378
379 // Get list of all services set up to handle the Instant Text intent.
380 final List<ResolveInfo> infos = packageManager.queryIntentServices(
381 getInstantTextIntent("", null, null), 0);
382
383 // Collect all the valid services
384 for (ResolveInfo resolveInfo : infos) {
385 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
386 if (serviceInfo == null) {
387 Log.w(TAG, "Ignore package without proper service.");
388 continue;
389 }
390
391 // A Service is valid only if it requires the permission
392 // PERMISSION_SEND_RESPOND_VIA_MESSAGE
393 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
394 componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
395 serviceInfo.name));
396 }
397 }
398
399 return componentsWithPermission;
400 }
401
402 private void showPackageSelectionDialog(String phoneNumber, String message,
403 List<ComponentName> components) {
404 if (DBG) log("showPackageSelectionDialog()...");
405
406 dismissPopup();
407
408 BaseAdapter adapter = new PackageSelectionAdapter(mInCallScreen, components);
409
410 PackageClickListener clickListener =
411 new PackageClickListener(phoneNumber, message, components);
412
413 final CharSequence title = mInCallScreen.getResources().getText(
414 com.android.internal.R.string.whichApplication);
415 LayoutInflater inflater =
416 (LayoutInflater) mInCallScreen.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
417
418 final View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
419 final CheckBox alwaysUse = (CheckBox) view.findViewById(
420 com.android.internal.R.id.alwaysUse);
421 alwaysUse.setText(com.android.internal.R.string.alwaysUse);
422 alwaysUse.setOnCheckedChangeListener(clickListener);
423
424 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
425 .setTitle(title)
426 .setCancelable(true)
427 .setOnCancelListener(new RespondViaSmsCancelListener())
428 .setAdapter(adapter, clickListener)
429 .setView(view);
430 mPackageSelectionPopup = builder.create();
431 mPackageSelectionPopup.show();
432 }
433
434 private class PackageSelectionAdapter extends BaseAdapter {
435 private final LayoutInflater mInflater;
436 private final List<ComponentName> mComponents;
437
438 public PackageSelectionAdapter(Context context, List<ComponentName> components) {
439 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
440 mComponents = components;
441 }
442
443 @Override
444 public int getCount() {
445 return mComponents.size();
446 }
447
448 @Override
449 public Object getItem(int position) {
450 return mComponents.get(position);
451 }
452
453 @Override
454 public long getItemId(int position) {
455 return position;
456 }
457
458 @Override
459 public View getView(int position, View convertView, ViewGroup parent) {
460 if (convertView == null) {
461 convertView = mInflater.inflate(
462 com.android.internal.R.layout.resolve_list_item, parent, false);
463 }
464
465 final ComponentName component = mComponents.get(position);
466 final String packageName = component.getPackageName();
467 final PackageManager packageManager = mInCallScreen.getPackageManager();
468
469 // Set the application label
470 final TextView text = (TextView) convertView.findViewById(
471 com.android.internal.R.id.text1);
472 final TextView text2 = (TextView) convertView.findViewById(
473 com.android.internal.R.id.text2);
474
475 // Reset any previous values
476 text.setText("");
477 text2.setVisibility(View.GONE);
478 try {
479 final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
480 final CharSequence label = packageManager.getApplicationLabel(appInfo);
481 if (label != null) {
482 text.setText(label);
483 }
484 } catch (PackageManager.NameNotFoundException e) {
485 Log.w(TAG, "Failed to load app label because package was not found.");
486 }
487
488 // Set the application icon
489 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
490 Drawable drawable = null;
491 try {
492 drawable = mInCallScreen.getPackageManager().getApplicationIcon(packageName);
493 } catch (PackageManager.NameNotFoundException e) {
494 Log.w(TAG, "Failed to load icon because it wasn't found.");
495 }
496 if (drawable == null) {
497 drawable = mInCallScreen.getPackageManager().getDefaultActivityIcon();
498 }
499 icon.setImageDrawable(drawable);
500 ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) icon.getLayoutParams();
501 lp.width = lp.height = getIconSize();
502
503 return convertView;
504 }
505
506 }
507
508 private class PackageClickListener implements DialogInterface.OnClickListener,
509 CompoundButton.OnCheckedChangeListener {
510 /** Phone number to send the SMS to. */
511 final private String mPhoneNumber;
512 final private String mMessage;
513 final private List<ComponentName> mComponents;
514 private boolean mMakeDefault = false;
515
516 public PackageClickListener(String phoneNumber, String message,
517 List<ComponentName> components) {
518 mPhoneNumber = phoneNumber;
519 mMessage = message;
520 mComponents = components;
521 }
522
523 @Override
524 public void onClick(DialogInterface dialog, int which) {
525 ComponentName component = mComponents.get(which);
526 sendTextAndExit(mPhoneNumber, mMessage, component, mMakeDefault);
527 }
528
529 @Override
530 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
531 Log.i(TAG, "mMakeDefault : " + isChecked);
532 mMakeDefault = isChecked;
533 }
534 }
535
536 private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
537 boolean setDefaultComponent) {
538 // Send the selected message immediately with no user interaction.
539 sendText(phoneNumber, message, component);
540
541 if (setDefaultComponent) {
542 final SharedPreferences prefs = mInCallScreen.getSharedPreferences(
543 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
544 prefs.edit()
545 .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
546 .apply();
547 }
548
549 // ...and show a brief confirmation to the user (since
550 // otherwise it's hard to be sure that anything actually
551 // happened.)
552 final Resources res = mInCallScreen.getResources();
553 final String formatString = res.getString(R.string.respond_via_sms_confirmation_format);
554 final String confirmationMsg = String.format(formatString, phoneNumber);
555 Toast.makeText(mInCallScreen,
556 confirmationMsg,
557 Toast.LENGTH_LONG).show();
558
559 // TODO: If the device is locked, this toast won't actually ever
560 // be visible! (That's because we're about to dismiss the call
561 // screen, which means that the device will return to the
562 // keyguard. But toasts aren't visible on top of the keyguard.)
563 // Possible fixes:
564 // (1) Is it possible to allow a specific Toast to be visible
565 // on top of the keyguard?
566 // (2) Artifically delay the dismissCallScreen() call by 3
567 // seconds to allow the toast to be seen?
568 // (3) Don't use a toast at all; instead use a transient state
569 // of the InCallScreen (perhaps via the InCallUiState
570 // progressIndication feature), and have that state be
571 // visible for 3 seconds before calling dismissCallScreen().
572
573 onPostMessageSent();
574 }
575
576 /**
577 * Sends a text message without any interaction from the user.
578 */
579 private void sendText(String phoneNumber, String message, ComponentName component) {
580 if (VDBG) log("sendText: number "
581 + phoneNumber + ", message '" + message + "'");
582
583 mInCallScreen.startService(getInstantTextIntent(phoneNumber, message, component));
584 }
585
586 private void onPostMessageSent() {
587 // At this point the user is done dealing with the incoming call, so
588 // there's no reason to keep it around. (It's also confusing for
589 // the "incoming call" icon in the status bar to still be visible.)
590 // So reject the call now.
591 mInCallScreen.hangupRingingCall();
592
593 dismissPopup();
594
595 final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
596 if (state == PhoneConstants.State.IDLE) {
597 // There's no other phone call to interact. Exit the entire in-call screen.
598 PhoneGlobals.getInstance().dismissCallScreen();
599 } else {
600 // The user is still in the middle of other phone calls, so we should keep the
601 // in-call screen.
602 mInCallScreen.requestUpdateScreen();
603 }
604 }
605
606 /**
607 * Brings up the standard SMS compose UI.
608 */
609 private void launchSmsCompose(String phoneNumber) {
610 if (VDBG) log("launchSmsCompose: number " + phoneNumber);
611
612 Intent intent = getInstantTextIntent(phoneNumber, null, null);
613
614 if (VDBG) log("- Launching SMS compose UI: " + intent);
615 mInCallScreen.startService(intent);
616 }
617
618 /**
619 * @param phoneNumber Must not be null.
620 * @param message Can be null. If message is null, the returned Intent will be configured to
621 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
622 * to be sent with no interaction from the user.
623 * @param component The component that should handle this intent.
624 * @return Service Intent for the instant response.
625 */
626 private static Intent getInstantTextIntent(String phoneNumber, String message,
627 ComponentName component) {
628 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
629 Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
630 if (message != null) {
631 intent.putExtra(Intent.EXTRA_TEXT, message);
632 } else {
633 intent.putExtra("exit_on_sent", true);
634 intent.putExtra("showUI", true);
635 }
636 if (component != null) {
637 intent.setComponent(component);
638 }
639 return intent;
640 }
641
642 /**
643 * Settings activity under "Call settings" to let you manage the
644 * canned responses; see respond_via_sms_settings.xml
645 */
646 public static class Settings extends PreferenceActivity
647 implements Preference.OnPreferenceChangeListener {
648 @Override
649 protected void onCreate(Bundle icicle) {
650 super.onCreate(icicle);
651 if (DBG) log("Settings: onCreate()...");
652
653 getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
654
655 // This preference screen is ultra-simple; it's just 4 plain
656 // <EditTextPreference>s, one for each of the 4 "canned responses".
657 //
658 // The only nontrivial thing we do here is copy the text value of
659 // each of those EditTextPreferences and use it as the preference's
660 // "title" as well, so that the user will immediately see all 4
661 // strings when they arrive here.
662 //
663 // Also, listen for change events (since we'll need to update the
664 // title any time the user edits one of the strings.)
665
666 addPreferencesFromResource(R.xml.respond_via_sms_settings);
667
668 EditTextPreference pref;
669 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
670 pref.setTitle(pref.getText());
671 pref.setOnPreferenceChangeListener(this);
672
673 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
674 pref.setTitle(pref.getText());
675 pref.setOnPreferenceChangeListener(this);
676
677 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
678 pref.setTitle(pref.getText());
679 pref.setOnPreferenceChangeListener(this);
680
681 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
682 pref.setTitle(pref.getText());
683 pref.setOnPreferenceChangeListener(this);
684
685 ActionBar actionBar = getActionBar();
686 if (actionBar != null) {
687 // android.R.id.home will be triggered in onOptionsItemSelected()
688 actionBar.setDisplayHomeAsUpEnabled(true);
689 }
690 }
691
692 // Preference.OnPreferenceChangeListener implementation
693 @Override
694 public boolean onPreferenceChange(Preference preference, Object newValue) {
695 if (DBG) log("onPreferenceChange: key = " + preference.getKey());
696 if (VDBG) log(" preference = '" + preference + "'");
697 if (VDBG) log(" newValue = '" + newValue + "'");
698
699 EditTextPreference pref = (EditTextPreference) preference;
700
701 // Copy the new text over to the title, just like in onCreate().
702 // (Watch out: onPreferenceChange() is called *before* the
703 // Preference itself gets updated, so we need to use newValue here
704 // rather than pref.getText().)
705 pref.setTitle((String) newValue);
706
707 return true; // means it's OK to update the state of the Preference with the new value
708 }
709
710 @Override
711 public boolean onOptionsItemSelected(MenuItem item) {
712 final int itemId = item.getItemId();
713 switch (itemId) {
714 case android.R.id.home:
715 // See ActionBar#setDisplayHomeAsUpEnabled()
716 CallFeaturesSetting.goUpToTopLevelSetting(this);
717 return true;
718 case R.id.respond_via_message_reset:
719 // Reset the preferences settings
720 SharedPreferences prefs = getSharedPreferences(
721 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
722 SharedPreferences.Editor editor = prefs.edit();
723 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
724 editor.apply();
725
726 return true;
727 default:
728 }
729 return super.onOptionsItemSelected(item);
730 }
731
732 @Override
733 public boolean onCreateOptionsMenu(Menu menu) {
734 getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
735 return super.onCreateOptionsMenu(menu);
736 }
737 }
738
739 /**
740 * Read the (customizable) canned responses from SharedPreferences,
741 * or from defaults if the user has never actually brought up
742 * the Settings UI.
743 *
744 * This method does disk I/O (reading the SharedPreferences file)
745 * so don't call it from the main thread.
746 *
747 * @see RespondViaSmsManager.Settings
748 */
749 private String[] loadCannedResponses() {
750 if (DBG) log("loadCannedResponses()...");
751
752 SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
753 Context.MODE_PRIVATE);
754 final Resources res = mInCallScreen.getResources();
755
756 String[] responses = new String[NUM_CANNED_RESPONSES];
757
758 // Note the default values here must agree with the corresponding
759 // android:defaultValue attributes in respond_via_sms_settings.xml.
760
761 responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
762 res.getString(R.string.respond_via_sms_canned_response_1));
763 responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
764 res.getString(R.string.respond_via_sms_canned_response_2));
765 responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
766 res.getString(R.string.respond_via_sms_canned_response_3));
767 responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
768 res.getString(R.string.respond_via_sms_canned_response_4));
769 return responses;
770 }
771
772 /**
773 * @return true if the "Respond via SMS" feature should be enabled
774 * for the specified incoming call.
775 *
776 * The general rule is that we *do* allow "Respond via SMS" except for
777 * the few (relatively rare) cases where we know for sure it won't
778 * work, namely:
779 * - a bogus or blank incoming number
780 * - a call from a SIP address
781 * - a "call presentation" that doesn't allow the number to be revealed
782 *
783 * In all other cases, we allow the user to respond via SMS.
784 *
785 * Note that this behavior isn't perfect; for example we have no way
786 * to detect whether the incoming call is from a landline (with most
787 * networks at least), so we still enable this feature even though
788 * SMSes to that number will silently fail.
789 */
790 public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
791 if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")...");
792
793 // First some basic sanity checks:
794 if (ringingCall == null) {
795 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
796 return false;
797 }
798 if (!ringingCall.isRinging()) {
799 // The call is in some state other than INCOMING or WAITING!
800 // (This should almost never happen, but it *could*
801 // conceivably happen if the ringing call got disconnected by
802 // the network just *after* we got it from the CallManager.)
803 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
804 + ringingCall.getState());
805 return false;
806 }
807 Connection conn = ringingCall.getLatestConnection();
808 if (conn == null) {
809 // The call doesn't have any connections! (Again, this can
810 // happen if the ringing call disconnects at the exact right
811 // moment, but should almost never happen in practice.)
812 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
813 return false;
814 }
815
816 // Check the incoming number:
817 final String number = conn.getAddress();
818 if (DBG) log("- number: '" + number + "'");
819 if (TextUtils.isEmpty(number)) {
820 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
821 return false;
822 }
823 if (PhoneNumberUtils.isUriNumber(number)) {
824 // The incoming number is actually a URI (i.e. a SIP address),
825 // not a regular PSTN phone number, and we can't send SMSes to
826 // SIP addresses.
827 // (TODO: That might still be possible eventually, though. Is
828 // there some SIP-specific equivalent to sending a text message?)
829 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
830 return false;
831 }
832
833 // Finally, check the "call presentation":
834 int presentation = conn.getNumberPresentation();
835 if (DBG) log("- presentation: " + presentation);
836 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
837 // PRESENTATION_RESTRICTED means "caller-id blocked".
838 // The user isn't allowed to see the number in the first
839 // place, so obviously we can't let you send an SMS to it.
840 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
841 return false;
842 }
843
844 // Allow the feature only when there's a destination for it.
845 if (context.getPackageManager().resolveService(getInstantTextIntent(number, null, null) , 0)
846 == null) {
847 return false;
848 }
849
850 // TODO: with some carriers (in certain countries) you *can* actually
851 // tell whether a given number is a mobile phone or not. So in that
852 // case we could potentially return false here if the incoming call is
853 // from a land line.
854
855 // If none of the above special cases apply, it's OK to enable the
856 // "Respond via SMS" feature.
857 return true;
858 }
859
860 private int getIconSize() {
861 if (mIconSize < 0) {
862 final ActivityManager am =
863 (ActivityManager) mInCallScreen.getSystemService(Context.ACTIVITY_SERVICE);
864 mIconSize = am.getLauncherLargeIconSize();
865 }
866
867 return mIconSize;
868 }
869
870
871 private static void log(String msg) {
872 Log.d(TAG, msg);
873 }
874}