From 9c5fe4420bc1a1fe4edcac99e1f73b13bca63fa2 Mon Sep 17 00:00:00 2001 From: Chiao Cheng Date: Wed, 31 Oct 2012 11:12:12 -0700 Subject: Moving PhoneNumberInteraction from Contacts. Bug: 6993891 Change-Id: Id4701c00455de609850a5afea74e68c6bd84cbb3 --- src/com/android/dialer/DialtactsActivity.java | 2 +- .../interactions/PhoneNumberInteraction.java | 474 +++++++++++++++++++++ 2 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 src/com/android/dialer/interactions/PhoneNumberInteraction.java (limited to 'src') diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java index f2119e007..7154a12d3 100644 --- a/src/com/android/dialer/DialtactsActivity.java +++ b/src/com/android/dialer/DialtactsActivity.java @@ -58,7 +58,7 @@ import android.widget.SearchView.OnQueryTextListener; import com.android.contacts.common.CallUtil; import com.android.contacts.common.activity.TransactionSafeActivity; -import com.android.contacts.interactions.PhoneNumberInteraction; +import com.android.dialer.interactions.PhoneNumberInteraction; import com.android.contacts.list.ContactListFilterController; import com.android.contacts.list.ContactListFilterController.ContactListFilterListener; import com.android.contacts.list.ContactListItemView; 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 { + 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 { + 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 CREATOR + = new Parcelable.Creator() { + @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 { + private final int mInteractionType; + + public PhoneItemAdapter(Context context, List 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 mPhoneList; + private String mCallOrigin; + + public static void show(FragmentManager fragmentManager, + ArrayList 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 loader, Cursor cursor) { + if (cursor == null || !isSafeToCommitTransactions()) { + onDismiss(); + return; + } + + ArrayList phoneList = new ArrayList(); + 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 phoneList) { + PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(), + phoneList, mInteractionType, mCallOrigin); + } +} -- cgit v1.2.3