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