summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard MacGregor <rmacgregor@cyngn.com>2016-01-22 14:38:50 -0800
committerRichard MacGregor <rmacgregor@cyngn.com>2016-04-08 08:53:14 -0700
commit91d03c80740f1451bc881a5750a57a2184ccc9e4 (patch)
tree923245d948475d35e1c4c15b2a0ddb53cc7efe19
parentd69ce6c2a65c3d451dfb5837678221e56fef1880 (diff)
downloadandroid_packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.tar.gz
android_packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.tar.bz2
android_packages_apps_InCallUI-91d03c80740f1451bc881a5750a57a2184ccc9e4.zip
InCallAPI Handover M-bringup
Integrate Call handover to InCallAPI plugins Integrate InCallAPI snackbar Change-Id: I88897172dde3d01f33c94e24fa78da4048dcb75b
-rw-r--r--res/drawable/btn_change_to_video.xml2
-rw-r--r--res/layout/videocall_handoff_item.xml41
-rw-r--r--res/values/cm_colors.xml23
-rw-r--r--res/values/cm_dimens.xml27
-rw-r--r--res/values/cm_strings.xml7
-rw-r--r--res/values/styles.xml2
-rw-r--r--src/com/android/incallui/CallButtonFragment.java187
-rw-r--r--src/com/android/incallui/CallButtonPresenter.java150
-rw-r--r--src/com/android/incallui/ContactInfoCache.java120
-rw-r--r--src/com/android/incallui/InCallActivity.java40
-rw-r--r--src/com/android/incallui/incallapi/InCallPluginInfo.java127
-rw-r--r--src/com/android/incallui/incallapi/InCallPluginInfoAsyncTask.java246
12 files changed, 942 insertions, 30 deletions
diff --git a/res/drawable/btn_change_to_video.xml b/res/drawable/btn_change_to_video.xml
index a26cee3e..8c353b9e 100644
--- a/res/drawable/btn_change_to_video.xml
+++ b/res/drawable/btn_change_to_video.xml
@@ -21,7 +21,7 @@
<item android:id="@+id/backgroundItem"
android:drawable="@drawable/btn_background" />
- <item>
+ <item android:id="@+id/foregroundItem">
<bitmap android:src="@drawable/ic_toolbar_video"
android:gravity="center"
android:tint="@color/selectable_icon_tint"
diff --git a/res/layout/videocall_handoff_item.xml b/res/layout/videocall_handoff_item.xml
new file mode 100644
index 00000000..d4f4ac96
--- /dev/null
+++ b/res/layout/videocall_handoff_item.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/videocall_handoff_item_height">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/videocall_handoff_item_icon_width"
+ android:layout_height="@dimen/videocall_handoff_item_icon_height"
+ android:layout_marginStart="@dimen/videocall_handoff_item_start_margin"
+ android:layout_marginEnd="@dimen/videocall_handoff_item_icon_end_margin"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/videocall_handoff_item_end_margin"
+ android:textColor="@color/videocall_handoff_item_text_color"
+ android:textSize="@dimen/videocall_handoff_item_text_size"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_gravity="center_vertical" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values/cm_colors.xml b/res/values/cm_colors.xml
new file mode 100644
index 00000000..e85edd38
--- /dev/null
+++ b/res/values/cm_colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+
+<resources>
+ <color name="videocall_handoff_item_text_color">#de000000</color>
+ <color name="vidoecall_handoff_default_video_call_color">#8a000000</color>
+
+ <color name="snackbar_action_text_color">@color/dialer_theme_color</color>
+</resources> \ No newline at end of file
diff --git a/res/values/cm_dimens.xml b/res/values/cm_dimens.xml
new file mode 100644
index 00000000..8557fd67
--- /dev/null
+++ b/res/values/cm_dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+
+<resources>
+ <!-- Video call handoff list -->
+ <dimen name="videocall_handoff_item_height">48dp</dimen>
+ <dimen name="videocall_handoff_item_icon_width">24dp</dimen>
+ <dimen name="videocall_handoff_item_icon_height">24dp</dimen>
+ <dimen name="videocall_handoff_item_start_margin">23dp</dimen>
+ <dimen name="videocall_handoff_item_icon_end_margin">17dp</dimen>
+ <dimen name="videocall_handoff_item_end_margin">23dp</dimen>
+ <dimen name="videocall_handoff_item_text_size">18sp</dimen>
+</resources>
diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml
index 29c74c16..7d553cd3 100644
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -44,4 +44,11 @@
<string name="recording_warning_title">Enable call recording?</string>
<string name="recording_warning_text">Notice: You are responsible for compliance with any laws, regulations and rules that apply to the use of call recording functionality and the use or distribution of those recordings.</string>
<string name="onscreenTransferCall">Transfer call</string>
+
+ <!-- In-call screen: Video Call Options for VT or plugin handoff -->
+ <string name="video_call_option_title">Select video service</string>
+
+ <string name="snackbar_incall_plugin_no_invite_found">This contact is not a <xliff:g id="incall_plugin">%s</xliff:g> contact. You must add this person first to make video calls.</string>
+ <string name="snackbar_incall_plugin_contact_invite">This contact is not a <xliff:g id="incall_plugin">%s</xliff:g> contact. Invite this contact?</string>
+ <string name="snackbar_invite_action_text">INVITE</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index bcd0e70a..0935f332 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -74,7 +74,7 @@
circular reveal animation for a new outgoing call to work correctly. We don't just use
Theme.Black.NoTitleBar directly, since we want any popups or dialogs from the
InCallActivity to have the correct Material style. -->
- <style name="Theme.InCallScreen" parent="@android:style/Theme.Material.Light">
+ <style name="Theme.InCallScreen" parent="style/Theme.AppCompat.Light">
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
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();
+ }
+}