summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/interactions/PhoneNumberInteraction.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dialer/interactions/PhoneNumberInteraction.java')
-rw-r--r--src/com/android/dialer/interactions/PhoneNumberInteraction.java474
1 files changed, 474 insertions, 0 deletions
diff --git a/src/com/android/dialer/interactions/PhoneNumberInteraction.java b/src/com/android/dialer/interactions/PhoneNumberInteraction.java
new file mode 100644
index 000000000..cb19a1853
--- /dev/null
+++ b/src/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.interactions;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.Collapser.Collapsible;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.activity.TransactionSafeActivity;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.R;
+import com.android.dialer.contact.ContactUpdateService;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Initiates phone calls or a text message. If there are multiple candidates, this class shows a
+ * dialog to pick one. Creating one of these interactions should be done through the static
+ * factory methods.
+ *
+ * Note that this class initiates not only usual *phone* calls but also *SIP* calls.
+ *
+ * TODO: clean up code and documents since it is quite confusing to use "phone numbers" or
+ * "phone calls" here while they can be SIP addresses or SIP calls (See also issue 5039627).
+ */
+public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
+ private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
+
+ /**
+ * A model object for capturing a phone number for a given contact.
+ */
+ @VisibleForTesting
+ /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> {
+ long id;
+ String phoneNumber;
+ String accountType;
+ String dataSet;
+ long type;
+ String label;
+ /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */
+ String mimeType;
+
+ public PhoneItem() {
+ }
+
+ private PhoneItem(Parcel in) {
+ this.id = in.readLong();
+ this.phoneNumber = in.readString();
+ this.accountType = in.readString();
+ this.dataSet = in.readString();
+ this.type = in.readLong();
+ this.label = in.readString();
+ this.mimeType = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeString(phoneNumber);
+ dest.writeString(accountType);
+ dest.writeString(dataSet);
+ dest.writeLong(type);
+ dest.writeString(label);
+ dest.writeString(mimeType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean collapseWith(PhoneItem phoneItem) {
+ if (!shouldCollapseWith(phoneItem)) {
+ return false;
+ }
+ // Just keep the number and id we already have.
+ return true;
+ }
+
+ @Override
+ public boolean shouldCollapseWith(PhoneItem phoneItem) {
+ return MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE, phoneNumber,
+ Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
+ }
+
+ @Override
+ public String toString() {
+ return phoneNumber;
+ }
+
+ public static final Parcelable.Creator<PhoneItem> CREATOR
+ = new Parcelable.Creator<PhoneItem>() {
+ @Override
+ public PhoneItem createFromParcel(Parcel in) {
+ return new PhoneItem(in);
+ }
+
+ @Override
+ public PhoneItem[] newArray(int size) {
+ return new PhoneItem[size];
+ }
+ };
+ }
+
+ /**
+ * A list adapter that populates the list of contact's phone numbers.
+ */
+ private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> {
+ private final int mInteractionType;
+
+ public PhoneItemAdapter(Context context, List<PhoneItem> list,
+ int interactionType) {
+ super(context, R.layout.phone_disambig_item, android.R.id.text2, list);
+ mInteractionType = interactionType;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = super.getView(position, convertView, parent);
+
+ final PhoneItem item = getItem(position);
+ final TextView typeView = (TextView) view.findViewById(android.R.id.text1);
+ CharSequence value = ContactDisplayUtils.getLabelForCallOrSms((int) item.type,
+ item.label, mInteractionType, getContext());
+
+ typeView.setText(value);
+ return view;
+ }
+ }
+
+ /**
+ * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which
+ * one will be chosen to make a call or initiate an sms message.
+ *
+ * It is recommended to use
+ * {@link PhoneNumberInteraction#startInteractionForPhoneCall(TransactionSafeActivity, Uri)} or
+ * {@link PhoneNumberInteraction#startInteractionForTextMessage(TransactionSafeActivity, Uri)}
+ * instead of directly using this class, as those methods handle one or multiple data cases
+ * appropriately.
+ */
+ /* Made public to let the system reach this class */
+ public static class PhoneDisambiguationDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+ private static final String ARG_PHONE_LIST = "phoneList";
+ private static final String ARG_INTERACTION_TYPE = "interactionType";
+ private static final String ARG_CALL_ORIGIN = "callOrigin";
+
+ private int mInteractionType;
+ private ListAdapter mPhonesAdapter;
+ private List<PhoneItem> mPhoneList;
+ private String mCallOrigin;
+
+ public static void show(FragmentManager fragmentManager,
+ ArrayList<PhoneItem> phoneList, int interactionType,
+ String callOrigin) {
+ PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
+ bundle.putSerializable(ARG_INTERACTION_TYPE, interactionType);
+ bundle.putString(ARG_CALL_ORIGIN, callOrigin);
+ fragment.setArguments(bundle);
+ fragment.show(fragmentManager, TAG);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
+ mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE);
+ mCallOrigin = getArguments().getString(ARG_CALL_ORIGIN);
+
+ mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
+ final LayoutInflater inflater = activity.getLayoutInflater();
+ final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
+ return new AlertDialog.Builder(activity)
+ .setAdapter(mPhonesAdapter, this)
+ .setTitle(mInteractionType == ContactDisplayUtils.INTERACTION_SMS
+ ? R.string.sms_disambig_title : R.string.call_disambig_title)
+ .setView(setPrimaryView)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Activity activity = getActivity();
+ if (activity == null) return;
+ final AlertDialog alertDialog = (AlertDialog)dialog;
+ if (mPhoneList.size() > which && which >= 0) {
+ final PhoneItem phoneItem = mPhoneList.get(which);
+ final CheckBox checkBox = (CheckBox)alertDialog.findViewById(R.id.setPrimary);
+ if (checkBox.isChecked()) {
+ // Request to mark the data as primary in the background.
+ final Intent serviceIntent = ContactUpdateService.createSetSuperPrimaryIntent(
+ activity, phoneItem.id);
+ activity.startService(serviceIntent);
+ }
+
+ PhoneNumberInteraction.performAction(activity, phoneItem.phoneNumber,
+ mInteractionType, mCallOrigin);
+ } else {
+ dialog.dismiss();
+ }
+ }
+ }
+
+ private static final String[] PHONE_NUMBER_PROJECTION = new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.IS_SUPER_PRIMARY,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.MIMETYPE
+ };
+
+ private static final String PHONE_NUMBER_SELECTION =
+ Data.MIMETYPE + " IN ('"
+ + Phone.CONTENT_ITEM_TYPE + "', "
+ + "'" + SipAddress.CONTENT_ITEM_TYPE + "') AND "
+ + Data.DATA1 + " NOT NULL";
+
+ private final Context mContext;
+ private final OnDismissListener mDismissListener;
+ private final int mInteractionType;
+
+ private final String mCallOrigin;
+
+ private CursorLoader mLoader;
+
+ /**
+ * Constructs a new {@link PhoneNumberInteraction}. The constructor takes in a {@link Context}
+ * instead of a {@link TransactionSafeActivity} for testing purposes to verify the functionality
+ * of this class. However, all factory methods for creating {@link PhoneNumberInteraction}s
+ * require a {@link TransactionSafeActivity} (i.e. see {@link #startInteractionForPhoneCall}).
+ */
+ @VisibleForTesting
+ /* package */ PhoneNumberInteraction(Context context, int interactionType,
+ DialogInterface.OnDismissListener dismissListener) {
+ this(context, interactionType, dismissListener, null);
+ }
+
+ private PhoneNumberInteraction(Context context, int interactionType,
+ DialogInterface.OnDismissListener dismissListener, String callOrigin) {
+ mContext = context;
+ mInteractionType = interactionType;
+ mDismissListener = dismissListener;
+ mCallOrigin = callOrigin;
+ }
+
+ private void performAction(String phoneNumber) {
+ PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mCallOrigin);
+ }
+
+ private static void performAction(
+ Context context, String phoneNumber, int interactionType,
+ String callOrigin) {
+ Intent intent;
+ switch (interactionType) {
+ case ContactDisplayUtils.INTERACTION_SMS:
+ intent = new Intent(
+ Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
+ break;
+ default:
+ intent = CallUtil.getCallIntent(phoneNumber, callOrigin);
+ break;
+ }
+ context.startActivity(intent);
+ }
+
+ /**
+ * Initiates the interaction. This may result in a phone call or sms message started
+ * or a disambiguation dialog to determine which phone number should be used.
+ */
+ @VisibleForTesting
+ /* package */ void startInteraction(Uri uri) {
+ if (mLoader != null) {
+ mLoader.reset();
+ }
+
+ final Uri queryUri;
+ final String inputUriAsString = uri.toString();
+ if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
+ if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
+ queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+ } else {
+ queryUri = uri;
+ }
+ } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
+ queryUri = uri;
+ } else {
+ throw new UnsupportedOperationException(
+ "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
+ }
+
+ mLoader = new CursorLoader(mContext,
+ queryUri,
+ PHONE_NUMBER_PROJECTION,
+ PHONE_NUMBER_SELECTION,
+ null,
+ null);
+ mLoader.registerListener(0, this);
+ mLoader.startLoading();
+ }
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
+ if (cursor == null || !isSafeToCommitTransactions()) {
+ onDismiss();
+ return;
+ }
+
+ ArrayList<PhoneItem> phoneList = new ArrayList<PhoneItem>();
+ String primaryPhone = null;
+ try {
+ while (cursor.moveToNext()) {
+ if (cursor.getInt(cursor.getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
+ // Found super primary, call it.
+ primaryPhone = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+ break;
+ }
+
+ PhoneItem item = new PhoneItem();
+ item.id = cursor.getLong(cursor.getColumnIndex(Data._ID));
+ item.phoneNumber = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+ item.accountType =
+ cursor.getString(cursor.getColumnIndex(RawContacts.ACCOUNT_TYPE));
+ item.dataSet = cursor.getString(cursor.getColumnIndex(RawContacts.DATA_SET));
+ item.type = cursor.getInt(cursor.getColumnIndex(Phone.TYPE));
+ item.label = cursor.getString(cursor.getColumnIndex(Phone.LABEL));
+ item.mimeType = cursor.getString(cursor.getColumnIndex(Phone.MIMETYPE));
+
+ phoneList.add(item);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (primaryPhone != null) {
+ performAction(primaryPhone);
+ onDismiss();
+ return;
+ }
+
+ Collapser.collapseList(phoneList);
+
+ if (phoneList.size() == 0) {
+ onDismiss();
+ } else if (phoneList.size() == 1) {
+ PhoneItem item = phoneList.get(0);
+ onDismiss();
+ performAction(item.phoneNumber);
+ } else {
+ // There are multiple candidates. Let the user choose one.
+ showDisambiguationDialog(phoneList);
+ }
+ }
+
+ private boolean isSafeToCommitTransactions() {
+ return mContext instanceof TransactionSafeActivity ?
+ ((TransactionSafeActivity) mContext).isSafeToCommitTransactions() : true;
+ }
+
+ private void onDismiss() {
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss(null);
+ }
+ }
+
+ /**
+ * Start call action using given contact Uri. If there are multiple candidates for the phone
+ * call, dialog is automatically shown and the user is asked to choose one.
+ *
+ * @param activity that is calling this interaction. This must be of type
+ * {@link TransactionSafeActivity} because we need to check on the activity state after the
+ * phone numbers have been queried for.
+ * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
+ * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
+ * data Uri won't.
+ */
+ public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri) {
+ (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null))
+ .startInteraction(uri);
+ }
+
+ /**
+ * @param activity that is calling this interaction. This must be of type
+ * {@link TransactionSafeActivity} because we need to check on the activity state after the
+ * phone numbers have been queried for.
+ * @param callOrigin If non null, {@link PhoneConstants#EXTRA_CALL_ORIGIN} will be
+ * appended to the Intent initiating phone call. See comments in Phone package (PhoneApp)
+ * for more detail.
+ */
+ public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
+ String callOrigin) {
+ (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null, callOrigin))
+ .startInteraction(uri);
+ }
+
+ /**
+ * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple
+ * candidates for the phone call, dialog is automatically shown and the user is asked to choose
+ * one.
+ *
+ * @param activity that is calling this interaction. This must be of type
+ * {@link TransactionSafeActivity} because we need to check on the activity state after the
+ * phone numbers have been queried for.
+ * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
+ * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
+ * data Uri won't.
+ */
+ public static void startInteractionForTextMessage(TransactionSafeActivity activity, Uri uri) {
+ (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_SMS, null))
+ .startInteraction(uri);
+ }
+
+ @VisibleForTesting
+ /* package */ CursorLoader getLoader() {
+ return mLoader;
+ }
+
+ @VisibleForTesting
+ /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
+ PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(),
+ phoneList, mInteractionType, mCallOrigin);
+ }
+}