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