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