PhoneNumberInteraction.java revision 67c8d4da26099a928a1568edaa59ef912dc13898
1/* 2 * Copyright (C) 2010 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 */ 16package com.android.dialer.interactions; 17 18import android.app.Activity; 19import android.app.AlertDialog; 20import android.app.Dialog; 21import android.app.DialogFragment; 22import android.app.FragmentManager; 23import android.content.Context; 24import android.content.CursorLoader; 25import android.content.DialogInterface; 26import android.content.DialogInterface.OnDismissListener; 27import android.content.Intent; 28import android.content.Loader; 29import android.content.Loader.OnLoadCompleteListener; 30import android.database.Cursor; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Parcel; 34import android.os.Parcelable; 35import android.provider.ContactsContract.CommonDataKinds.Phone; 36import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37import android.provider.ContactsContract.Contacts; 38import android.provider.ContactsContract.Data; 39import android.provider.ContactsContract.RawContacts; 40import android.view.LayoutInflater; 41import android.view.View; 42import android.view.ViewGroup; 43import android.widget.ArrayAdapter; 44import android.widget.CheckBox; 45import android.widget.ListAdapter; 46import android.widget.TextView; 47 48import com.android.contacts.common.CallUtil; 49import com.android.contacts.common.Collapser; 50import com.android.contacts.common.Collapser.Collapsible; 51import com.android.contacts.common.MoreContactUtils; 52import com.android.contacts.common.activity.TransactionSafeActivity; 53import com.android.contacts.common.util.ContactDisplayUtils; 54import com.android.dialer.R; 55import com.android.dialer.contact.ContactUpdateService; 56import com.android.dialer.util.DialerUtils; 57 58import com.google.common.annotations.VisibleForTesting; 59 60import java.util.ArrayList; 61import java.util.List; 62 63/** 64 * Initiates phone calls or a text message. If there are multiple candidates, this class shows a 65 * dialog to pick one. Creating one of these interactions should be done through the static 66 * factory methods. 67 * 68 * Note that this class initiates not only usual *phone* calls but also *SIP* calls. 69 * 70 * TODO: clean up code and documents since it is quite confusing to use "phone numbers" or 71 * "phone calls" here while they can be SIP addresses or SIP calls (See also issue 5039627). 72 */ 73public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { 74 private static final String TAG = PhoneNumberInteraction.class.getSimpleName(); 75 76 /** 77 * A model object for capturing a phone number for a given contact. 78 */ 79 @VisibleForTesting 80 /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> { 81 long id; 82 String phoneNumber; 83 String accountType; 84 String dataSet; 85 long type; 86 String label; 87 /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */ 88 String mimeType; 89 90 public PhoneItem() { 91 } 92 93 private PhoneItem(Parcel in) { 94 this.id = in.readLong(); 95 this.phoneNumber = in.readString(); 96 this.accountType = in.readString(); 97 this.dataSet = in.readString(); 98 this.type = in.readLong(); 99 this.label = in.readString(); 100 this.mimeType = in.readString(); 101 } 102 103 @Override 104 public void writeToParcel(Parcel dest, int flags) { 105 dest.writeLong(id); 106 dest.writeString(phoneNumber); 107 dest.writeString(accountType); 108 dest.writeString(dataSet); 109 dest.writeLong(type); 110 dest.writeString(label); 111 dest.writeString(mimeType); 112 } 113 114 @Override 115 public int describeContents() { 116 return 0; 117 } 118 119 @Override 120 public void collapseWith(PhoneItem phoneItem) { 121 // Just keep the number and id we already have. 122 } 123 124 @Override 125 public boolean shouldCollapseWith(PhoneItem phoneItem) { 126 return MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE, phoneNumber, 127 Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber); 128 } 129 130 /** 131 * Temp method so build doesn't break while other CLs go in 132 */ 133 public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) { 134 return false; 135 } 136 137 @Override 138 public String toString() { 139 return phoneNumber; 140 } 141 142 public static final Parcelable.Creator<PhoneItem> CREATOR 143 = new Parcelable.Creator<PhoneItem>() { 144 @Override 145 public PhoneItem createFromParcel(Parcel in) { 146 return new PhoneItem(in); 147 } 148 149 @Override 150 public PhoneItem[] newArray(int size) { 151 return new PhoneItem[size]; 152 } 153 }; 154 } 155 156 /** 157 * A list adapter that populates the list of contact's phone numbers. 158 */ 159 private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> { 160 private final int mInteractionType; 161 162 public PhoneItemAdapter(Context context, List<PhoneItem> list, 163 int interactionType) { 164 super(context, R.layout.phone_disambig_item, android.R.id.text2, list); 165 mInteractionType = interactionType; 166 } 167 168 @Override 169 public View getView(int position, View convertView, ViewGroup parent) { 170 final View view = super.getView(position, convertView, parent); 171 172 final PhoneItem item = getItem(position); 173 final TextView typeView = (TextView) view.findViewById(android.R.id.text1); 174 CharSequence value = ContactDisplayUtils.getLabelForCallOrSms((int) item.type, 175 item.label, mInteractionType, getContext()); 176 177 typeView.setText(value); 178 return view; 179 } 180 } 181 182 /** 183 * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which 184 * one will be chosen to make a call or initiate an sms message. 185 * 186 * It is recommended to use 187 * {@link PhoneNumberInteraction#startInteractionForPhoneCall(TransactionSafeActivity, Uri)} or 188 * {@link PhoneNumberInteraction#startInteractionForTextMessage(TransactionSafeActivity, Uri)} 189 * instead of directly using this class, as those methods handle one or multiple data cases 190 * appropriately. 191 */ 192 /* Made public to let the system reach this class */ 193 public static class PhoneDisambiguationDialogFragment extends DialogFragment 194 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 195 196 private static final String ARG_PHONE_LIST = "phoneList"; 197 private static final String ARG_INTERACTION_TYPE = "interactionType"; 198 private static final String ARG_CALL_ORIGIN = "callOrigin"; 199 200 private int mInteractionType; 201 private ListAdapter mPhonesAdapter; 202 private List<PhoneItem> mPhoneList; 203 private String mCallOrigin; 204 205 public static void show(FragmentManager fragmentManager, 206 ArrayList<PhoneItem> phoneList, int interactionType, 207 String callOrigin) { 208 PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment(); 209 Bundle bundle = new Bundle(); 210 bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList); 211 bundle.putSerializable(ARG_INTERACTION_TYPE, interactionType); 212 bundle.putString(ARG_CALL_ORIGIN, callOrigin); 213 fragment.setArguments(bundle); 214 fragment.show(fragmentManager, TAG); 215 } 216 217 @Override 218 public Dialog onCreateDialog(Bundle savedInstanceState) { 219 final Activity activity = getActivity(); 220 mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST); 221 mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE); 222 mCallOrigin = getArguments().getString(ARG_CALL_ORIGIN); 223 224 mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType); 225 final LayoutInflater inflater = activity.getLayoutInflater(); 226 final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null); 227 return new AlertDialog.Builder(activity) 228 .setAdapter(mPhonesAdapter, this) 229 .setTitle(mInteractionType == ContactDisplayUtils.INTERACTION_SMS 230 ? R.string.sms_disambig_title : R.string.call_disambig_title) 231 .setView(setPrimaryView) 232 .create(); 233 } 234 235 @Override 236 public void onClick(DialogInterface dialog, int which) { 237 final Activity activity = getActivity(); 238 if (activity == null) return; 239 final AlertDialog alertDialog = (AlertDialog)dialog; 240 if (mPhoneList.size() > which && which >= 0) { 241 final PhoneItem phoneItem = mPhoneList.get(which); 242 final CheckBox checkBox = (CheckBox)alertDialog.findViewById(R.id.setPrimary); 243 if (checkBox.isChecked()) { 244 // Request to mark the data as primary in the background. 245 final Intent serviceIntent = ContactUpdateService.createSetSuperPrimaryIntent( 246 activity, phoneItem.id); 247 activity.startService(serviceIntent); 248 } 249 250 PhoneNumberInteraction.performAction(activity, phoneItem.phoneNumber, 251 mInteractionType, mCallOrigin); 252 } else { 253 dialog.dismiss(); 254 } 255 } 256 } 257 258 private static final String[] PHONE_NUMBER_PROJECTION = new String[] { 259 Phone._ID, // 0 260 Phone.NUMBER, // 1 261 Phone.IS_SUPER_PRIMARY, // 2 262 RawContacts.ACCOUNT_TYPE, // 3 263 RawContacts.DATA_SET, // 4 264 Phone.TYPE, // 5 265 Phone.LABEL, // 6 266 Phone.MIMETYPE, // 7 267 Phone.CONTACT_ID // 8 268 }; 269 270 private static final int _ID = 0; 271 private static final int NUMBER = 1; 272 private static final int IS_SUPER_PRIMARY = 2; 273 private static final int ACCOUNT_TYPE = 3; 274 private static final int DATA_SET = 4; 275 private static final int TYPE = 5; 276 private static final int LABEL = 6; 277 private static final int MIMETYPE = 7; 278 private static final int CONTACT_ID = 8; 279 280 private static final String PHONE_NUMBER_SELECTION = 281 Data.MIMETYPE + " IN ('" 282 + Phone.CONTENT_ITEM_TYPE + "', " 283 + "'" + SipAddress.CONTENT_ITEM_TYPE + "') AND " 284 + Data.DATA1 + " NOT NULL"; 285 286 private final Context mContext; 287 private final OnDismissListener mDismissListener; 288 private final int mInteractionType; 289 290 private final String mCallOrigin; 291 private boolean mUseDefault; 292 293 private static final int UNKNOWN_CONTACT_ID = -1; 294 private long mContactId = UNKNOWN_CONTACT_ID; 295 296 private CursorLoader mLoader; 297 298 /** 299 * Constructs a new {@link PhoneNumberInteraction}. The constructor takes in a {@link Context} 300 * instead of a {@link TransactionSafeActivity} for testing purposes to verify the functionality 301 * of this class. However, all factory methods for creating {@link PhoneNumberInteraction}s 302 * require a {@link TransactionSafeActivity} (i.e. see {@link #startInteractionForPhoneCall}). 303 */ 304 @VisibleForTesting 305 /* package */ PhoneNumberInteraction(Context context, int interactionType, 306 DialogInterface.OnDismissListener dismissListener) { 307 this(context, interactionType, dismissListener, null); 308 } 309 310 private PhoneNumberInteraction(Context context, int interactionType, 311 DialogInterface.OnDismissListener dismissListener, String callOrigin) { 312 mContext = context; 313 mInteractionType = interactionType; 314 mDismissListener = dismissListener; 315 mCallOrigin = callOrigin; 316 } 317 318 private void performAction(String phoneNumber) { 319 PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mCallOrigin); 320 } 321 322 private static void performAction( 323 Context context, String phoneNumber, int interactionType, 324 String callOrigin) { 325 Intent intent; 326 switch (interactionType) { 327 case ContactDisplayUtils.INTERACTION_SMS: 328 intent = new Intent( 329 Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null)); 330 break; 331 default: 332 intent = CallUtil.getCallIntent(phoneNumber, callOrigin); 333 break; 334 } 335 DialerUtils.startActivityWithErrorToast(context, intent); 336 } 337 338 /** 339 * Initiates the interaction. This may result in a phone call or sms message started 340 * or a disambiguation dialog to determine which phone number should be used. If there 341 * is a primary phone number, it will be automatically used and a disambiguation dialog 342 * will no be shown. 343 */ 344 @VisibleForTesting 345 /* package */ void startInteraction(Uri uri) { 346 startInteraction(uri, true); 347 } 348 349 /** 350 * Initiates the interaction to result in either a phone call or sms message for a contact. 351 * @param uri Contact Uri 352 * @param useDefault Whether or not to use the primary(default) phone number. If true, the 353 * primary phone number will always be used by default if one is available. If false, a 354 * disambiguation dialog will be shown regardless of whether or not a primary phone number 355 * is available. 356 */ 357 @VisibleForTesting 358 /* package */ void startInteraction(Uri uri, boolean useDefault) { 359 if (mLoader != null) { 360 mLoader.reset(); 361 } 362 mUseDefault = useDefault; 363 final Uri queryUri; 364 final String inputUriAsString = uri.toString(); 365 if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) { 366 if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) { 367 queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY); 368 } else { 369 queryUri = uri; 370 } 371 } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) { 372 queryUri = uri; 373 } else { 374 throw new UnsupportedOperationException( 375 "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")"); 376 } 377 378 mLoader = new CursorLoader(mContext, 379 queryUri, 380 PHONE_NUMBER_PROJECTION, 381 PHONE_NUMBER_SELECTION, 382 null, 383 null); 384 mLoader.registerListener(0, this); 385 mLoader.startLoading(); 386 } 387 388 @Override 389 public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) { 390 if (cursor == null || !isSafeToCommitTransactions()) { 391 onDismiss(); 392 return; 393 } 394 ArrayList<PhoneItem> phoneList = new ArrayList<PhoneItem>(); 395 String primaryPhone = null; 396 try { 397 while (cursor.moveToNext()) { 398 if (mContactId == UNKNOWN_CONTACT_ID) { 399 mContactId = cursor.getLong(CONTACT_ID); 400 } 401 402 if (mUseDefault && cursor.getInt(IS_SUPER_PRIMARY) != 0) { 403 // Found super primary, call it. 404 primaryPhone = cursor.getString(NUMBER); 405 } 406 407 PhoneItem item = new PhoneItem(); 408 item.id = cursor.getLong(_ID); 409 item.phoneNumber = cursor.getString(NUMBER); 410 item.accountType = cursor.getString(ACCOUNT_TYPE); 411 item.dataSet = cursor.getString(DATA_SET); 412 item.type = cursor.getInt(TYPE); 413 item.label = cursor.getString(LABEL); 414 item.mimeType = cursor.getString(MIMETYPE); 415 416 phoneList.add(item); 417 } 418 } finally { 419 cursor.close(); 420 } 421 422 if (mUseDefault && primaryPhone != null) { 423 performAction(primaryPhone); 424 onDismiss(); 425 return; 426 } 427 428 Collapser.collapseList(phoneList); 429 430 if (phoneList.size() == 0) { 431 onDismiss(); 432 } else if (phoneList.size() == 1) { 433 PhoneItem item = phoneList.get(0); 434 onDismiss(); 435 performAction(item.phoneNumber); 436 } else { 437 // There are multiple candidates. Let the user choose one. 438 showDisambiguationDialog(phoneList); 439 } 440 } 441 442 private boolean isSafeToCommitTransactions() { 443 return mContext instanceof TransactionSafeActivity ? 444 ((TransactionSafeActivity) mContext).isSafeToCommitTransactions() : true; 445 } 446 447 private void onDismiss() { 448 if (mDismissListener != null) { 449 mDismissListener.onDismiss(null); 450 } 451 } 452 453 /** 454 * Start call action using given contact Uri. If there are multiple candidates for the phone 455 * call, dialog is automatically shown and the user is asked to choose one. 456 * 457 * @param activity that is calling this interaction. This must be of type 458 * {@link TransactionSafeActivity} because we need to check on the activity state after the 459 * phone numbers have been queried for. 460 * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri 461 * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while 462 * data Uri won't. 463 */ 464 public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri) { 465 (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null)) 466 .startInteraction(uri, true); 467 } 468 469 /** 470 * Start call action using given contact Uri. If there are multiple candidates for the phone 471 * call, dialog is automatically shown and the user is asked to choose one. 472 * 473 * @param activity that is calling this interaction. This must be of type 474 * {@link TransactionSafeActivity} because we need to check on the activity state after the 475 * phone numbers have been queried for. 476 * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri 477 * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while 478 * data Uri won't. 479 * @param useDefault Whether or not to use the primary(default) phone number. If true, the 480 * primary phone number will always be used by default if one is available. If false, a 481 * disambiguation dialog will be shown regardless of whether or not a primary phone number 482 * is available. 483 */ 484 public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri, 485 boolean useDefault) { 486 (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null)) 487 .startInteraction(uri, useDefault); 488 } 489 490 /** 491 * @param activity that is calling this interaction. This must be of type 492 * {@link TransactionSafeActivity} because we need to check on the activity state after the 493 * phone numbers have been queried for. 494 * @param callOrigin If non null, {@link PhoneConstants#EXTRA_CALL_ORIGIN} will be 495 * appended to the Intent initiating phone call. See comments in Phone package (PhoneApp) 496 * for more detail. 497 */ 498 public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri, 499 String callOrigin) { 500 (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null, callOrigin)) 501 .startInteraction(uri, true); 502 } 503 504 /** 505 * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple 506 * candidates for the phone call, dialog is automatically shown and the user is asked to choose 507 * one. 508 * 509 * @param activity that is calling this interaction. This must be of type 510 * {@link TransactionSafeActivity} because we need to check on the activity state after the 511 * phone numbers have been queried for. 512 * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri 513 * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while 514 * data Uri won't. 515 */ 516 public static void startInteractionForTextMessage(TransactionSafeActivity activity, Uri uri) { 517 (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_SMS, null)) 518 .startInteraction(uri, true); 519 } 520 521 @VisibleForTesting 522 /* package */ CursorLoader getLoader() { 523 return mLoader; 524 } 525 526 @VisibleForTesting 527 /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) { 528 PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(), 529 phoneList, mInteractionType, mCallOrigin); 530 } 531} 532