Blanket copy of PhoneApp to services/Telephony.
First phase of splitting out InCallUI from PhoneApp.
Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/RespondViaSmsManager.java b/src/com/android/phone/RespondViaSmsManager.java
new file mode 100644
index 0000000..c851471
--- /dev/null
+++ b/src/com/android/phone/RespondViaSmsManager.java
@@ -0,0 +1,874 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone;
+
+import android.app.ActivityManager;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.PhoneConstants;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ *
+ * @see InCallScreen.internalRespondViaSms()
+ */
+public class RespondViaSmsManager {
+ private static final String TAG = "RespondViaSmsManager";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
+ "android.permission.SEND_RESPOND_VIA_MESSAGE";
+
+ private int mIconSize = -1;
+
+ /**
+ * Reference to the InCallScreen activity that owns us. This may be
+ * null if we haven't been initialized yet *or* after the InCallScreen
+ * activity has been destroyed.
+ */
+ private InCallScreen mInCallScreen;
+
+ /**
+ * The popup showing the list of canned responses.
+ *
+ * This is an AlertDialog containing a ListView showing the possible
+ * choices. This may be null if the InCallScreen hasn't ever called
+ * showRespondViaSmsPopup() yet, or if the popup was visible once but
+ * then got dismissed.
+ */
+ private Dialog mCannedResponsePopup;
+
+ /**
+ * The popup dialog allowing the user to chose which app handles respond-via-sms.
+ *
+ * An AlertDialog showing the Resolve-App UI resource from the framework wchih we then fill in
+ * with the appropriate data set. Can be null when not visible.
+ */
+ private Dialog mPackageSelectionPopup;
+
+ /** The array of "canned responses"; see loadCannedResponses(). */
+ private String[] mCannedResponses;
+
+ /** SharedPreferences file name for our persistent settings. */
+ private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
+
+ // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
+ // Since (for now at least) the number of messages is fixed at 4, and since
+ // SharedPreferences can't deal with arrays anyway, just store the messages
+ // as 4 separate strings.
+ private static final int NUM_CANNED_RESPONSES = 4;
+ private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
+ private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
+ private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
+ private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
+ private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
+ private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
+
+ /**
+ * RespondViaSmsManager constructor.
+ */
+ public RespondViaSmsManager() {
+ }
+
+ public void setInCallScreenInstance(InCallScreen inCallScreen) {
+ mInCallScreen = inCallScreen;
+
+ if (mInCallScreen != null) {
+ // Prefetch shared preferences to make the first canned response lookup faster
+ // (and to prevent StrictMode violation)
+ mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ }
+ }
+
+ /**
+ * Brings up the "Respond via SMS" popup for an incoming call.
+ *
+ * @param ringingCall the current incoming call
+ */
+ public void showRespondViaSmsPopup(Call ringingCall) {
+ if (DBG) log("showRespondViaSmsPopup()...");
+
+ // Very quick succession of clicks can cause this to run twice.
+ // Stop here to avoid creating more than one popup.
+ if (isShowingPopup()) {
+ if (DBG) log("Skip showing popup when one is already shown.");
+ return;
+ }
+
+ ListView lv = new ListView(mInCallScreen);
+
+ // Refresh the array of "canned responses".
+ mCannedResponses = loadCannedResponses();
+
+ // Build the list: start with the canned responses, but manually add
+ // the write-your-own option as the last choice.
+ int numPopupItems = mCannedResponses.length + 1;
+ String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems);
+ popupItems[numPopupItems - 1] = mInCallScreen.getResources()
+ .getString(R.string.respond_via_sms_custom_message);
+
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(mInCallScreen,
+ android.R.layout.simple_list_item_1,
+ android.R.id.text1,
+ popupItems);
+ lv.setAdapter(adapter);
+
+ // Create a RespondViaSmsItemClickListener instance to handle item
+ // clicks from the popup.
+ // (Note we create a fresh instance for each incoming call, and
+ // stash away the call's phone number, since we can't necessarily
+ // assume this call will still be ringing when the user finally
+ // chooses a response.)
+
+ Connection c = ringingCall.getLatestConnection();
+ if (VDBG) log("- connection: " + c);
+
+ if (c == null) {
+ // Uh oh -- the "ringingCall" doesn't have any connections any more.
+ // (In other words, it's no longer ringing.) This is rare, but can
+ // happen if the caller hangs up right at the exact moment the user
+ // selects the "Respond via SMS" option.
+ // There's nothing to do here (since the incoming call is gone),
+ // so just bail out.
+ Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out...");
+ return;
+ }
+
+ // TODO: at this point we probably should re-check c.getAddress()
+ // and c.getNumberPresentation() for validity. (i.e. recheck the
+ // same cases in InCallTouchUi.showIncomingCallWidget() where we
+ // should have disallowed the "respond via SMS" feature in the
+ // first place.)
+
+ String phoneNumber = c.getAddress();
+ if (VDBG) log("- phoneNumber: " + phoneNumber);
+ lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
+ .setCancelable(true)
+ .setOnCancelListener(new RespondViaSmsCancelListener())
+ .setView(lv);
+ mCannedResponsePopup = builder.create();
+ mCannedResponsePopup.show();
+ }
+
+ /**
+ * Dismiss currently visible popups.
+ *
+ * This is safe to call even if the popup is already dismissed, and
+ * even if you never called showRespondViaSmsPopup() in the first
+ * place.
+ */
+ public void dismissPopup() {
+ if (mCannedResponsePopup != null) {
+ mCannedResponsePopup.dismiss(); // safe even if already dismissed
+ mCannedResponsePopup = null;
+ }
+ if (mPackageSelectionPopup != null) {
+ mPackageSelectionPopup.dismiss();
+ mPackageSelectionPopup = null;
+ }
+ }
+
+ public boolean isShowingPopup() {
+ return (mCannedResponsePopup != null && mCannedResponsePopup.isShowing())
+ || (mPackageSelectionPopup != null && mPackageSelectionPopup.isShowing());
+ }
+
+ /**
+ * OnItemClickListener for the "Respond via SMS" popup.
+ */
+ public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
+ // Phone number to send the SMS to.
+ private String mPhoneNumber;
+
+ public RespondViaSmsItemClickListener(String phoneNumber) {
+ mPhoneNumber = phoneNumber;
+ }
+
+ /**
+ * Handles the user selecting an item from the popup.
+ */
+ @Override
+ public void onItemClick(AdapterView<?> parent, // The ListView
+ View view, // The TextView that was clicked
+ int position,
+ long id) {
+ if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
+ String message = (String) parent.getItemAtPosition(position);
+ if (VDBG) log("- message: '" + message + "'");
+
+ // The "Custom" choice is a special case.
+ // (For now, it's guaranteed to be the last item.)
+ if (position == (parent.getCount() - 1)) {
+ // Take the user to the standard SMS compose UI.
+ launchSmsCompose(mPhoneNumber);
+ onPostMessageSent();
+ } else {
+ sendTextToDefaultActivity(mPhoneNumber, message);
+ }
+ }
+ }
+
+
+ /**
+ * OnCancelListener for the "Respond via SMS" popup.
+ */
+ public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener {
+ public RespondViaSmsCancelListener() {
+ }
+
+ /**
+ * Handles the user canceling the popup, either by touching
+ * outside the popup or by pressing Back.
+ */
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (DBG) log("RespondViaSmsCancelListener.onCancel()...");
+
+ dismissPopup();
+
+ final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
+ if (state == PhoneConstants.State.IDLE) {
+ // This means the incoming call is already hung up when the user chooses not to
+ // use "Respond via SMS" feature. Let's just exit the whole in-call screen.
+ PhoneGlobals.getInstance().dismissCallScreen();
+ } else {
+
+ // If the user cancels the popup, this presumably means that
+ // they didn't actually mean to bring up the "Respond via SMS"
+ // UI in the first place (and instead want to go back to the
+ // state where they can either answer or reject the call.)
+ // So restart the ringer and bring back the regular incoming
+ // call UI.
+
+ // This will have no effect if the incoming call isn't still ringing.
+ PhoneGlobals.getInstance().notifier.restartRinger();
+
+ // We hid the GlowPadView widget way back in
+ // InCallTouchUi.onTrigger(), when the user first selected
+ // the "SMS" trigger.
+ //
+ // To bring it back, just force the entire InCallScreen to
+ // update itself based on the current telephony state.
+ // (Assuming the incoming call is still ringing, this will
+ // cause the incoming call widget to reappear.)
+ mInCallScreen.requestUpdateScreen();
+ }
+ }
+ }
+
+ private void sendTextToDefaultActivity(String phoneNumber, String message) {
+ if (DBG) log("sendTextToDefaultActivity()...");
+ final PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ // Check to see if the default component to receive this intent is already saved
+ // and check to see if it still has the corrent permissions.
+ final SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
+ Context.MODE_PRIVATE);
+ final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
+ if (flattenedName != null) {
+ if (DBG) log("Default package was found." + flattenedName);
+
+ final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
+ ServiceInfo serviceInfo = null;
+ try {
+ serviceInfo = packageManager.getServiceInfo(componentName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Default service does not have permission.");
+ }
+
+ if (serviceInfo != null &&
+ PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+ sendTextAndExit(phoneNumber, message, componentName, false);
+ return;
+ } else {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+ editor.apply();
+ }
+ }
+
+ final ArrayList<ComponentName> componentsWithPermission =
+ getPackagesWithInstantTextPermission();
+
+ final int size = componentsWithPermission.size();
+ if (size == 0) {
+ Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
+ onPostMessageSent();
+ } else if (size == 1) {
+ sendTextAndExit(phoneNumber, message, componentsWithPermission.get(0), false);
+ } else {
+ showPackageSelectionDialog(phoneNumber, message, componentsWithPermission);
+ }
+ }
+
+ /**
+ * Queries the System to determine what packages contain services that can handle the instant
+ * text response Action AND have permissions to do so.
+ */
+ private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
+ PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ ArrayList<ComponentName> componentsWithPermission = Lists.newArrayList();
+
+ // Get list of all services set up to handle the Instant Text intent.
+ final List<ResolveInfo> infos = packageManager.queryIntentServices(
+ getInstantTextIntent("", null, null), 0);
+
+ // Collect all the valid services
+ for (ResolveInfo resolveInfo : infos) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ Log.w(TAG, "Ignore package without proper service.");
+ continue;
+ }
+
+ // A Service is valid only if it requires the permission
+ // PERMISSION_SEND_RESPOND_VIA_MESSAGE
+ if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+ componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
+ serviceInfo.name));
+ }
+ }
+
+ return componentsWithPermission;
+ }
+
+ private void showPackageSelectionDialog(String phoneNumber, String message,
+ List<ComponentName> components) {
+ if (DBG) log("showPackageSelectionDialog()...");
+
+ dismissPopup();
+
+ BaseAdapter adapter = new PackageSelectionAdapter(mInCallScreen, components);
+
+ PackageClickListener clickListener =
+ new PackageClickListener(phoneNumber, message, components);
+
+ final CharSequence title = mInCallScreen.getResources().getText(
+ com.android.internal.R.string.whichApplication);
+ LayoutInflater inflater =
+ (LayoutInflater) mInCallScreen.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
+ final CheckBox alwaysUse = (CheckBox) view.findViewById(
+ com.android.internal.R.id.alwaysUse);
+ alwaysUse.setText(com.android.internal.R.string.alwaysUse);
+ alwaysUse.setOnCheckedChangeListener(clickListener);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
+ .setTitle(title)
+ .setCancelable(true)
+ .setOnCancelListener(new RespondViaSmsCancelListener())
+ .setAdapter(adapter, clickListener)
+ .setView(view);
+ mPackageSelectionPopup = builder.create();
+ mPackageSelectionPopup.show();
+ }
+
+ private class PackageSelectionAdapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+ private final List<ComponentName> mComponents;
+
+ public PackageSelectionAdapter(Context context, List<ComponentName> components) {
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mComponents = components;
+ }
+
+ @Override
+ public int getCount() {
+ return mComponents.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mComponents.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(
+ com.android.internal.R.layout.resolve_list_item, parent, false);
+ }
+
+ final ComponentName component = mComponents.get(position);
+ final String packageName = component.getPackageName();
+ final PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ // Set the application label
+ final TextView text = (TextView) convertView.findViewById(
+ com.android.internal.R.id.text1);
+ final TextView text2 = (TextView) convertView.findViewById(
+ com.android.internal.R.id.text2);
+
+ // Reset any previous values
+ text.setText("");
+ text2.setVisibility(View.GONE);
+ try {
+ final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
+ final CharSequence label = packageManager.getApplicationLabel(appInfo);
+ if (label != null) {
+ text.setText(label);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to load app label because package was not found.");
+ }
+
+ // Set the application icon
+ final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+ Drawable drawable = null;
+ try {
+ drawable = mInCallScreen.getPackageManager().getApplicationIcon(packageName);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to load icon because it wasn't found.");
+ }
+ if (drawable == null) {
+ drawable = mInCallScreen.getPackageManager().getDefaultActivityIcon();
+ }
+ icon.setImageDrawable(drawable);
+ ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) icon.getLayoutParams();
+ lp.width = lp.height = getIconSize();
+
+ return convertView;
+ }
+
+ }
+
+ private class PackageClickListener implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+ /** Phone number to send the SMS to. */
+ final private String mPhoneNumber;
+ final private String mMessage;
+ final private List<ComponentName> mComponents;
+ private boolean mMakeDefault = false;
+
+ public PackageClickListener(String phoneNumber, String message,
+ List<ComponentName> components) {
+ mPhoneNumber = phoneNumber;
+ mMessage = message;
+ mComponents = components;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ComponentName component = mComponents.get(which);
+ sendTextAndExit(mPhoneNumber, mMessage, component, mMakeDefault);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Log.i(TAG, "mMakeDefault : " + isChecked);
+ mMakeDefault = isChecked;
+ }
+ }
+
+ private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
+ boolean setDefaultComponent) {
+ // Send the selected message immediately with no user interaction.
+ sendText(phoneNumber, message, component);
+
+ if (setDefaultComponent) {
+ final SharedPreferences prefs = mInCallScreen.getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ prefs.edit()
+ .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
+ .apply();
+ }
+
+ // ...and show a brief confirmation to the user (since
+ // otherwise it's hard to be sure that anything actually
+ // happened.)
+ final Resources res = mInCallScreen.getResources();
+ final String formatString = res.getString(R.string.respond_via_sms_confirmation_format);
+ final String confirmationMsg = String.format(formatString, phoneNumber);
+ Toast.makeText(mInCallScreen,
+ confirmationMsg,
+ Toast.LENGTH_LONG).show();
+
+ // TODO: If the device is locked, this toast won't actually ever
+ // be visible! (That's because we're about to dismiss the call
+ // screen, which means that the device will return to the
+ // keyguard. But toasts aren't visible on top of the keyguard.)
+ // Possible fixes:
+ // (1) Is it possible to allow a specific Toast to be visible
+ // on top of the keyguard?
+ // (2) Artifically delay the dismissCallScreen() call by 3
+ // seconds to allow the toast to be seen?
+ // (3) Don't use a toast at all; instead use a transient state
+ // of the InCallScreen (perhaps via the InCallUiState
+ // progressIndication feature), and have that state be
+ // visible for 3 seconds before calling dismissCallScreen().
+
+ onPostMessageSent();
+ }
+
+ /**
+ * Sends a text message without any interaction from the user.
+ */
+ private void sendText(String phoneNumber, String message, ComponentName component) {
+ if (VDBG) log("sendText: number "
+ + phoneNumber + ", message '" + message + "'");
+
+ mInCallScreen.startService(getInstantTextIntent(phoneNumber, message, component));
+ }
+
+ private void onPostMessageSent() {
+ // At this point the user is done dealing with the incoming call, so
+ // there's no reason to keep it around. (It's also confusing for
+ // the "incoming call" icon in the status bar to still be visible.)
+ // So reject the call now.
+ mInCallScreen.hangupRingingCall();
+
+ dismissPopup();
+
+ final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
+ if (state == PhoneConstants.State.IDLE) {
+ // There's no other phone call to interact. Exit the entire in-call screen.
+ PhoneGlobals.getInstance().dismissCallScreen();
+ } else {
+ // The user is still in the middle of other phone calls, so we should keep the
+ // in-call screen.
+ mInCallScreen.requestUpdateScreen();
+ }
+ }
+
+ /**
+ * Brings up the standard SMS compose UI.
+ */
+ private void launchSmsCompose(String phoneNumber) {
+ if (VDBG) log("launchSmsCompose: number " + phoneNumber);
+
+ Intent intent = getInstantTextIntent(phoneNumber, null, null);
+
+ if (VDBG) log("- Launching SMS compose UI: " + intent);
+ mInCallScreen.startService(intent);
+ }
+
+ /**
+ * @param phoneNumber Must not be null.
+ * @param message Can be null. If message is null, the returned Intent will be configured to
+ * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
+ * to be sent with no interaction from the user.
+ * @param component The component that should handle this intent.
+ * @return Service Intent for the instant response.
+ */
+ private static Intent getInstantTextIntent(String phoneNumber, String message,
+ ComponentName component) {
+ final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+ Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+ if (message != null) {
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ } else {
+ intent.putExtra("exit_on_sent", true);
+ intent.putExtra("showUI", true);
+ }
+ if (component != null) {
+ intent.setComponent(component);
+ }
+ return intent;
+ }
+
+ /**
+ * Settings activity under "Call settings" to let you manage the
+ * canned responses; see respond_via_sms_settings.xml
+ */
+ public static class Settings extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener {
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ if (DBG) log("Settings: onCreate()...");
+
+ getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
+
+ // This preference screen is ultra-simple; it's just 4 plain
+ // <EditTextPreference>s, one for each of the 4 "canned responses".
+ //
+ // The only nontrivial thing we do here is copy the text value of
+ // each of those EditTextPreferences and use it as the preference's
+ // "title" as well, so that the user will immediately see all 4
+ // strings when they arrive here.
+ //
+ // Also, listen for change events (since we'll need to update the
+ // title any time the user edits one of the strings.)
+
+ addPreferencesFromResource(R.xml.respond_via_sms_settings);
+
+ EditTextPreference pref;
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ // Preference.OnPreferenceChangeListener implementation
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (DBG) log("onPreferenceChange: key = " + preference.getKey());
+ if (VDBG) log(" preference = '" + preference + "'");
+ if (VDBG) log(" newValue = '" + newValue + "'");
+
+ EditTextPreference pref = (EditTextPreference) preference;
+
+ // Copy the new text over to the title, just like in onCreate().
+ // (Watch out: onPreferenceChange() is called *before* the
+ // Preference itself gets updated, so we need to use newValue here
+ // rather than pref.getText().)
+ pref.setTitle((String) newValue);
+
+ return true; // means it's OK to update the state of the Preference with the new value
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ switch (itemId) {
+ case android.R.id.home:
+ // See ActionBar#setDisplayHomeAsUpEnabled()
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ case R.id.respond_via_message_reset:
+ // Reset the preferences settings
+ SharedPreferences prefs = getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+ editor.apply();
+
+ return true;
+ default:
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+ }
+
+ /**
+ * Read the (customizable) canned responses from SharedPreferences,
+ * or from defaults if the user has never actually brought up
+ * the Settings UI.
+ *
+ * This method does disk I/O (reading the SharedPreferences file)
+ * so don't call it from the main thread.
+ *
+ * @see RespondViaSmsManager.Settings
+ */
+ private String[] loadCannedResponses() {
+ if (DBG) log("loadCannedResponses()...");
+
+ SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
+ Context.MODE_PRIVATE);
+ final Resources res = mInCallScreen.getResources();
+
+ String[] responses = new String[NUM_CANNED_RESPONSES];
+
+ // Note the default values here must agree with the corresponding
+ // android:defaultValue attributes in respond_via_sms_settings.xml.
+
+ responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
+ res.getString(R.string.respond_via_sms_canned_response_1));
+ responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
+ res.getString(R.string.respond_via_sms_canned_response_2));
+ responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
+ res.getString(R.string.respond_via_sms_canned_response_3));
+ responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
+ res.getString(R.string.respond_via_sms_canned_response_4));
+ return responses;
+ }
+
+ /**
+ * @return true if the "Respond via SMS" feature should be enabled
+ * for the specified incoming call.
+ *
+ * The general rule is that we *do* allow "Respond via SMS" except for
+ * the few (relatively rare) cases where we know for sure it won't
+ * work, namely:
+ * - a bogus or blank incoming number
+ * - a call from a SIP address
+ * - a "call presentation" that doesn't allow the number to be revealed
+ *
+ * In all other cases, we allow the user to respond via SMS.
+ *
+ * Note that this behavior isn't perfect; for example we have no way
+ * to detect whether the incoming call is from a landline (with most
+ * networks at least), so we still enable this feature even though
+ * SMSes to that number will silently fail.
+ */
+ public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
+ if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")...");
+
+ // First some basic sanity checks:
+ if (ringingCall == null) {
+ Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
+ return false;
+ }
+ if (!ringingCall.isRinging()) {
+ // The call is in some state other than INCOMING or WAITING!
+ // (This should almost never happen, but it *could*
+ // conceivably happen if the ringing call got disconnected by
+ // the network just *after* we got it from the CallManager.)
+ Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
+ + ringingCall.getState());
+ return false;
+ }
+ Connection conn = ringingCall.getLatestConnection();
+ if (conn == null) {
+ // The call doesn't have any connections! (Again, this can
+ // happen if the ringing call disconnects at the exact right
+ // moment, but should almost never happen in practice.)
+ Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
+ return false;
+ }
+
+ // Check the incoming number:
+ final String number = conn.getAddress();
+ if (DBG) log("- number: '" + number + "'");
+ if (TextUtils.isEmpty(number)) {
+ Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
+ return false;
+ }
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // The incoming number is actually a URI (i.e. a SIP address),
+ // not a regular PSTN phone number, and we can't send SMSes to
+ // SIP addresses.
+ // (TODO: That might still be possible eventually, though. Is
+ // there some SIP-specific equivalent to sending a text message?)
+ Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
+ return false;
+ }
+
+ // Finally, check the "call presentation":
+ int presentation = conn.getNumberPresentation();
+ if (DBG) log("- presentation: " + presentation);
+ if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
+ // PRESENTATION_RESTRICTED means "caller-id blocked".
+ // The user isn't allowed to see the number in the first
+ // place, so obviously we can't let you send an SMS to it.
+ Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
+ return false;
+ }
+
+ // Allow the feature only when there's a destination for it.
+ if (context.getPackageManager().resolveService(getInstantTextIntent(number, null, null) , 0)
+ == null) {
+ return false;
+ }
+
+ // TODO: with some carriers (in certain countries) you *can* actually
+ // tell whether a given number is a mobile phone or not. So in that
+ // case we could potentially return false here if the incoming call is
+ // from a land line.
+
+ // If none of the above special cases apply, it's OK to enable the
+ // "Respond via SMS" feature.
+ return true;
+ }
+
+ private int getIconSize() {
+ if (mIconSize < 0) {
+ final ActivityManager am =
+ (ActivityManager) mInCallScreen.getSystemService(Context.ACTIVITY_SERVICE);
+ mIconSize = am.getLauncherLargeIconSize();
+ }
+
+ return mIconSize;
+ }
+
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}