diff options
author | Richard MacGregor <rmacgregor@cyngn.com> | 2016-01-22 14:38:50 -0800 |
---|---|---|
committer | Richard MacGregor <rmacgregor@cyngn.com> | 2016-04-08 08:53:14 -0700 |
commit | 91d03c80740f1451bc881a5750a57a2184ccc9e4 (patch) | |
tree | 923245d948475d35e1c4c15b2a0ddb53cc7efe19 /src | |
parent | d69ce6c2a65c3d451dfb5837678221e56fef1880 (diff) | |
download | packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.tar.gz packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.tar.bz2 packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.zip |
InCallAPI Handover M-bringup
Integrate Call handover to InCallAPI plugins
Integrate InCallAPI snackbar
Change-Id: I88897172dde3d01f33c94e24fa78da4048dcb75b
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/incallui/CallButtonFragment.java | 187 | ||||
-rw-r--r-- | src/com/android/incallui/CallButtonPresenter.java | 150 | ||||
-rw-r--r-- | src/com/android/incallui/ContactInfoCache.java | 120 | ||||
-rw-r--r-- | src/com/android/incallui/InCallActivity.java | 40 | ||||
-rw-r--r-- | src/com/android/incallui/incallapi/InCallPluginInfo.java | 127 | ||||
-rw-r--r-- | src/com/android/incallui/incallapi/InCallPluginInfoAsyncTask.java | 246 |
6 files changed, 842 insertions, 28 deletions
diff --git a/src/com/android/incallui/CallButtonFragment.java b/src/com/android/incallui/CallButtonFragment.java index 34eca3cc..8712637f 100644 --- a/src/com/android/incallui/CallButtonFragment.java +++ b/src/com/android/incallui/CallButtonFragment.java @@ -19,10 +19,14 @@ package com.android.incallui; import static com.android.incallui.CallButtonFragment.Buttons.*; import android.annotation.NonNull; +import android.app.AlertDialog; +import android.app.PendingIntent; import android.content.Context; +import android.content.DialogInterface; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.GradientDrawable; @@ -31,6 +35,7 @@ import android.graphics.drawable.StateListDrawable; import android.os.Bundle; import android.telecom.CallAudioState; import android.util.SparseIntArray; +import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; @@ -38,11 +43,21 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.CompoundButton; import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListAdapter; import android.widget.PopupMenu; import android.widget.PopupMenu.OnDismissListener; import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.TextView; + +import com.android.incallui.incallapi.InCallPluginInfo; + +import java.lang.Override; +import java.util.ArrayList; +import java.util.List; import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; @@ -53,6 +68,9 @@ public class CallButtonFragment extends BaseFragment<CallButtonPresenter, CallButtonPresenter.CallButtonUi> implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener, View.OnClickListener { + private static final String TAG = CallButtonFragment.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final int INVALID_INDEX = -1; private int mButtonMaxVisible; // The button is currently visible in the UI @@ -68,9 +86,9 @@ public class CallButtonFragment public static final int BUTTON_AUDIO = 0; public static final int BUTTON_MUTE = 1; public static final int BUTTON_DIALPAD = 2; - public static final int BUTTON_HOLD = 3; - public static final int BUTTON_SWAP = 4; - public static final int BUTTON_UPGRADE_TO_VIDEO = 5; + public static final int BUTTON_UPGRADE_TO_VIDEO = 3; + public static final int BUTTON_HOLD = 4; + public static final int BUTTON_SWAP = 5; public static final int BUTTON_SWITCH_CAMERA = 6; public static final int BUTTON_ADD_CALL = 7; public static final int BUTTON_MERGE = 8; @@ -225,7 +243,7 @@ public class CallButtonFragment getPresenter().addParticipantClicked(); break; case R.id.changeToVideoButton: - getPresenter().changeToVideoClicked(); + getPresenter().switchToVideoCall(); break; case R.id.switchCameraButton: getPresenter().switchCameraClicked( @@ -447,6 +465,61 @@ public class CallButtonFragment mSwitchCameraButton.setSelected(isBackFacingCamera); } + public void modifyChangeToVideoButton() { + boolean canVideoCall = getPresenter().canVideoCall(); + List<InCallPluginInfo> contactInCallPlugins = + getPresenter().getContactInCallPluginInfoList(); + int listSize = (contactInCallPlugins != null) ? contactInCallPlugins.size() : 0; + if (!canVideoCall && listSize == 1) { + InCallPluginInfo info = contactInCallPlugins.get(0); + if (info != null && info.getPluginSingleColorIcon() != null) { + LayerDrawable layerDrawable = + (LayerDrawable) getResources().getDrawable(R.drawable.btn_change_to_video) + .mutate(); + + int buttonWidth = mChangeToVideoButton.getWidth(); + int buttonHeight = mChangeToVideoButton.getWidth(); + if (buttonWidth == 0 || buttonHeight == 0) { + buttonWidth = + getResources().getDimensionPixelSize(R.dimen.in_call_button_dimension); + buttonHeight = + getResources().getDimensionPixelSize(R.dimen.in_call_button_dimension); + } + int xInset = buttonWidth - layerDrawable.getIntrinsicWidth(); + if (xInset > 0) { + xInset = xInset / 2; + } else { + xInset = 0; + } + int yInset = buttonHeight - layerDrawable.getIntrinsicHeight(); + if (yInset > 0) { + yInset = yInset / 2; + } else { + yInset = 0; + } + + if (DEBUG) { + Log.i(TAG, "mChangeToVideoButton: [w h] [" + mChangeToVideoButton.getWidth() + + " " + mChangeToVideoButton.getHeight() + "]"); + Log.i(TAG, "adjusted button: [w h] [" + buttonWidth + " " + buttonHeight + "]"); + Log.i(TAG, "layerDrawable: [w h] [" + layerDrawable.getIntrinsicWidth() + " " + + layerDrawable.getIntrinsicHeight() + "]"); + Log.i(TAG, "xInset = " + xInset); + Log.i(TAG, "xInset = " + yInset); + } + + Drawable icon = info.getPluginSingleColorIcon(); + icon.setTintList(getResources().getColorStateList(R.color.selectable_icon_tint)); + icon.setAutoMirrored(false); + + // layer 0 is background, layer 1 is the icon to use. + layerDrawable.setLayerInset(1, xInset, yInset, xInset, yInset); + layerDrawable.setDrawableByLayerId(R.id.foregroundItem, icon); + mChangeToVideoButton.setBackground(layerDrawable); + } + } + } + @Override public void setVideoPaused(boolean isPaused) { mPauseVideoButton.setSelected(isPaused); @@ -542,6 +615,69 @@ public class CallButtonFragment } } + /**The function is called when Video Call button gets pressed. The function creates and + * displays video call options. + */ + @Override + public void displayVideoCallOptions() { + CallButtonPresenter.CallButtonUi ui = getUi(); + if (ui == null) { + Log.e(this, "Cannot display VideoCallOptions as ui is null"); + return; + } + + Context context = getContext(); + + final ArrayList<Drawable> icons = new ArrayList<Drawable>(); + final ArrayList<String> items = new ArrayList<String>(); + final ArrayList<Integer> itemToCallType = new ArrayList<Integer>(); + final Resources res = ui.getContext().getResources(); + + // Prepare the string array and mapping. + List<InCallPluginInfo> contactInCallPlugins = + getPresenter().getContactInCallPluginInfoList(); + if (contactInCallPlugins != null && !contactInCallPlugins.isEmpty()) { + int i = 0; + for (InCallPluginInfo info : contactInCallPlugins) { + items.add(info.getPluginTitle()); + icons.add(info.getPluginColorIcon()); + itemToCallType.add(i++); + } + } + + boolean canVideoCall = getPresenter().canVideoCall(); + if (canVideoCall) { + // First item, if available is VT IMS call + items.add(res.getString(R.string.modify_call_option_vt)); + Drawable icon = res.getDrawable(R.drawable.ic_toolbar_video); + icon.setTint(res.getColor(R.color.vidoecall_handoff_default_video_call_color)); + icons.add(icon); + itemToCallType.add(-1); + } + + ListAdapter adapter = new ListItemWithImageArrayAdapter(context.getApplicationContext(), + R.layout.videocall_handoff_item, items, icons); + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + final int selCallType = itemToCallType.get(item); + if (selCallType < 0) { + // VT Call selected + getPresenter().changeToVideoClicked(); + } else { + // InCall Plugin selected + getPresenter().handoverCallToVoIPPlugin(selCallType); + } + dialog.dismiss(); + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder(getUi().getContext()); + builder.setTitle(R.string.video_call_option_title); + builder.setAdapter(adapter, listener); + final AlertDialog alert; + alert = builder.create(); + alert.show(); + } + @Override public void setAudio(int mode) { updateAudioButtons(getPresenter().getSupportedAudio()); @@ -871,4 +1007,47 @@ public class CallButtonFragment public Context getContext() { return getActivity(); } + + @Override + public void showInviteSnackbar(final PendingIntent inviteIntent, String inviteText) { + if (TextUtils.isEmpty(inviteText)) { + return; + } + final InCallActivity activity = (InCallActivity) getActivity(); + if (activity != null) { + activity.showInviteSnackbar(inviteIntent, inviteText); + } + } + + /** + * Adapter used to Array adapter with an icon and custom item layout + */ + private class ListItemWithImageArrayAdapter extends ArrayAdapter<String> { + private int mLayout; + private List<Drawable> mIcons; + + public ListItemWithImageArrayAdapter(Context context, int layout, List<String> titles, + List<Drawable> icons) { + super(context, 0, titles); + mLayout = layout; + mIcons = icons; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + String title = getItem(position); + Drawable icon = mIcons.get(position); + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(mLayout, parent, false); + } + + TextView textView = (TextView) convertView.findViewById(R.id.title); + textView.setText(title); + + ImageView msgIcon = (ImageView) convertView.findViewById(R.id.icon); + msgIcon.setImageDrawable(icon); + + return convertView; + } + } } diff --git a/src/com/android/incallui/CallButtonPresenter.java b/src/com/android/incallui/CallButtonPresenter.java index 1e9380d6..cd8c9415 100644 --- a/src/com/android/incallui/CallButtonPresenter.java +++ b/src/com/android/incallui/CallButtonPresenter.java @@ -19,18 +19,30 @@ package com.android.incallui; import static com.android.incallui.CallButtonFragment.Buttons.*; import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; +import android.content.CursorLoader; import android.content.DialogInterface; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ResultReceiver; import android.telecom.CallAudioState; import android.telecom.InCallService.VideoCall; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.VideoProfile; +import android.text.TextUtils; import com.android.incallui.AudioModeProvider.AudioModeListener; +import com.android.incallui.ContactInfoCache; +import com.android.incallui.ContactInfoCache.ContactCacheEntry; +import com.android.incallui.incallapi.InCallPluginInfo; import com.android.incallui.InCallCameraManager.Listener; import com.android.incallui.InCallPresenter.CanAddCallListener; import com.android.incallui.InCallPresenter.InCallState; @@ -38,6 +50,16 @@ import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.incallui.InCallPresenter.IncomingCallListener; import com.android.incallui.InCallPresenter.InCallDetailsListener; +import com.android.phone.common.ambient.AmbientConnection; +import com.android.phone.common.util.StartInCallCallReceiver; + +import com.cyanogen.ambient.common.api.AmbientApiClient; +import com.cyanogen.ambient.incall.InCallServices; +import com.cyanogen.ambient.incall.extension.OriginCodes; +import com.cyanogen.ambient.incall.extension.StatusCodes; +import com.cyanogen.ambient.incall.extension.StartCallRequest; + +import java.util.List; import java.util.Objects; /** @@ -45,16 +67,39 @@ import java.util.Objects; */ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButtonUi> implements InCallStateListener, AudioModeListener, IncomingCallListener, - InCallDetailsListener, CanAddCallListener, CallList.ActiveSubChangeListener, Listener { + InCallDetailsListener, CanAddCallListener, CallList.ActiveSubChangeListener, Listener, + StartInCallCallReceiver.Receiver { + private static final String TAG = CallButtonPresenter.class.getSimpleName(); private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted"; private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state"; private static final String RECORDING_WARNING_PRESENTED = "recording_warning_presented"; + private static final boolean DEBUG = false; private Call mCall; private boolean mAutomaticallyMuted = false; private boolean mPreviousMuteState = false; + private StartInCallCallReceiver mCallback; + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + if (DEBUG) Log.i(TAG, "Got InCallPlugin result callback code = " + resultCode); + + switch (resultCode) { + case StatusCodes.StartCall.HANDOVER_CONNECTED: + if (mCall == null) { + return; + } + + if (DEBUG) Log.i(TAG, "Disconnecting call: " + mCall); + TelecomAdapter.getInstance().disconnectCall(mCall.getId()); + break; + default: + Log.i(TAG, "Nothing to do for this InCallPlugin resultcode = " + resultCode); + } + } + public CallButtonPresenter() { } @@ -258,6 +303,77 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto InCallPresenter.getInstance().sendAddParticipantIntent(); } + public List<InCallPluginInfo> getContactInCallPluginInfoList() { + List<InCallPluginInfo> inCallPluginInfoList = null; + if (mCall != null) { + ContactCacheEntry contactInfo = + ContactInfoCache.getInstance(getUi().getContext()).getInfo(mCall.getId()); + if (contactInfo != null) { + inCallPluginInfoList = contactInfo.inCallPluginInfoList; + } + } + return inCallPluginInfoList; + } + + public void handoverCallToVoIPPlugin() { + handoverCallToVoIPPlugin(0); + } + + public void handoverCallToVoIPPlugin(int contactPluginIndex) { + List<InCallPluginInfo> inCallPluginInfoList = getContactInCallPluginInfoList(); + if (inCallPluginInfoList != null && inCallPluginInfoList.size() > contactPluginIndex) { + InCallPluginInfo info = inCallPluginInfoList.get(contactPluginIndex); + final ComponentName component = info.getPluginComponent(); + final String userId = info.getUserId(); + final String mimeType = info.getMimeType(); + if (component != null && !TextUtils.isEmpty(component.flattenToString()) && + !TextUtils.isEmpty(mimeType)) { + // Attempt call handover + final PendingIntent inviteIntent = info.getPluginInviteIntent(); + if (!TextUtils.isEmpty(userId)) { + AmbientApiClient client = AmbientConnection.CLIENT + .get(getUi().getContext().getApplicationContext()); + + mCallback = new StartInCallCallReceiver(new Handler(Looper.myLooper())); + mCallback.setReceiver(CallButtonPresenter.this); + StartCallRequest request = new StartCallRequest(userId, + OriginCodes.CALL_HANDOVER, + StartCallRequest.FLAG_CALL_TRANSFER, + mCallback); + + if (DEBUG) Log.i(TAG, "Starting InCallPlugin call for = " + userId); + InCallServices.getInstance().startVideoCall(client, component, request); + } else if (inviteIntent != null) { + // Attempt contact invite + if (DEBUG) { + final com.android.incallui.ContactInfoCache cache = + ContactInfoCache.getInstance(getUi().getContext()); + ContactCacheEntry entry = cache.getInfo(mCall.getId()); + Uri lookupUri = entry.lookupUri; + Log.i(TAG, "Attempting invite for " + lookupUri.toString()); + } + String inviteText = getUi().getContext().getApplicationContext() + .getString(R.string.snackbar_incall_plugin_contact_invite, + info.getPluginTitle()); + getUi().showInviteSnackbar(inviteIntent, inviteText); + } else { + // Inform user to add contact manually, no invite intent found + if (DEBUG) { + final com.android.incallui.ContactInfoCache cache = + ContactInfoCache.getInstance(getUi().getContext()); + ContactCacheEntry entry = cache.getInfo(mCall.getId()); + Uri lookupUri = entry.lookupUri; + Log.i(TAG, "No invite intent for " + lookupUri.toString()); + } + String inviteText = getUi().getContext().getApplicationContext() + .getString(R.string.snackbar_incall_plugin_no_invite_found, + info.getPluginTitle()); + getUi().showInviteSnackbar(null, inviteText); + } + } + } + } + public void addCallClicked() { // Automatically mute the current call mAutomaticallyMuted = true; @@ -303,6 +419,22 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); } + public void switchToVideoCall() { + boolean canVideoCall = canVideoCall(); + List<InCallPluginInfo> contactInCallPlugins = getContactInCallPluginInfoList(); + int listSize = (contactInCallPlugins != null) ? contactInCallPlugins.size() : 0; + if (canVideoCall && listSize == 0) { + // If only VT Call available + changeToVideoClicked(); + } else if (!canVideoCall && listSize == 1) { + // If only one InCall Plugin available + handoverCallToVoIPPlugin(); + } else if (canVideoCall || listSize > 0){ + // If multiple sources available + getUi().displayVideoCallOptions(); + } + } + /** * Switches the camera between the front-facing and back-facing camera. * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or @@ -433,6 +565,11 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto updateButtonsState(call); } + public boolean canVideoCall() { + return (mCall == null) ? false : (QtiCallUtils.hasVideoCapabilities(mCall) || + QtiCallUtils.hasVoiceCapabilities(mCall)); + } + /** * Updates the buttons applicable for the UI. * @@ -441,7 +578,6 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto private void updateButtonsState(Call call) { Log.v(this, "updateButtonsState"); final CallButtonUi ui = getUi(); - final boolean isVideo = CallUtils.isVideoCall(call); // Common functionality (audio, hold, etc). @@ -460,9 +596,11 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto final boolean showMerge = call.can( android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); final int callState = call.getState(); + List<InCallPluginInfo> contactInCallPlugins = getContactInCallPluginInfoList(); final boolean showUpgradeToVideo = (!isVideo || useExt) && (QtiCallUtils.hasVideoCapabilities(call) || - QtiCallUtils.hasVoiceCapabilities(call)) && + QtiCallUtils.hasVoiceCapabilities(call) || + (contactInCallPlugins != null && !contactInCallPlugins.isEmpty())) && (callState == Call.State.ACTIVE || callState == Call.State.ONHOLD); final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE); @@ -482,6 +620,9 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto ui.showButton(BUTTON_MUTE, showMute); ui.showButton(BUTTON_ADD_CALL, showAddCall); ui.showButton(BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); + if (showUpgradeToVideo) { + ui.modifyChangeToVideoButton(); + } ui.showButton(BUTTON_SWITCH_CAMERA, isVideo); ui.showButton(BUTTON_PAUSE_VIDEO, isVideo && !useExt); ui.showButton(BUTTON_DIALPAD, !isVideo || useExt); @@ -536,6 +677,9 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto void requestCallRecordingPermission(String[] permissions); void displayDialpad(boolean on, boolean animate); boolean isDialpadVisible(); + void modifyChangeToVideoButton(); + void displayVideoCallOptions(); + void showInviteSnackbar(PendingIntent inviteIntent, String inviteText); /** * Once showButton() has been called on each of the individual buttons in the UI, call diff --git a/src/com/android/incallui/ContactInfoCache.java b/src/com/android/incallui/ContactInfoCache.java index 0206e0a4..e94ea07b 100644 --- a/src/com/android/incallui/ContactInfoCache.java +++ b/src/com/android/incallui/ContactInfoCache.java @@ -16,6 +16,7 @@ package com.android.incallui; +import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -29,25 +30,37 @@ import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.telecom.TelecomManager; import android.text.TextUtils; - +import com.android.contacts.common.model.Contact; +import com.android.contacts.common.model.RawContact; import com.android.contacts.common.util.PhoneNumberHelper; +import com.android.contacts.common.util.UriUtils; import com.android.dialer.calllog.ContactInfo; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; +import com.android.incallui.incallapi.InCallPluginInfo; +import com.android.incallui.incallapi.InCallPluginInfoAsyncTask; import com.android.incallui.service.PhoneNumberService; import com.android.incalluibind.ObjectFactory; import com.android.services.telephony.common.MoreStrings; -import org.json.JSONException; -import org.json.JSONObject; - +import com.cyanogen.ambient.incall.extension.InCallContactInfo; +import com.cyanogen.ambient.incall.util.InCallHelper; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; import java.util.HashMap; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.json.JSONException; +import org.json.JSONObject; /** * Class responsible for querying Contact Information for Call objects. Can perform asynchronous @@ -63,8 +76,9 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete private final Context mContext; private final PhoneNumberService mPhoneNumberService; private final CachedNumberLookupService mCachedNumberLookupService; - private final HashMap<String, ContactCacheEntry> mInfoMap = Maps.newHashMap(); + private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap(); private final HashMap<String, Set<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap(); + private InCallPluginInfoAsyncTask mPluginInfoAsyncTask; private static ContactInfoCache sCache = null; @@ -221,6 +235,22 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete sendInfoNotifications(callId, cacheEntry); if (didLocalLookup) { + if (!callerInfo.isEmergencyNumber() && cacheEntry.inCallPluginInfoList == null && + (cacheEntry.lookupUri != null || !TextUtils.isEmpty(cacheEntry.number))) { + if (mPluginInfoAsyncTask != null) { + mPluginInfoAsyncTask.cancel(true); + mPluginInfoAsyncTask = null; + } + + final InCallPluginInfoAsyncTask.IInCallPostExecute callback = + new InCallPluginInfoCallback(callId); + final InCallContactInfo contactInfo = new InCallContactInfo(cacheEntry.name, + cacheEntry.number, cacheEntry.lookupUri); + mPluginInfoAsyncTask = + new InCallPluginInfoAsyncTask(mContext, contactInfo, callback); + mPluginInfoAsyncTask.execute(); + } + // Before issuing a request for more data from other services, we only check that the // contact wasn't found in the local DB. We don't check the if the cache entry already // has a name because we allow overriding cnap data with data from other services. @@ -278,23 +308,26 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete mContext.getResources(), type, label); entry.label = typeStr == null ? null : typeStr.toString(); } - final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); - if (oldEntry != null) { - // Location is only obtained from local lookup so persist - // the value for remote lookups. Once we have a name this - // field is no longer used; it is persisted here in case - // the UI is ever changed to use it. - entry.location = oldEntry.location; - } + synchronized (mInfoMap) { + final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); + if (oldEntry != null) { + // Location is only obtained from local lookup so persist + // the value for remote lookups. Once we have a name this + // field is no longer used; it is persisted here in case + // the UI is ever changed to use it. + entry.location = oldEntry.location; + entry.inCallPluginInfoList = oldEntry.inCallPluginInfoList; + } - // If no image and it's a business, switch to using the default business avatar. - if (info.getImageUrl() == null && info.isBusiness()) { - Log.d(TAG, "Business has no image. Using default."); - entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); - } + // If no image and it's a business, switch to using the default business avatar. + if (info.getImageUrl() == null && info.isBusiness()) { + Log.d(TAG, "Business has no image. Using default."); + entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); + } - // Add the contact info to the cache. - mInfoMap.put(mCallId, entry); + // Add the contact info to the cache. + mInfoMap.put(mCallId, entry); + } sendInfoNotifications(mCallId, entry); // If there is no image then we should not expect another callback. @@ -310,6 +343,34 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete } } + class InCallPluginInfoCallback implements InCallPluginInfoAsyncTask.IInCallPostExecute { + + private String mCallId; + + public InCallPluginInfoCallback(String callId) { + mCallId = callId; + } + + @Override + public void onPostExecuteTask(List<InCallPluginInfo> inCallPluginInfoList) { + // If we got a miss, return. + if (inCallPluginInfoList == null || inCallPluginInfoList.isEmpty()) { + Log.d(TAG, "No InCall plugins associated with this contact."); + return; + } + + synchronized (mInfoMap) { + final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); + ContactCacheEntry entry = new ContactCacheEntry(oldEntry); + entry.inCallPluginInfoList = inCallPluginInfoList; + + // Add the contact info to the cache. + mInfoMap.put(mCallId, entry); + sendInfoNotifications(mCallId, entry); + } + } + } + /** * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. * make sure that the call state is reflected after the image is loaded. @@ -587,6 +648,25 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete public Uri displayPhotoUri; public Uri lookupUri; // Sent to NotificationMananger public String lookupKey; + public List<InCallPluginInfo> inCallPluginInfoList; + + public ContactCacheEntry() {} + + public ContactCacheEntry(ContactCacheEntry entry) { + if (entry != null) { + this.name = entry.name; + this.number = entry.number; + this.location = entry.location; + this.label = entry.label; + this.photo = entry.photo; + this.isSipCall = entry.isSipCall; + this.contactUri = entry.contactUri; + this.displayPhotoUri = entry.displayPhotoUri; + this.lookupUri = entry.lookupUri; + this.lookupKey = entry.lookupKey; + this.inCallPluginInfoList = entry.inCallPluginInfoList; + } + } @Override public String toString() { diff --git a/src/com/android/incallui/InCallActivity.java b/src/com/android/incallui/InCallActivity.java index a9e31b58..3daa7dcd 100644 --- a/src/com/android/incallui/InCallActivity.java +++ b/src/com/android/incallui/InCallActivity.java @@ -17,7 +17,6 @@ package com.android.incallui; import android.app.ActionBar; -import android.app.FragmentTransaction; import android.app.ActionBar.Tab; import android.app.Activity; import android.app.ActivityManager; @@ -26,6 +25,8 @@ import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -37,6 +38,7 @@ import android.graphics.Point; import android.hardware.SensorManager; import android.os.Bundle; import android.os.Trace; +import android.support.design.widget.Snackbar; import android.telecom.DisconnectCause; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; @@ -71,6 +73,7 @@ import java.util.Locale; public class InCallActivity extends Activity implements FragmentDisplayManager { public static final String TAG = InCallActivity.class.getSimpleName(); + public static final boolean DEBUG = false; public static final String SHOW_DIALPAD_EXTRA = "InCallActivity.show_dialpad"; public static final String DIALPAD_TEXT_EXTRA = "InCallActivity.dialpad_text"; @@ -86,6 +89,8 @@ public class InCallActivity extends Activity implements FragmentDisplayManager { private static final int DIALPAD_REQUEST_SHOW = 2; private static final int DIALPAD_REQUEST_HIDE = 3; + private static final int SNACKBAR_TIMEOUT = 10000; // 10 seconds auto dismiss + private CallButtonFragment mCallButtonFragment; private CallCardFragment mCallCardFragment; private AnswerFragment mAnswerFragment; @@ -96,6 +101,7 @@ public class InCallActivity extends Activity implements FragmentDisplayManager { private boolean mIsVisible; private AlertDialog mDialog; private InCallOrientationEventListener mInCallOrientationEventListener; + private Snackbar mInviteSnackbar; /** * Used to indicate whether the dialpad should be hidden or shown {@link #onResume}. @@ -310,6 +316,11 @@ public class InCallActivity extends Activity implements FragmentDisplayManager { @Override protected void onPause() { Log.d(this, "onPause()..."); + + if (mInviteSnackbar != null) { + mInviteSnackbar.dismiss(); + } + if (mDialpadFragment != null ) { mDialpadFragment.onDialerKeyUp(null); } @@ -743,6 +754,9 @@ public class InCallActivity extends Activity implements FragmentDisplayManager { if ((show && isDialpadVisible()) || (!show && !isDialpadVisible())) { return; } + if (mInviteSnackbar != null) { + mInviteSnackbar.dismiss(); + } // We don't do a FragmentTransaction on the hide case because it will be dealt with when // the listener is fired after an animation finishes. if (!animate) { @@ -1014,4 +1028,28 @@ public class InCallActivity extends Activity implements FragmentDisplayManager { mInCallOrientationEventListener.disable(); } } + + public void showInviteSnackbar(final PendingIntent inviteIntent, String inviteText) { + final View rootView = getCallCardFragment().getView(); + if (rootView.getVisibility() != View.VISIBLE || TextUtils.isEmpty(inviteText)) { + return; + } + mInviteSnackbar = Snackbar.make(rootView, inviteText, SNACKBAR_TIMEOUT); + if (inviteIntent != null) { + mInviteSnackbar.setActionTextColor(getResources() + .getColor(R.color.snackbar_action_text_color)) + .setAction(R.string.snackbar_invite_action_text, new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + inviteIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Caught CanceledException from InCall Plugin invite" + + " intent", e); + } + } + }); + } + mInviteSnackbar.show(); + } } diff --git a/src/com/android/incallui/incallapi/InCallPluginInfo.java b/src/com/android/incallui/incallapi/InCallPluginInfo.java new file mode 100644 index 00000000..e6b809af --- /dev/null +++ b/src/com/android/incallui/incallapi/InCallPluginInfo.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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.incallui.incallapi; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.graphics.drawable.Drawable; + +public class InCallPluginInfo { + private ComponentName mPluginComponent; + private String mPluginTitle; + private String mUserId; + private String mMimeType; + private Drawable mPluginColorIcon; + private Drawable mPluginSingleColorIcon; + private PendingIntent mInviteIntent; + + private InCallPluginInfo() { + } + + public ComponentName getPluginComponent() { + return mPluginComponent; + } + + public String getPluginTitle() { + return mPluginTitle; + } + + public String getUserId() { + return mUserId; + } + + public String getMimeType() { + return mMimeType; + } + + public Drawable getPluginColorIcon() { + return mPluginColorIcon; + } + + public Drawable getPluginSingleColorIcon() { + return mPluginSingleColorIcon; + } + + public PendingIntent getPluginInviteIntent() { + return mInviteIntent; + } + + public static class Builder { + private ComponentName mPluginComponent; + private String mPluginTitle; + private String mUserId; + private String mMimeType; + private Drawable mPluginColorIcon; + private Drawable mPluginSingleColorIcon; + private PendingIntent mInviteIntent; + + public Builder() { + } + + public Builder setPluginComponent(ComponentName pluginComponent) { + this.mPluginComponent = pluginComponent; + return this; + } + + public Builder setPluginTitle(String pluginTitle) { + this.mPluginTitle = pluginTitle; + return this; + } + + public Builder setUserId(String userId) { + this.mUserId = userId; + return this; + } + + public Builder setMimeType(String mimeType) { + this.mMimeType = mimeType; + return this; + } + + public Builder setPluginColorIcon(Drawable pluginColorIcon) { + this.mPluginColorIcon = pluginColorIcon; + return this; + } + + public Builder setPluginSingleColorIcon(Drawable pluginSingleColorIcon) { + this.mPluginSingleColorIcon = pluginSingleColorIcon; + return this; + } + + public Builder setPluginInviteIntent(PendingIntent pluginInviteIntent) { + this.mInviteIntent = pluginInviteIntent; + return this; + } + + // TODO: Check if we want to require an invite intent or not + public InCallPluginInfo build() throws IllegalStateException{ + if (mPluginComponent == null || mPluginTitle == null || mMimeType == null + || mPluginColorIcon == null || mPluginSingleColorIcon == null) { + throw new IllegalStateException(); + } + InCallPluginInfo info = new InCallPluginInfo(); + info.mPluginComponent = mPluginComponent; + info.mPluginTitle = mPluginTitle; + info.mUserId = mUserId; + info.mMimeType = mMimeType; + info.mPluginColorIcon = mPluginColorIcon; + info.mPluginSingleColorIcon = mPluginSingleColorIcon; + info.mInviteIntent = mInviteIntent; + return info; + } + } +}
\ No newline at end of file diff --git a/src/com/android/incallui/incallapi/InCallPluginInfoAsyncTask.java b/src/com/android/incallui/incallapi/InCallPluginInfoAsyncTask.java new file mode 100644 index 00000000..aa22d972 --- /dev/null +++ b/src/com/android/incallui/incallapi/InCallPluginInfoAsyncTask.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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.incallui.incallapi; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.text.TextUtils; +import android.util.Log; +import com.android.phone.common.ambient.AmbientConnection; +import com.cyanogen.ambient.common.api.AmbientApiClient; +import com.cyanogen.ambient.incall.InCallApi; +import com.cyanogen.ambient.incall.InCallServices; +import com.cyanogen.ambient.incall.extension.InCallContactInfo; +import com.cyanogen.ambient.incall.results.MimeTypeListResult; +import com.cyanogen.ambient.incall.results.PendingIntentResult; +import com.cyanogen.ambient.incall.results.PluginStatusResult; +import com.cyanogen.ambient.incall.results.InCallProviderInfoResult; +import com.cyanogen.ambient.incall.results.MimeTypeResult; +import com.cyanogen.ambient.plugin.PluginStatus; +import com.google.common.base.Joiner; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implements a Loader class to asynchronously load InCall plugin Info. + */ +public class InCallPluginInfoAsyncTask extends AsyncTask<Void, Void, List<InCallPluginInfo>> { + private static final String TAG = InCallPluginInfoAsyncTask.class.getSimpleName(); + private static final boolean DEBUG = false; + private final Context mContext; + private InCallContactInfo mContactInfo; + private WeakReference<IInCallPostExecute> mPostExecute; + + public interface IInCallPostExecute { + void onPostExecuteTask(List<InCallPluginInfo> inCallPluginInfoList); + } + + private static final String[] CONTACT_PROJECTION = new String[] { + Phone.NUMBER, // 0 + Phone.MIMETYPE, // 1 + }; + + public InCallPluginInfoAsyncTask(Context context, InCallContactInfo contactInfo, + IInCallPostExecute postExecute) { + mContext = context.getApplicationContext(); + mContactInfo = contactInfo; + mPostExecute = new WeakReference<IInCallPostExecute>(postExecute); + } + + /** + * Loads the CallMethods in background. + * @return List of available (authenticated and enabled) incall plugins associated with the + * specified contact. + */ + @Override + protected List<InCallPluginInfo> doInBackground(Void... params) { + List<InCallPluginInfo> inCallPluginList = new ArrayList<InCallPluginInfo>(); + List<InCallPluginInfo.Builder> inCallPluginInfoBuilderList = + new ArrayList<InCallPluginInfo.Builder>(); + Map<String, Integer> pluginIndex = new HashMap<String, Integer>(); + AmbientApiClient client = AmbientConnection.CLIENT.get(mContext.getApplicationContext()); + InCallApi inCallServices = InCallServices.getInstance(); + List<ComponentName> plugins = inCallServices.getInstalledPlugins(client).await().components; + MimeTypeListResult mimeTypeListResult = + inCallServices.getVideoCallableMimeTypeList(client).await(); + + if (mContactInfo == null) { + return inCallPluginList; + } + + if (mContactInfo.mLookupUri != null && + !TextUtils.isEmpty(mContactInfo.mLookupUri.toString())) { + // Query contact info with these mimetypes + final Uri queryUri; + final String inputUriAsString = mContactInfo.mLookupUri.toString(); + if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) { + if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) { + queryUri = Uri.withAppendedPath(mContactInfo.mLookupUri, + Contacts.Data.CONTENT_DIRECTORY); + } else { + queryUri = mContactInfo.mLookupUri; + } + } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) { + queryUri = mContactInfo.mLookupUri; + } else { + throw new UnsupportedOperationException( + "Input Uri must be contact Uri or data Uri (input: \"" + + mContactInfo.mLookupUri + "\")"); + } + + if (mimeTypeListResult != null && mimeTypeListResult.mimeTypeList != null && + !mimeTypeListResult.mimeTypeList.isEmpty() && queryUri != null) { + Cursor cursor = mContext.getContentResolver().query( + queryUri, + CONTACT_PROJECTION, + constructSelection(mimeTypeListResult.mimeTypeList), + null, + null); + if (cursor != null) { + try { + final Context context = mContext; + while (cursor.moveToNext()) { + int cursorIndex = cursor.getColumnIndex(Phone.NUMBER); + final String id = cursorIndex == -1 ? + null : cursor.getString(cursorIndex); + cursorIndex = cursor.getColumnIndex(Phone.MIMETYPE); + final String mimeType = + cursorIndex == -1 ? null : cursor.getString(cursorIndex); + InCallPluginInfo.Builder infoBuilder = new InCallPluginInfo.Builder() + .setUserId(id).setMimeType(mimeType); + inCallPluginInfoBuilderList.add(infoBuilder); + pluginIndex.put(mimeType, inCallPluginInfoBuilderList.size() - 1); + } + } finally { + cursor.close(); + } + } + } else { + if (DEBUG) Log.i("InCall", "No InCall plugins found with video callable mimetypes"); + return null; + } + } + + // Fill in plugin Info. + if (plugins != null && !plugins.isEmpty()) { + PackageManager packageManager = mContext.getPackageManager(); + for (ComponentName component : plugins) { + PluginStatusResult statusResult = + inCallServices.getPluginStatus(client, component).await(); + MimeTypeResult mimeTypeResult = + inCallServices.getVideoCallableMimeType(client, component).await(); + + if (statusResult.status != PluginStatus.ENABLED) { + // Contact does not have account with this plugin OR plugin is not enabled. + if (DEBUG) { + Log.d(TAG, "Contact does not have account with this plugin OR plugin is not" + + " enabled. Component=" + component.flattenToString()); + } + continue; + } + + if (!pluginIndex.containsKey(mimeTypeResult.mimeType)) { + if (DEBUG) { + Log.d(TAG, "Contact does not have account with this plugin, looking up" + + " invite for Component=" + component.flattenToString() + " and Uri=" + + mContactInfo.mLookupUri.toString()); + } + PendingIntentResult inviteResult = + inCallServices.getInviteIntent(client, component, mContactInfo) + .await(); + InCallPluginInfo.Builder infoBuilder = + new InCallPluginInfo.Builder().setUserId(null) + .setMimeType(mimeTypeResult.mimeType) + .setPluginInviteIntent(inviteResult == null ? + null : inviteResult.intent); + inCallPluginInfoBuilderList.add(infoBuilder); + pluginIndex.put(mimeTypeResult.mimeType, + inCallPluginInfoBuilderList.size() - 1); + } + + Resources pluginResources = null; + try { + pluginResources = packageManager.getResourcesForApplication( + component.getPackageName()); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Plugin isn't installed: " + component); + continue; + } + + InCallProviderInfoResult providerInfo = + inCallServices.getProviderInfo(client, component).await(); + if (providerInfo != null && providerInfo.inCallProviderInfo != null && + pluginResources != null) { + int index = pluginIndex.get(mimeTypeResult.mimeType); + InCallPluginInfo.Builder infoBuilder = inCallPluginInfoBuilderList.get(index); + infoBuilder.setPluginComponent(component) + .setPluginTitle(providerInfo.inCallProviderInfo.getTitle()); + + try { + infoBuilder.setPluginColorIcon(pluginResources.getDrawable( + providerInfo.inCallProviderInfo.getBrandIcon(), null)) + .setPluginSingleColorIcon(pluginResources.getDrawable( + providerInfo.inCallProviderInfo.getSingleColorBrandIcon(), + null)); + + inCallPluginList.add(infoBuilder.build()); + } catch (Resources.NotFoundException e) { + Log.e(TAG, "Unable to retrieve icon for plugin: " + component); + continue; + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to build InCallPluginInfo object."); + continue; + } + } + } + } + return inCallPluginList; + } + + @Override + protected void onPostExecute(List<InCallPluginInfo> inCallPluginInfoList) { + if (mPostExecute != null) { + final IInCallPostExecute postExecute = mPostExecute.get(); + if (postExecute != null) { + postExecute.onPostExecuteTask(inCallPluginInfoList); + } + } + } + + private String constructSelection(List<String> mimeTypesList) { + StringBuilder selection = new StringBuilder(); + if (mimeTypesList != null && !mimeTypesList.isEmpty()) { + selection.append(Data.MIMETYPE + " IN ('"); + selection.append(Joiner.on("', '").skipNulls().join(mimeTypesList)); + selection.append("') AND " + Data.DATA1 + " NOT NULL"); + } + return selection.toString(); + } +} |