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