...
 
Commits (2)
  • Torsten Grote's avatar
    Show call method chooser dialog before making calls · 2dcfcf59
    Torsten Grote authored
    The chooser is always shown, even if only legacy call options are
    available. This is to warn people that they are about to make a call
    that is not encrypted end-to-end.
    
    If Signal or WhatsApp are available as call-options, they are presented
    above legacy calls. Even if not available, Signal is always shown
    either as unavailable or installable.
    
    When making calls from searches, we don't have access to third-party
    calling accounts, so the DialogFragment implementing the chooser can
    asynchronously lookup the required IDs for calls to Signal and WhatsApp.
    2dcfcf59
  • Torsten Grote's avatar
    Show privacy warning on in-call screen · 6c023360
    Torsten Grote authored
    Change-Id: Ic9b18c110481df7042ca1daa36182e0999fe948f
    6c023360
......@@ -176,6 +176,6 @@ public abstract class ContactTileView extends FrameLayout {
Uri contactLookupUri, Rect viewRect, CallSpecificAppData callSpecificAppData);
/** Notification that the specified number is to be called. */
void onCallNumberDirectly(String phoneNumber, CallSpecificAppData callSpecificAppData);
void onCallNumberDirectly(String phoneNumber, CallSpecificAppData callSpecificAppData, String lookupKey);
}
}
......@@ -30,7 +30,7 @@ public interface OnPhoneNumberPickerActionListener {
* either as an audio or video call.
*/
void onPickPhoneNumber(
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData);
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData, String lookupKey);
/** Called when home menu in {@link ActionBar} is clicked by the user. */
void onHomeInActionBarSelected();
......
......@@ -91,6 +91,7 @@ import com.android.dialer.callintent.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.UiUtil;
import com.android.dialer.common.accounts.SelectAccountDialogFragment;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.common.concurrent.ThreadUtil;
import com.android.dialer.configprovider.ConfigProviderComponent;
......@@ -1407,17 +1408,17 @@ public class DialtactsActivity extends TransactionSafeActivity
@Override
public void onPickPhoneNumber(
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) {
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData, String lookupKey) {
if (phoneNumber == null) {
// Invalid phone number, but let the call go through so that InCallUI can show
// an error message.
phoneNumber = "";
}
PreCall.start(
this,
this, phoneNumber,
new CallIntentBuilder(phoneNumber, callSpecificAppData)
.setIsVideoCall(isVideoCall)
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()));
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()), lookupKey);
clearSearchOnPause = true;
}
......
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2020 The Calyx Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -19,6 +20,7 @@ package com.android.dialer.app.calllog;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
......@@ -67,6 +69,8 @@ import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
import com.android.dialer.clipboard.ClipboardUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.accounts.SelectAccountDialogFragment;
import com.android.dialer.common.accounts.SpecialCallingAccounts;
import com.android.dialer.common.concurrent.AsyncTaskExecutors;
import com.android.dialer.configprovider.ConfigProviderComponent;
import com.android.dialer.constants.ActivityRequestCodes;
......@@ -89,6 +93,7 @@ import com.android.dialer.phonenumbercache.CachedNumberLookupService;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumbercache.PhoneNumberCache;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.precall.PreCallCoordinator;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.CallUtil;
import com.android.dialer.util.DialerUtils;
......@@ -1063,11 +1068,23 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
((Activity) context)
.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_CALL_DETAILS);
} else {
CallIntentBuilder callIntentBuilder = intent.getParcelableExtra(PreCallCoordinator.EXTRA_CALL_INTENT_BUILDER);
if (Intent.ACTION_CALL.equals(intent.getAction())
&& intent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, -1)
== VideoProfile.STATE_BIDIRECTIONAL) {
Logger.get(context).logImpression(DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_CALL_LOG);
}
// hijack call to show chooser dialog (hopefully only for regular call backs)
else if (SpecialCallingAccounts.showDialog(intent, callIntentBuilder)) {
Intent phoneIntent;
if (callIntentBuilder != null) phoneIntent = callIntentBuilder.build();
else phoneIntent = intent;
FragmentManager fragmentManager = ((Activity) context).getFragmentManager();
SelectAccountDialogFragment
.newInstance(phoneIntent, info.normalizedNumber, info.signalId, info.whatsAppId)
.show(fragmentManager, "SELECT_ACCOUNT");
return; // do not start activity below
}
DialerUtils.startActivityWithErrorToast(context, intent);
}
......
......@@ -415,9 +415,9 @@ public class OldSpeedDialFragment extends Fragment
}
@Override
public void onCallNumberDirectly(String phoneNumber, CallSpecificAppData callSpecificAppData) {
public void onCallNumberDirectly(String phoneNumber, CallSpecificAppData callSpecificAppData, String lookupKey) {
FragmentUtils.getParentUnsafe(fragment, OnPhoneNumberPickerActionListener.class)
.onPickPhoneNumber(phoneNumber, false /* isVideoCall */, callSpecificAppData);
.onPickPhoneNumber(phoneNumber, false /* isVideoCall */, callSpecificAppData, lookupKey);
}
}
......
......@@ -64,6 +64,7 @@ public abstract class PhoneFavoriteTileView extends ContactTileView {
private View shadowOverlay;
/** Users' most frequent phone number. */
private String phoneNumberString;
private String lookupKey;
private boolean isPinned;
private boolean isStarred;
......@@ -99,6 +100,7 @@ public abstract class PhoneFavoriteTileView extends ContactTileView {
sendViewNotification(getContext(), entry.lookupUri);
// Grab the phone-number to call directly. See {@link onClick()}.
phoneNumberString = entry.phoneNumber;
lookupKey = entry.lookupKey;
// If this is a blank entry, don't show anything. For this to truly look like an empty row
// the entire ContactTileRow needs to be hidden.
......@@ -155,7 +157,7 @@ public abstract class PhoneFavoriteTileView extends ContactTileView {
// call them at the number that you usually talk to them
// at (i.e. the one displayed in the UI), regardless of
// whether that's their default number.
mListener.onCallNumberDirectly(phoneNumberString, callSpecificAppData.build());
mListener.onCallNumberDirectly(phoneNumberString, callSpecificAppData.build(), lookupKey);
}
}
};
......
/*
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import javax.annotation.Nullable;
class CallAccount {
enum Status { ENABLED, NOT_INSTALLED, DISABLED }
final Intent intent;
final String name;
final Drawable icon;
final boolean encrypted;
final Status status;
@Nullable
final String unavailableText;
CallAccount(Intent intent, String name, Drawable icon, boolean encrypted, Status status, @Nullable String unavailableText) {
this.intent = intent;
this.name = name;
this.icon = icon;
this.encrypted = encrypted;
this.status = status;
this.unavailableText = unavailableText;
}
CallAccount(Intent intent, String name, Drawable icon, boolean encrypted) {
this(intent, name, icon, encrypted, Status.ENABLED, null);
}
}
/*
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.dialer.common.accounts.CallAccount.Status;
import com.android.dialer.contacts.resources.R;
import java.util.List;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
class CallAccountAdapter extends ArrayAdapter<CallAccount> {
static final class ViewHolder {
ImageView accountIcon;
TextView label;
ImageView statusIcon;
TextView status;
}
private final int mResId;
CallAccountAdapter(Context context, int resource, List<CallAccount> accounts) {
super(context, resource, accounts);
mResId = resource;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return getItem(position).status != Status.DISABLED;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rowView;
final ViewHolder ui;
if (convertView == null) {
rowView = inflater.inflate(mResId, null);
ui = new ViewHolder();
ui.accountIcon = rowView.findViewById(R.id.icon);
ui.label = rowView.findViewById(R.id.label);
ui.statusIcon = rowView.findViewById(R.id.statusIcon);
ui.status = rowView.findViewById(R.id.status);
rowView.setTag(ui);
} else {
rowView = convertView;
ui = (ViewHolder) rowView.getTag();
}
CallAccount account = getItem(position);
rowView.setEnabled(account.status == Status.ENABLED);
ui.accountIcon.setImageDrawable(account.icon);
ui.accountIcon.setAlpha(account.status == Status.ENABLED ? 1.0f : 0.5f);
ui.label.setText(account.name);
if (account.encrypted) {
ui.statusIcon.setImageResource(account.status == Status.ENABLED ?
R.drawable.ic_baseline_lock : R.drawable.ic_baseline_warning);
ui.statusIcon.setVisibility(VISIBLE);
} else {
ui.statusIcon.setVisibility(GONE);
}
Context context = rowView.getContext();
if (account.status == Status.ENABLED) {
ui.status.setText(account.encrypted ?
context.getString(R.string.call_account_private) : context.getString(R.string.call_account_not_private));
} else {
ui.status.setText(account.unavailableText);
}
return rowView;
}
}
/*
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.Uri;
import android.provider.ContactsContract.Data;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
import com.android.contacts.common.compat.PhoneAccountCompat;
import com.android.dialer.common.accounts.CallAccount.Status;
import com.android.dialer.contacts.resources.R;
import com.android.dialer.util.CallUtil;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import static android.content.Intent.ACTION_VIEW;
import static android.telecom.PhoneAccount.SCHEME_SIP;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MARKET_URI_SIGNAL;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_SIGNAL;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_WHATSAPP;
class CallAccountCreator {
private final Context context;
private final TelecomManager telecomManager;
private final PackageManager packageManager;
CallAccountCreator(Context context) {
this.context = context;
this.telecomManager = context.getSystemService(TelecomManager.class);
this.packageManager = context.getPackageManager();
}
CallAccount getSignalAccount(long id) {
Intent intent;
Drawable icon;
String name = context.getString(R.string.call_account_signal);
boolean isOffline = isOffline();
if (id == -1 || isOffline) {
Status status;
String unavailableText;
if (isInstalled(MIME_TYPE_SIGNAL)) {
intent = getCustomCallIntent(id, MIME_TYPE_SIGNAL);
icon = getAppIcon(intent);
status = Status.DISABLED;
int unavailableRes = isOffline ? R.string.call_account_offline : R.string.call_account_unavailable;
unavailableText = context.getString(unavailableRes);
} else {
intent = new Intent(ACTION_VIEW, MARKET_URI_SIGNAL);
icon = context.getDrawable(R.drawable.logo_signal_disabled);
status = Status.NOT_INSTALLED;
unavailableText = context.getString(R.string.call_account_not_installed);
}
return new CallAccount(intent, name, icon, true, status, unavailableText);
} else {
intent = getCustomCallIntent(id, MIME_TYPE_SIGNAL);
icon = getAppIcon(intent);
return new CallAccount(intent, name, icon, true);
}
}
@Nullable
CallAccount getWhatsAppAccount(long id) {
Status status;
String unavailableText;
if (id == -1) {
if (isInstalled(MIME_TYPE_WHATSAPP)) {
status = Status.DISABLED;
unavailableText = context.getString(R.string.call_account_unavailable);
} else {
// do not show WhatsApp if it is not installed
return null;
}
} else if (isOffline()) {
status = Status.DISABLED;
unavailableText = context.getString(R.string.call_account_offline);
} else {
status = Status.ENABLED;
unavailableText = null;
}
Intent intent = getCustomCallIntent(id, MIME_TYPE_WHATSAPP);
String name = context.getString(R.string.call_account_whatsapp);
return new CallAccount(intent, name, getAppIcon(intent), true, status, unavailableText);
}
private Intent getCustomCallIntent(long id, String mimeType) {
Intent intent = new Intent(ACTION_VIEW);
final Uri uri = android.content.ContentUris.withAppendedId(Data.CONTENT_URI, id);
intent.setDataAndType(uri, mimeType);
return intent;
}
private boolean isOffline() {
ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
return capabilities == null || !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
private boolean isInstalled(String mimeType) {
Intent i = getCustomCallIntent(0, mimeType);
final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
return !resolveInfos.isEmpty();
}
@Nullable
private Drawable getAppIcon(Intent i) {
final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfos.isEmpty()) return null;
ResolveInfo match = resolveInfos.get(0);
if (match != null) return match.loadIcon(packageManager);
return null;
}
List<CallAccount> getPhoneAccounts(Intent phoneIntent, @Nullable String number) {
List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
ArrayList<CallAccount> callAccounts = new ArrayList<>(accountHandles.size());
for (PhoneAccountHandle accountHandle : accountHandles) {
CallAccount callAccount = getPhoneAccount(accountHandle, phoneIntent, number);
if (callAccount != null) callAccounts.add(callAccount);
}
return callAccounts;
}
@Nullable
private CallAccount getPhoneAccount(PhoneAccountHandle accountHandle, Intent phoneIntent, @Nullable String number) {
PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
Intent intent = transformIntent(account, phoneIntent, number);
if (intent == null) return null;
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
String name = account.getLabel().toString();
if (name.isEmpty()) {
TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
name = telephonyManager.getSimOperatorName();
if (name == null || name.isEmpty()) {
name = context.getString(R.string.call_account_unknown);
}
}
Drawable icon = PhoneAccountCompat.createIconDrawable(account, context);
return new CallAccount(intent, name, icon, false);
}
/**
* Creates a copy of the given Intent or transforms the Intent from sip:// to tel:// if needed.
* Returns null, if the given account does not support the URI scheme.
*/
@Nullable
private Intent transformIntent(PhoneAccount account, Intent phoneIntent, @Nullable String number) {
Uri uri = phoneIntent.getData();
if (uri != null && SCHEME_SIP.equals(uri.getScheme()) && !account.supportsUriScheme(uri.getScheme())
&& number != null && !number.isEmpty()) {
Intent intent = new Intent(phoneIntent);
intent.setData(CallUtil.getCallUri(number));
return intent;
} else if (uri != null && !account.supportsUriScheme(uri.getScheme())) {
return null;
}
return new Intent(phoneIntent);
}
}
/*
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.database.Cursor;
import android.telephony.PhoneNumberUtils;
import com.android.dialer.common.LogUtil;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_SIGNAL;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_WHATSAPP;
public class CallAccountIds {
public final long signalId, whatsappId;
private CallAccountIds(long signalId, long whatsappId) {
this.signalId = signalId;
this.whatsappId = whatsappId;
}
public static CallAccountIds fromCursor(Cursor cursor, String number) {
String normalizedNumber = normalizeNumber(number);
long signalId = -1;
long whatsappId = -1;
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String mimeType = cursor.getString(1);
String data1 = cursor.getString(2);
if (isMatch(data1, normalizedNumber)) {
if (MIME_TYPE_SIGNAL.equals(mimeType)) signalId = id;
else if (MIME_TYPE_WHATSAPP.equals(mimeType)) whatsappId = id;
} else {
LogUtil.w("CallAccountIds",
"Numbers did not match: data=" + data1 + " input=" + number + " inputNorm=" + normalizedNumber);
}
}
return new CallAccountIds(signalId, whatsappId);
}
private static boolean isMatch(String dataNumber, String targetNumber) {
String normalizedData = normalizeNumber(dataNumber);
if (normalizedData.length() >= targetNumber.length()) return normalizedData.endsWith(targetNumber);
else return targetNumber.endsWith(normalizedData);
}
private static String normalizeNumber(String number) {
String result = number;
// remove 00 that is sometimes used instead of +
if (result.startsWith("00")) result = result.replaceFirst("00", "");
// remove email style number that WhatsApp is using
if (result.indexOf('@') != -1) result = result.substring(0, result.indexOf('@'));
// normalize number, removes dashes, brackets and other stuff
result = PhoneNumberUtils.normalizeNumber(result);
// remove the + that sometimes appears after normalizing
if (result.startsWith("+")) result = result.replaceFirst("\\+", "");
return result;
}
}
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Loader;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.os.Bundle;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import com.android.dialer.contacts.resources.R;
import com.android.dialer.util.DialerUtils;
import javax.annotation.Nullable;
import java.util.ArrayList;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.ACCOUNTS_PROJECTION;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.ACCOUNTS_SELECTION;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_PHONE;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_SIGNAL;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_WHATSAPP;
/**
* Dialog that allows the user to select an accounts to call a number.
*/
@SuppressWarnings("deprecation")
public class SelectAccountDialogFragment extends DialogFragment implements LoaderCallbacks<Cursor> {
/**
* Create new fragment instance with known account IDs.
*/
public static SelectAccountDialogFragment newInstance(Intent phoneIntent, @Nullable String number,
long signalId, long whatsappId) {
SelectAccountDialogFragment fragment = new SelectAccountDialogFragment();
Bundle arguments = new Bundle();
arguments.putLong(MIME_TYPE_SIGNAL, signalId);
arguments.putLong(MIME_TYPE_WHATSAPP, whatsappId);
arguments.putString(Phone.NUMBER, number);
arguments.putParcelable(MIME_TYPE_PHONE, phoneIntent);
fragment.setArguments(arguments);
return fragment;
}
/**
* Create new fragment instance with a lookup key to lookup account IDs.
*/
public static SelectAccountDialogFragment newInstance(Intent phoneIntent, String lookupKey, String number) {
SelectAccountDialogFragment fragment = new SelectAccountDialogFragment();
Bundle arguments = new Bundle();
arguments.putString(Data.LOOKUP_KEY, lookupKey);
arguments.putString(Phone.NUMBER, number);
arguments.putParcelable(MIME_TYPE_PHONE, phoneIntent);
fragment.setArguments(arguments);
return fragment;
}
private CallAccountCreator callAccountCreator;
private CallAccountAdapter adapter;
private final ArrayList<CallAccount> callAccounts = new ArrayList<>();
private final NetworkReceiver receiver = new NetworkReceiver();
private boolean isOnline;
private String number;
private long latestSignalId = -1, latestWhatsappId = -1;
private Intent phoneIntent;
@Override
public void onAttach(Context context) {
super.onAttach(context);
Bundle arguments = getArguments();
// initialize variable that need to be available when the loader finishes
callAccountCreator = new CallAccountCreator(context);
phoneIntent = arguments.getParcelable(MIME_TYPE_PHONE);
number = arguments.getString(Phone.NUMBER);
// kick of loader, if necessary (aka a lookup key was provided)
String lookupKey = arguments.getString(Data.LOOKUP_KEY, null);
if (lookupKey != null) {
getLoaderManager().initLoader(0, getArguments(), this);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// only update accounts if the loader has not done so already
if (callAccounts.isEmpty()) {
Bundle arguments = getArguments();
final long signalId = arguments.getLong(MIME_TYPE_SIGNAL, -1);
final long whatsappId = arguments.getLong(MIME_TYPE_WHATSAPP, -1);
updateAccounts(signalId, whatsappId);
}
adapter = new CallAccountAdapter(getContext(), R.layout.call_account_list_item, callAccounts);
isOnline = isOnline(getContext());
// Register BroadcastReceiver to track network connection changes.
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
getContext().registerReceiver(receiver, filter);
}
@Override
public void onDestroy() {
super.onDestroy();
getContext().unregisterReceiver(receiver);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
final DialogInterface.OnClickListener selectionListener = (dialog, which) -> {
CallAccount callAccount = callAccounts.get(which);
DialerUtils.startActivityWithErrorToast(getContext(), callAccount.intent);
};
return builder
.setTitle(R.string.call_account_choose_title)
.setAdapter(adapter, selectionListener)
.create();
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
String lookupKey = bundle.getString(Data.LOOKUP_KEY);
return new CursorLoader(
getContext(),
Data.CONTENT_URI,
ACCOUNTS_PROJECTION,
ACCOUNTS_SELECTION,
new String[]{lookupKey, MIME_TYPE_SIGNAL, MIME_TYPE_WHATSAPP},
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
CallAccountIds ids = CallAccountIds.fromCursor(cursor, number);
updateAccounts(ids.signalId, ids.whatsappId);
// the adapter might not have been initialized
if (adapter != null) {
// the adapter is using the callAccounts field, so no need to swap items, just notify about change
adapter.notifyDataSetChanged();
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
/**
* Clears callAccounts and populates it with fresh accounts.
*/
private void updateAccounts(long signalId, long whatsappId) {
latestSignalId = signalId;
latestWhatsappId = whatsappId;
ArrayList<CallAccount> accounts = new ArrayList<>();
accounts.add(callAccountCreator.getSignalAccount(signalId));
CallAccount whatsappAccount = callAccountCreator.getWhatsAppAccount(whatsappId);
if (whatsappAccount != null) accounts.add(whatsappAccount);
accounts.addAll(callAccountCreator.getPhoneAccounts(phoneIntent, number));
callAccounts.clear();
callAccounts.addAll(accounts);
}
private boolean isOnline(Context context) {
ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
private class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
boolean wasOnline = isOnline;
isOnline = isOnline(context);
if (wasOnline != isOnline) {
updateAccounts(latestSignalId, latestWhatsappId);
if (adapter != null) adapter.notifyDataSetChanged();
}
}
}
}
/*
* Copyright (C) 2020 The Calyx Institute
*
* 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.common.accounts;
import android.content.Intent;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import com.android.dialer.callintent.CallIntentBuilder;
import javax.annotation.Nullable;
import static android.telecom.PhoneAccount.SCHEME_SIP;
import static android.telecom.PhoneAccount.SCHEME_TEL;
public interface SpecialCallingAccounts {
String MIME_TYPE_SIGNAL = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call";
String MIME_TYPE_WHATSAPP = "vnd.android.cursor.item/vnd.com.whatsapp.voip.call";
String MIME_TYPE_PHONE = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
String[] ACCOUNTS_PROJECTION = new String[]{Data._ID, Data.MIMETYPE, Data.DATA1};
String ACCOUNTS_SELECTION = Data.LOOKUP_KEY + " = ? AND " + Data.MIMETYPE + " in (?, ?)";
// normally this is "market://details?id=org.thoughtcrime.securesms" but we want to force-open F-Droid on CalyxOS
Uri MARKET_URI_SIGNAL = Uri.parse("fdroid.app:org.thoughtcrime.securesms");
static boolean showDialog(String phoneNumber, @Nullable CallIntentBuilder builder) {
if (phoneNumber == null || phoneNumber.isEmpty()) return false;
return showDialog(builder);
}
static boolean showDialog(Intent intent, @Nullable CallIntentBuilder builder) {
if (Intent.ACTION_CALL.equals(intent.getAction())) return true;
return showDialog(builder);
}
static boolean showDialog(@Nullable CallIntentBuilder builder) {
if (builder == null) return false;
if (builder.isDuoCall() || builder.isVideoCall()) return false;
String scheme = builder.getUri().getScheme();
if (!SCHEME_TEL.equals(scheme) && !SCHEME_SIP.equals(scheme)) return false;
return true;
}
}
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="16dp"
android:width="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?attr/colorControlNormal"
android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"/>
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/logo_signal_background_disabled"/>
<foreground android:drawable="@drawable/logo_signal_foreground"/>
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M48.24,30.7l0.54,2.18a21.46,21.46 0,0 0,-6 2.5L41.6,33.45A24,24 0,0 1,48.24 30.7ZM59.76,30.7 L59.22,32.88a21.46,21.46 0,0 1,6 2.5l1.16,-1.93A24,24 0,0 0,59.76 30.7ZM33.45,41.6a24,24 0,0 0,-2.75 6.64l2.18,0.54a21.46,21.46 0,0 1,2.5 -6ZM32.25,54a21.85,21.85 0,0 1,0.24 -3.26l-2.22,-0.34a24.13,24.13 0,0 0,0 7.2l2.22,-0.34A21.85,21.85 0,0 1,32.25 54ZM66.4,74.55l-1.16,-1.93a21.46,21.46 0,0 1,-6 2.5l0.54,2.18A24,24 0,0 0,66.4 74.55ZM75.75,54a21.85,21.85 0,0 1,-0.24 3.26l2.22,0.34a24.13,24.13 0,0 0,0 -7.2l-2.22,0.34A21.85,21.85 0,0 1,75.75 54ZM77.3,59.76 L75.12,59.22a21.46,21.46 0,0 1,-2.5 6l1.93,1.16A24,24 0,0 0,77.3 59.76ZM57.3,75.51a22.26,22.26 0,0 1,-6.52 0l-0.34,2.22a24.14,24.14 0,0 0,7.2 0ZM71.51,66.9a21.9,21.9 0,0 1,-4.61 4.61l1.34,1.81a24.46,24.46 0,0 0,5.08 -5.08ZM66.9,36.49a21.9,21.9 0,0 1,4.61 4.61l1.81,-1.34a24.46,24.46 0,0 0,-5.08 -5.08ZM36.49,41.1a21.9,21.9 0,0 1,4.61 -4.61l-1.34,-1.81a24.46,24.46 0,0 0,-5.08 5.08ZM74.55,41.6 L72.62,42.76a21.46,21.46 0,0 1,2.5 6l2.18,-0.54A24,24 0,0 0,74.55 41.6ZM50.74,32.49a22.26,22.26 0,0 1,6.52 0l0.34,-2.22a24.13,24.13 0,0 0,-7.2 0ZM37.65,73.91 L33,75l1.09,-4.65 -2.2,-0.51 -1.08,4.65a2.25,2.25 0,0 0,2.7 2.7l4.65,-1.08ZM32.36,67.83 L34.55,68.34 35.31,65.12a21.41,21.41 0,0 1,-2.43 -5.9l-2.18,0.54a23.85,23.85 0,0 0,2.22 5.7ZM42.88,72.69 L39.66,73.45 40.17,75.64 42.53,75.08a24.09,24.09 0,0 0,5.71 2.22l0.54,-2.18A21.41,21.41 0,0 1,42.88 72.69ZM54,34.5A19.5,19.5 0,0 0,37.49 64.37l-1.87,8 8,-1.87A19.5,19.5 0,1 0,54 34.5Z"
android:strokeAlpha="0.95"
android:fillColor="#fff"
android:fillAlpha="0.95"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~ Copyright (C) 2020 The Calyx Institute
~
~ 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
-->
<!-- Layout of a single item in the InCallUI Account Chooser Dialog. -->
<com.android.contacts.common.widget.ActivityTouchLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="64dp"
android:padding="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitCenter"/>
<LinearLayout
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="24dp"
android:gravity="start|center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/statusIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_baseline_lock"/>
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</LinearLayout>
</com.android.contacts.common.widget.ActivityTouchLinearLayout>
......@@ -15,4 +15,15 @@
limitations under the License.
-->
<resources>
<string name="call_account_choose_title">Choose a provider for this call</string>
<string name="call_account_signal" translatable="false">Signal</string>
<string name="call_account_whatsapp" translatable="false">WhatsApp</string>
<string name="call_account_unknown">Unknown</string>
<string name="call_account_private">Audio and video are private</string>
<string name="call_account_not_private">Audio is not private</string>
<string name="call_account_unavailable">Unavailable for this number</string>
<string name="call_account_offline">No internet connection</string>
<string name="call_account_not_installed">Not installed - Tap to install</string>
<string name="incall_screen_privacy_data_collection">The location and audio of this call are not private.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="logo_signal_background_disabled">?attr/colorControlNormal</color>
<color name="incall_header_background">#f3b514</color>
</resources>
......@@ -100,6 +100,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
Phone.LABEL,
Phone.MIMETYPE,
Phone.CONTACT_ID,
Phone.LOOKUP_KEY
};
private static final String PHONE_NUMBER_SELECTION =
......@@ -113,7 +114,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
+ Data.DATA1
+ " NOT NULL";
private static final int UNKNOWN_CONTACT_ID = -1;
private final Context context;
private final Activity context;
private final int interactionType;
private final CallSpecificAppData callSpecificAppData;
private long contactId = UNKNOWN_CONTACT_ID;
......@@ -157,7 +158,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
}
private PhoneNumberInteraction(
Context context,
Activity context,
int interactionType,
boolean isVideoCall,
CallSpecificAppData callSpecificAppData) {
......@@ -172,24 +173,24 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
}
private static void performAction(
Context context,
Activity context,
String phoneNumber,
int interactionType,
boolean isVideoCall,
CallSpecificAppData callSpecificAppData) {
CallSpecificAppData callSpecificAppData,
String lookupKey) {
Intent intent;
switch (interactionType) {
case ContactDisplayUtils.INTERACTION_SMS:
intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
break;
default:
intent =
PreCall.getIntent(
context,
PreCall.start(
context, phoneNumber,
new CallIntentBuilder(phoneNumber, callSpecificAppData)
.setIsVideoCall(isVideoCall)
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()));
break;
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()), lookupKey);
return;
}
DialerUtils.startActivityWithErrorToast(context, intent);
}
......@@ -211,9 +212,9 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
.startInteraction(uri);
}
private void performAction(String phoneNumber) {
private void performAction(String phoneNumber, String lookupKey) {
PhoneNumberInteraction.performAction(
context, phoneNumber, interactionType, isVideoCall, callSpecificAppData);
context, phoneNumber, interactionType, isVideoCall, callSpecificAppData, lookupKey);
}
/**
......@@ -284,6 +285,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
interactionError(InteractionErrorCode.USER_LEAVING_ACTIVITY);
return;
}
String lookupKey;
if (cursor.moveToFirst()) {
int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID);
int isSuperPrimaryColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY);
......@@ -294,6 +296,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE);
int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL);
int phoneMimeTpeColumn = cursor.getColumnIndexOrThrow(Phone.MIMETYPE);
int phoneLookupKeyColumn = cursor.getColumnIndexOrThrow(Phone.LOOKUP_KEY);
do {
if (contactId == UNKNOWN_CONTACT_ID) {
contactId = cursor.getLong(contactIdColumn);
......@@ -303,6 +306,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
// Found super primary, call it.
primaryPhone = cursor.getString(phoneNumberColumn);
}
lookupKey = cursor.getString(phoneLookupKeyColumn);
PhoneItem item = new PhoneItem();
item.id = cursor.getLong(phoneIdColumn);
......@@ -321,7 +325,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
}
if (primaryPhone != null) {
performAction(primaryPhone);
performAction(primaryPhone, lookupKey);
return;
}
......@@ -330,10 +334,10 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
interactionError(InteractionErrorCode.CONTACT_HAS_NO_NUMBER);
} else if (phoneList.size() == 1) {
PhoneItem item = phoneList.get(0);
performAction(item.phoneNumber);
performAction(item.phoneNumber, lookupKey);
} else {
// There are multiple candidates. Let the user choose one.
showDisambiguationDialog(phoneList);
showDisambiguationDialog(phoneList, lookupKey);
}
} finally {
cursor.close();
......@@ -355,7 +359,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
return loader;
}
private void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
private void showDisambiguationDialog(ArrayList<PhoneItem> phoneList, String lookupKey) {
// TODO(a bug): don't leak the activity
final Activity activity = (Activity) context;
if (activity.isFinishing()) {
......@@ -375,7 +379,8 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
phoneList,
interactionType,
isVideoCall,
callSpecificAppData);
callSpecificAppData,
lookupKey);
} catch (IllegalStateException e) {
// ignore to be safe. Shouldn't happen because we checked the
// activity wasn't destroyed, but to be safe.
......@@ -497,12 +502,14 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
private static final String ARG_PHONE_LIST = "phoneList";
private static final String ARG_INTERACTION_TYPE = "interactionType";
private static final String ARG_IS_VIDEO_CALL = "is_video_call";
private static final String ARG_LOOKUP_KEY = "lookupKey";
private int interactionType;
private ListAdapter phonesAdapter;
private List<PhoneItem> phoneList;
private CallSpecificAppData callSpecificAppData;
private boolean isVideoCall;
private String lookupKey;
public PhoneDisambiguationDialogFragment() {
super();
......@@ -513,12 +520,14 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
ArrayList<PhoneItem> phoneList,
int interactionType,
boolean isVideoCall,
CallSpecificAppData callSpecificAppData) {
CallSpecificAppData callSpecificAppData,
String lookupKey) {
PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
bundle.putString(ARG_LOOKUP_KEY, lookupKey);
CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData);
fragment.setArguments(bundle);
fragment.show(fragmentManager, TAG);
......@@ -532,6 +541,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
phoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
interactionType = getArguments().getInt(ARG_INTERACTION_TYPE);
isVideoCall = getArguments().getBoolean(ARG_IS_VIDEO_CALL);
lookupKey = getArguments().getString(ARG_LOOKUP_KEY);
callSpecificAppData = CallIntentParser.getCallSpecificAppData(getArguments());
phonesAdapter = new PhoneItemAdapter(activity, phoneList, interactionType);
......@@ -572,7 +582,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
}
PhoneNumberInteraction.performAction(
activity, phoneItem.phoneNumber, interactionType, isVideoCall, callSpecificAppData);
activity, phoneItem.phoneNumber, interactionType, isVideoCall, callSpecificAppData, lookupKey);
} else {
dialog.dismiss();
}
......
......@@ -78,6 +78,7 @@ import com.android.dialer.calllog.config.CallLogConfigComponent;
import com.android.dialer.calllog.ui.NewCallLogFragment;
import com.android.dialer.common.FragmentUtils.FragmentUtilListener;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.accounts.SelectAccountDialogFragment;
import com.android.dialer.common.concurrent.DefaultFutureCallback;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.common.concurrent.ThreadUtil;
......@@ -1144,17 +1145,17 @@ public class OldMainActivityPeer implements MainActivityPeer, FragmentUtilListen
@Override
public void onPickPhoneNumber(
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) {
String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData, String lookupKey) {
if (phoneNumber == null) {
// Invalid phone number, but let the call go through so that InCallUI can show
// an error message.
phoneNumber = "";
}
PreCall.start(
activity,
activity, phoneNumber,
new CallIntentBuilder(phoneNumber, callSpecificAppData)
.setIsVideoCall(isVideoCall)
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()));
.setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()), lookupKey);
}
@Override
......
......@@ -69,6 +69,9 @@ public class ContactInfo {
/** @see android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE */
public int carrierPresence;
public long signalId = -1;
public long whatsAppId = -1;
@Override
public int hashCode() {
// Uses only name and contactUri to determine hashcode.
......
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2020 The Calyx Institute
*
* 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
......@@ -14,6 +15,7 @@
package com.android.dialer.phonenumbercache;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
......@@ -23,6 +25,7 @@ import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.PhoneLookup;
......@@ -35,6 +38,7 @@ import com.android.contacts.common.ContactsUtils.UserType;
import com.android.contacts.common.util.Constants;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.accounts.CallAccountIds;
import com.android.dialer.logging.ContactSource;
import com.android.dialer.oem.CequintCallerIdManager;
import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
......@@ -48,6 +52,11 @@ import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.ACCOUNTS_PROJECTION;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.ACCOUNTS_SELECTION;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_SIGNAL;
import static com.android.dialer.common.accounts.SpecialCallingAccounts.MIME_TYPE_WHATSAPP;
/** Utility class to look up the contact information for a given number. */
public class ContactInfoHelper {
......@@ -349,6 +358,7 @@ public class ContactInfoHelper {
String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
fillAdditionalContactInfo(context, contactInfo);
fillCalyxContactInfo(context, contactInfo);
return contactInfo;
}
}
......@@ -392,6 +402,21 @@ public class ContactInfoHelper {
}
}
private void fillCalyxContactInfo(Context context, ContactInfo contactInfo) {
if (contactInfo.lookupKey == null) {
return;
}
String number = contactInfo.normalizedNumber == null ? contactInfo.number : contactInfo.normalizedNumber;
ContentResolver cr = context.getContentResolver();
String[] args = new String[]{contactInfo.lookupKey, MIME_TYPE_SIGNAL, MIME_TYPE_WHATSAPP};
try (Cursor c = cr.query(Data.CONTENT_URI, ACCOUNTS_PROJECTION, ACCOUNTS_SELECTION, args, null)) {
if (c == null) return;
CallAccountIds ids = CallAccountIds.fromCursor(c, number);
contactInfo.signalId = ids.signalId;
contactInfo.whatsAppId = ids.whatsappId;
}
}
/**
* Determines the contact information for the given phone number.
*
......
/*
* Copyright (C) 2017 The Android Open Source Project
* Copyright (C) 2020 The Calyx Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -16,11 +17,15 @@
package com.android.dialer.precall;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.common.accounts.SelectAccountDialogFragment;
import com.android.dialer.common.accounts.SpecialCallingAccounts;
import com.android.dialer.util.DialerUtils;
/** Interface to prepare a {@link CallIntentBuilder} before placing the call with telecom. */
......@@ -42,4 +47,15 @@ public interface PreCall {
static void start(Context context, CallIntentBuilder builder) {
DialerUtils.startActivityWithErrorToast(context, getIntent(context, builder));
}
static void start(Activity activity, String phoneNumber, CallIntentBuilder builder, @Nullable String lookupKey) {
if (SpecialCallingAccounts.showDialog(phoneNumber, builder) && lookupKey != null) {
Intent intent = builder.build();
SelectAccountDialogFragment.newInstance(intent, lookupKey, phoneNumber)
.show(activity.getFragmentManager(), "SELECT_ACCOUNT");
} else {
start(activity, builder);
}
}
}
......@@ -26,7 +26,7 @@ public interface RowClickListener {
*
* @param ranking position in the list relative to the other elements
*/
void placeVoiceCall(String phoneNumber, int ranking);
void placeVoiceCall(String phoneNumber, String lookupKey, int ranking);
/**
* Places an IMS video call.
......
......@@ -74,6 +74,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick
private int position;
private String number;
private String lookupKey;
private DialerContact dialerContact;
private @CallToAction int currentAction;
......@@ -96,6 +97,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick
dialerContact = getDialerContact(context, cursor);
position = cursor.getPosition();
number = cursor.getString(Projections.PHONE_NUMBER);
lookupKey = cursor.getString(Projections.LOOKUP_KEY);
String name = cursor.getString(Projections.DISPLAY_NAME);
String label = getLabel(context.getResources(), cursor);
String secondaryInfo =
......@@ -262,7 +264,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick
"Invalid Call to action type: " + currentAction);
}
} else {
listener.placeVoiceCall(number, position);
listener.placeVoiceCall(number, lookupKey, position);
}
}
......
......@@ -50,6 +50,7 @@ public final class DirectoryContactViewHolder extends RecyclerView.ViewHolder
private final RowClickListener listener;
private String number;
private String lookupKey;
private int position;
public DirectoryContactViewHolder(View view, RowClickListener listener) {
......@@ -69,6 +70,7 @@ public final class DirectoryContactViewHolder extends RecyclerView.ViewHolder
*/
public void bind(SearchCursor cursor, String query) {
number = cursor.getString(Projections.PHONE_NUMBER);
lookupKey = cursor.getString(Projections.LOOKUP_KEY);
position = cursor.getPosition();
String name = cursor.getString(Projections.DISPLAY_NAME);
String label = getLabel(context.getResources(), cursor);
......@@ -140,6 +142,6 @@ public final class DirectoryContactViewHolder extends RecyclerView.ViewHolder
@Override
public void onClick(View v) {
listener.placeVoiceCall(number, position);
listener.placeVoiceCall(number, lookupKey, position);
}
}
......@@ -50,6 +50,7 @@ import com.android.dialer.callintent.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.FragmentUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.accounts.SelectAccountDialogFragment;
import com.android.dialer.common.concurrent.ThreadUtil;
import com.android.dialer.dialercontact.DialerContact;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
......@@ -536,16 +537,16 @@ public final class NewSearchFragment extends Fragment
}
@Override
public void placeVoiceCall(String phoneNumber, int ranking) {
placeCall(phoneNumber, ranking, false);
public void placeVoiceCall(String phoneNumber, String lookupKey, int ranking) {
placeCall(phoneNumber, lookupKey, ranking, false);
}
@Override
public void placeVideoCall(String phoneNumber, int ranking) {
placeCall(phoneNumber, ranking, true);
placeCall(phoneNumber, null, ranking, true);
}
private void placeCall(String phoneNumber, int position, boolean isVideoCall) {
private void placeCall(String phoneNumber, String lookupKey, int position, boolean isVideoCall) {
CallSpecificAppData callSpecificAppData =
CallSpecificAppData.newBuilder()
.setCallInitiationType(callInitiationType)
......@@ -553,11 +554,15 @@ public final class NewSearchFragment extends Fragment
.setCharactersInSearchString(query == null ? 0 : query.length())
.setAllowAssistedDialing(true)
.build();
PreCall.start(
getContext(),
CallIntentBuilder callIntentBuilder = (
new CallIntentBuilder(phoneNumber, callSpecificAppData)
.setIsVideoCall(isVideoCall)
.setAllowAssistedDial(true));
if (lookupKey == null) {
PreCall.start(getContext(), callIntentBuilder);
} else {
PreCall.start(getActivity(), phoneNumber, callIntentBuilder, lookupKey);
}
FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).onCallPlacedFromSearch();
}
......
......@@ -72,6 +72,7 @@ final class SearchActionViewHolder extends RecyclerView.ViewHolder implements On
private @Action int action;
private int position;
private String query;
private String lookupKey;
SearchActionViewHolder(View view, RowClickListener listener) {
super(view);
......@@ -145,7 +146,7 @@ final class SearchActionViewHolder extends RecyclerView.ViewHolder implements On
break;
case Action.MAKE_VOICE_CALL:
listener.placeVoiceCall(query, position);
listener.placeVoiceCall(query, null, position);
break;
case Action.INVALID:
......
......@@ -44,6 +44,7 @@ public final class NearbyPlaceViewHolder extends RecyclerView.ViewHolder
private final RowClickListener listener;
private String number;
private String lookupKey;
private int position;
public NearbyPlaceViewHolder(View view, RowClickListener listener) {
......@@ -62,6 +63,7 @@ public final class NearbyPlaceViewHolder extends RecyclerView.ViewHolder
*/
public void bind(SearchCursor cursor, String query) {
number = cursor.getString(Projections.PHONE_NUMBER);
lookupKey = cursor.getString(Projections.LOOKUP_KEY);
position = cursor.getPosition();
String name = cursor.getString(Projections.DISPLAY_NAME);
String address = cursor.getString(Projections.PHONE_LABEL);
......@@ -95,6 +97,6 @@ public final class NearbyPlaceViewHolder extends RecyclerView.ViewHolder
@Override
public void onClick(View v) {
listener.placeVoiceCall(number, position);
listener.placeVoiceCall(number, lookupKey, position);
}
}
......@@ -100,9 +100,9 @@ public class ContextMenu extends PopupMenu implements OnMenuItemClickListener {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
if (menuItem.getItemId() == R.id.voice_call_container) {
listener.placeCall(Assert.isNotNull(voiceChannel));
listener.placeCall(Assert.isNotNull(voiceChannel), speedDialUiItem.lookupKey());
} else if (menuItem.getItemId() == R.id.video_call_container) {
listener.placeCall(Assert.isNotNull(videoChannel));
listener.placeCall(Assert.isNotNull(videoChannel), speedDialUiItem.lookupKey());
} else if (menuItem.getItemId() == R.id.send_message_container) {
listener.openSmsConversation(voiceChannel.number());
} else if (menuItem.getItemId() == R.id.remove_container) {
......@@ -125,7 +125,7 @@ public class ContextMenu extends PopupMenu implements OnMenuItemClickListener {
public interface ContextMenuItemListener {
/** Called when the user selects "voice call" or "video call" option from the context menu. */
void placeCall(Channel channel);
void placeCall(Channel channel, String lookupKey);
/** Called when the user selects "send message" from the context menu. */