blob: cb19a18536d9d9c95312983a749ed1225a20b9d1 [file] [log] [blame]
/*
* 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);
}
}