summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorErica Chang <echang@cyngn.com>2016-01-21 19:02:40 -0800
committerErica Chang <echang@cyngn.com>2016-04-07 12:05:10 -0700
commita7f0f11091c69868142597f972cc22cd95e27a76 (patch)
tree5cfd4766564a5b630453fd4ba46bea3371cc8aac
parent1e2ad0157e708d06728ef575aa556c1e0455d278 (diff)
downloadpackages_apps_Contacts-a7f0f11091c69868142597f972cc22cd95e27a76.tar.gz
packages_apps_Contacts-a7f0f11091c69868142597f972cc22cd95e27a76.tar.bz2
packages_apps_Contacts-a7f0f11091c69868142597f972cc22cd95e27a76.zip
Contacts: incall plugin implementation
Contacts Card: fixed encoded contact Uri invite intent -When a contacts card is launched from a dialed phone number history, the particular contact does not exist in contacts so the uri is encoded. In this case we'll provide either the display name or phone number as the Uri argument for getInvitePendingIntent. CD-346 Contacts Card: fix callog -added in cm-12.1 calllog related changes that accidentally didn't get merged in CD-354 Contacts Card: removed redundant code from cm-13.0 creation -in Quick contact card onclick handler there's redundant code since cm-13.0 was created Contacts Card: fix multiple ContacLoader calls issue -consodliate one AsynTask to load contact data -added a synchronized method checkAndBindContactData to fix contact card update condition Note. The internal implementation of AsyncTask.doInBackground is done on FutureTask; onPostExecute run on a main loop via Message. If the onPostExecute is already placed on the main loop, it can no longer be cancelled. cancel() is the most effective while an AsyncTask is still running doInBackground, upon completion, it'd either execute onCancelled() or onPostExecute() CD-324 Contacts: lookup plugin account handles as a backup -Need to rely on AccountManager to lookup logged in plugin account handles just in case the plugin fails to return a valid one CD-422 (2/2) Contacts: refactor -makes use of PhoneCommon/CallMethodInfo.java, CallMethodHelper -new InCallPluginHelper extends CallMethodHelper -InCallPluginUtils uses CallMethodHelper singleton and StartInCallCallReceiver CD-301,CD-374,CD-423 Change-Id: Iaa6f3f4539969a8cdad7c719f71ffaad8dd8cc39
-rw-r--r--Android.mk8
-rw-r--r--AndroidManifest.xml5
-rw-r--r--res/drawable/ic_add.xml19
-rw-r--r--res/drawable/ic_close.xml9
-rw-r--r--res/layout/expanding_entry_card_item.xml56
-rw-r--r--res/layout/people_activity.xml12
-rw-r--r--res/values/cm_strings.xml5
-rw-r--r--src/com/android/contacts/ContactsApplication.java4
-rw-r--r--src/com/android/contacts/StartCallResultReceiver.java42
-rw-r--r--src/com/android/contacts/activities/ActionBarAdapter.java39
-rw-r--r--src/com/android/contacts/activities/ContactSelectionActivity.java5
-rw-r--r--src/com/android/contacts/activities/MultiPickContactActivity.java3
-rw-r--r--src/com/android/contacts/activities/PeopleActivity.java541
-rw-r--r--src/com/android/contacts/group/GroupDetailFragment.java2
-rw-r--r--src/com/android/contacts/incall/InCallPluginHelper.java111
-rw-r--r--src/com/android/contacts/incall/InCallPluginInfo.java102
-rw-r--r--src/com/android/contacts/incall/InCallPluginUtils.java201
-rw-r--r--src/com/android/contacts/interactions/CallLogInteraction.java62
-rw-r--r--src/com/android/contacts/interactions/CallLogInteractionsLoader.java68
-rw-r--r--src/com/android/contacts/list/ContactTileListFragment.java2
-rw-r--r--src/com/android/contacts/list/PluginContactBrowseListFragment.java765
-rw-r--r--src/com/android/contacts/quickcontact/ExpandingEntryCardView.java162
-rw-r--r--src/com/android/contacts/quickcontact/QuickContactActivity.java563
23 files changed, 2528 insertions, 258 deletions
diff --git a/Android.mk b/Android.mk
index b9bcf90c3..3d19b64c1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -6,7 +6,10 @@ LOCAL_MODULE_TAGS := optional
contacts_common_dir := ../ContactsCommon
phone_common_dir := ../PhoneCommon
-src_dirs := src $(contacts_common_dir)/src $(phone_common_dir)/src
+src_dirs := src $(contacts_common_dir)/src \
+ $(phone_common_dir)/src \
+ $(phone_common_dir)/src-ambient
+
res_dirs := res $(contacts_common_dir)/res $(phone_common_dir)/res
LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
@@ -17,7 +20,8 @@ LOCAL_AAPT_FLAGS := \
--auto-add-overlay \
--extra-packages com.android.contacts.common \
--extra-packages com.android.phone.common \
- --extra-packages android.support.v7.cardview
+ --extra-packages android.support.v7.cardview \
+ --extra-packages com.cyanogen.ambient
LOCAL_JAVA_LIBRARIES := telephony-common voip-common
LOCAL_STATIC_JAVA_LIBRARIES := \
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 319857c04..c5c4a5dbe 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,6 +49,11 @@
<uses-permission android:name="android.permission.READ_PHONE_BLACKLIST" />
<uses-permission android:name="android.permission.CHANGE_PHONE_BLACKLIST" />
+ <!-- Receive plugin status changes -->
+ <uses-permission android:name="com.cyanogen.ambient.permission.PLUGIN_STATUS_CHANGED" />
+ <!-- Connect to AmbientCore to use InCall Plugins -->
+ <uses-permission android:name="com.cyanogen.ambient.permission.BIND_INCALL_SERVICE" />
+
<application
android:name="com.android.contacts.ContactsApplication"
android:label="@string/applicationLabel"
diff --git a/res/drawable/ic_add.xml b/res/drawable/ic_add.xml
new file mode 100644
index 000000000..2d64346f8
--- /dev/null
+++ b/res/drawable/ic_add.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="15dp"
+ android:height="14dp"
+ android:viewportWidth="15"
+ android:viewportHeight="14">
+
+ <group
+ android:translateX="-210.000000"
+ android:translateY="-762.000000">
+ <group
+ android:translateX="210.500000"
+ android:translateY="762.000000">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M14,6 L8,6 L8,0 L6,0 L6,6 L0,6 L0,8 L6,8 L6,14 L8,14 L8,8 L14,8 L14,6 L14,6 Z" />
+ </group>
+ </group>
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml
new file mode 100644
index 000000000..fdbfc4287
--- /dev/null
+++ b/res/drawable/ic_close.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"
+ android:fillColor="#9e9e9e"/>
+</vector>
diff --git a/res/layout/expanding_entry_card_item.xml b/res/layout/expanding_entry_card_item.xml
index 20e90ebf9..17239d39b 100644
--- a/res/layout/expanding_entry_card_item.xml
+++ b/res/layout/expanding_entry_card_item.xml
@@ -85,28 +85,38 @@
android:layout_marginTop="@dimen/expanding_entry_card_item_text_icon_margin_top"
android:layout_marginEnd="@dimen/expanding_entry_card_item_text_icon_margin_right" />
- <ImageView
- android:id="@+id/icon_alternate"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toStartOf="@+id/third_icon"
- android:layout_alignWithParentIfMissing="true"
- android:visibility="gone"
- android:background="?android:attr/selectableItemBackgroundBorderless"
- android:paddingTop="@dimen/expanding_entry_card_item_icon_margin_top"
- android:paddingBottom="@dimen/expanding_entry_card_item_alternate_icon_margin_bottom"
- android:layout_marginStart="@dimen/expanding_entry_card_item_alternate_icon_start_margin" />
+ <ImageView
+ android:id="@+id/icon_alternate"
+ android:layout_width="@dimen/expanding_entry_card_item_icon_width"
+ android:layout_height="@dimen/expanding_entry_card_item_icon_height"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@+id/third_icon"
+ android:layout_alignWithParentIfMissing="true"
+ android:visibility="gone"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:layout_marginTop="@dimen/expanding_entry_card_item_icon_margin_top"
+ android:layout_marginBottom="@dimen/expanding_entry_card_item_alternate_icon_margin_bottom"
+ android:layout_marginStart="@dimen/expanding_entry_card_item_alternate_icon_start_margin" />
+
+ <ImageView
+ android:id="@+id/third_icon"
+ android:layout_width="@dimen/expanding_entry_card_item_icon_width"
+ android:layout_height="@dimen/expanding_entry_card_item_icon_height"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:visibility="gone"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:layout_marginTop="@dimen/expanding_entry_card_item_icon_margin_top"
+ android:layout_marginBottom="@dimen/expanding_entry_card_item_alternate_icon_margin_bottom"
+ android:layout_marginStart="@dimen/expanding_entry_card_item_alternate_icon_start_margin" />
- <ImageView
- android:id="@+id/third_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentTop="true"
- android:visibility="gone"
- android:background="?android:attr/selectableItemBackgroundBorderless"
- android:paddingTop="@dimen/expanding_entry_card_item_icon_margin_top"
- android:paddingBottom="@dimen/expanding_entry_card_item_alternate_icon_margin_bottom"
- android:layout_marginStart="@dimen/expanding_entry_card_item_alternate_icon_start_margin" />
+ <TextView
+ android:id="@+id/third_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="@dimen/expanding_entry_card_item_alternate_icon_start_margin"
+ style="@android:style/TextAppearance.Material.Body2"
+ android:visibility="gone" />
</view>
diff --git a/res/layout/people_activity.xml b/res/layout/people_activity.xml
index ce995cb3d..6b571dfa5 100644
--- a/res/layout/people_activity.xml
+++ b/res/layout/people_activity.xml
@@ -37,17 +37,5 @@
android:layout_below="@id/toolbar_parent"
/>
- <FrameLayout
- android:id="@+id/contacts_unavailable_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_below="@id/toolbar_parent"
- android:visibility="gone">
- <FrameLayout
- android:id="@+id/contacts_unavailable_container"
- android:layout_height="match_parent"
- android:layout_width="match_parent" />
- </FrameLayout>
-
<include layout="@layout/floating_action_button" />
</RelativeLayout>
diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml
index 21ecba589..dc99dfa4d 100644
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -73,4 +73,9 @@
<string name="powered_by_provider">Powered by <xliff:g id="provider">%s</xliff:g></string>
+ <!-- InCall plugin directory search & invite -->
+ <string name="incall_plugin_directory_search">Search %1$s Directory</string>
+ <string name="incall_plugin_invite">INVITE</string>
+ <string name="incall_plugin_account_subheader">%1$s name</string>
+ <string name="incall_plugin_call_error"><xliff:g id="name">%s</xliff:g> cannot make this call</string>
</resources>
diff --git a/src/com/android/contacts/ContactsApplication.java b/src/com/android/contacts/ContactsApplication.java
index 798614c74..74fb9e5be 100644
--- a/src/com/android/contacts/ContactsApplication.java
+++ b/src/com/android/contacts/ContactsApplication.java
@@ -24,6 +24,7 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
+import android.os.Bundle;
import android.os.StrictMode;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.Contacts;
@@ -35,6 +36,8 @@ import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.testing.InjectedServices;
import com.android.contacts.common.util.Constants;
import com.android.contacts.commonbind.analytics.AnalyticsUtil;
+import com.android.contacts.incall.InCallPluginHelper;
+import com.android.phone.common.incall.CallMethodHelper;
import com.google.common.annotations.VisibleForTesting;
@@ -123,6 +126,7 @@ public final class ContactsApplication extends Application {
}
AnalyticsUtil.initialize(this);
+ InCallPluginHelper.init(this);
}
private class DelayedInitializer extends AsyncTask<Void, Void, Void> {
diff --git a/src/com/android/contacts/StartCallResultReceiver.java b/src/com/android/contacts/StartCallResultReceiver.java
new file mode 100644
index 000000000..f1bf664b5
--- /dev/null
+++ b/src/com/android/contacts/StartCallResultReceiver.java
@@ -0,0 +1,42 @@
+package com.android.contacts.incall;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+public class StartCallResultReceiver extends ResultReceiver {
+ private static final String TAG = StartCallResultReceiver.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private WeakReference<Receiver> mReceiver;
+
+ public StartCallResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (DEBUG) {
+ Log.d(TAG, "Result received resultCode: " + resultCode + " resultData: " + resultData);
+ }
+
+ if (mReceiver != null) {
+ Receiver receiver = mReceiver.get();
+ if (receiver != null) {
+ receiver.onReceiveResult(resultCode, resultData);
+ }
+ }
+ }
+
+ public interface Receiver {
+ public void onReceiveResult(int resultCode, Bundle resultData);
+ }
+
+ public void setReceiver(Receiver receiver) {
+ mReceiver = new WeakReference<Receiver>(receiver);
+ }
+
+} \ No newline at end of file
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index 6a81d066b..97e3f1777 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -73,8 +73,10 @@ public class ActionBarAdapter implements OnCloseListener {
private static final String EXTRA_KEY_QUERY = "navBar.query";
private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode";
+ private static final String EXTRA_KEY_TAB_COUNT = "navBar.tabCount";
private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
+ private static final String PERSISTENT_LAST_TAB_COUNT = "actionBarAdapter.lastTabCount";
private boolean mSelectionMode;
private boolean mSearchMode;
@@ -117,6 +119,7 @@ public class ActionBarAdapter implements OnCloseListener {
}
private int mCurrentTab = TabState.DEFAULT;
+ private int mTabCount = TabState.COUNT;
public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
View portraitTabs, View landscapeTabs, Toolbar toolbar) {
@@ -193,12 +196,13 @@ public class ActionBarAdapter implements OnCloseListener {
});
}
- public void initialize(Bundle savedState, ContactsRequest request) {
+
+ public void initialize(Bundle savedState, ContactsRequest request, int newTabCount) {
if (savedState == null) {
mSearchMode = request.isSearchMode();
mQueryString = request.getQueryString();
mCurrentTab = loadLastTabPreference();
- mSelectionMode = false;
+ mTabCount = loadLastTabCountPreference();
} else {
mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE);
@@ -206,8 +210,14 @@ public class ActionBarAdapter implements OnCloseListener {
// Just set to the field here. The listener will be notified by update().
mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
+ mTabCount = savedState.getInt(EXTRA_KEY_TAB_COUNT);
+ }
+ if (mTabCount != newTabCount) {
+ mTabCount = newTabCount;
+ saveLastTabCountPreference(mTabCount);
+ mCurrentTab = TabState.DEFAULT;
}
- if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
+ if (mCurrentTab >= mTabCount || mCurrentTab < 0) {
// Invalid tab index was saved (b/12938207). Restore the default.
mCurrentTab = TabState.DEFAULT;
}
@@ -254,21 +264,23 @@ public class ActionBarAdapter implements OnCloseListener {
/**
* Save the current tab selection, and notify the listener.
*/
- public void setCurrentTab(int tab) {
- setCurrentTab(tab, true);
+ public void setCurrentTab(int tab, int tabCount) {
+ setCurrentTab(tab, tabCount, true);
}
/**
* Save the current tab selection.
*/
- public void setCurrentTab(int tab, boolean notifyListener) {
- if (tab == mCurrentTab) {
+ public void setCurrentTab(int tab, int tabCount, boolean notifyListener) {
+ if (tab == mCurrentTab && tabCount == mTabCount) {
return;
}
mCurrentTab = tab;
+ mTabCount = tabCount;
if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
saveLastTabPreference(mCurrentTab);
+ saveLastTabCountPreference(mTabCount);
}
public int getCurrentTab() {
@@ -550,6 +562,7 @@ public class ActionBarAdapter implements OnCloseListener {
outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode);
outState.putString(EXTRA_KEY_QUERY, mQueryString);
outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
+ outState.putInt(EXTRA_KEY_TAB_COUNT, mTabCount);
}
public void setFocusOnSearchView() {
@@ -578,6 +591,18 @@ public class ActionBarAdapter implements OnCloseListener {
}
}
+ private void saveLastTabCountPreference(int tabCount) {
+ mPrefs.edit().putInt(PERSISTENT_LAST_TAB_COUNT, tabCount).apply();
+ }
+
+ private int loadLastTabCountPreference() {
+ try {
+ return mPrefs.getInt(PERSISTENT_LAST_TAB_COUNT, TabState.COUNT);
+ } catch (ClassCastException e) {
+ return TabState.COUNT;
+ }
+ }
+
private void animateTabHeightChange(int start, int end) {
if (mPortraitTabs == null) {
return;
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index 36e805ccb..a58545c5b 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -499,6 +499,11 @@ public class ContactSelectionActivity extends ContactsActivity
}
@Override
+ public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall, String mimeType) {
+ Log.w(TAG, "Unsupported call.");
+ }
+
+ @Override
public void onShortcutIntentCreated(Intent intent) {
returnPickerResult(intent);
}
diff --git a/src/com/android/contacts/activities/MultiPickContactActivity.java b/src/com/android/contacts/activities/MultiPickContactActivity.java
index ee7614b88..ac22f68b7 100644
--- a/src/com/android/contacts/activities/MultiPickContactActivity.java
+++ b/src/com/android/contacts/activities/MultiPickContactActivity.java
@@ -104,6 +104,7 @@ import com.android.contacts.common.list.ContactsSectionIndexer;
import com.android.contacts.common.list.DefaultContactListAdapter;
import com.android.contacts.common.MoreContactUtils;
import com.android.contacts.common.model.account.SimAccountType;
+import com.cyanogen.ambient.incall.CallLogConstants;
import java.util.ArrayList;
import java.util.Iterator;
@@ -928,7 +929,7 @@ public class MultiPickContactActivity extends ListActivity implements
break;
case MODE_DEFAULT_CALL:
case MODE_SEARCH_CALL:
- uri = Calls.CONTENT_URI_WITH_VOICEMAIL;
+ uri = CallLogConstants.CONTENT_ALL_URI_WITH_VOICEMAIL;
break;
case MODE_DEFAULT_SIM:
case MODE_SEARCH_SIM: {
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index bad3f6996..f548976eb 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -20,12 +20,15 @@ import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
+import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Context;
import android.content.BroadcastReceiver;
import android.content.IntentFilter;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
@@ -55,6 +58,8 @@ import android.widget.Toast;
import android.widget.Toolbar;
import com.android.contacts.ContactsActivity;
+import com.android.contacts.incall.InCallPluginHelper;
+import com.android.contacts.incall.InCallPluginInfo;
import com.android.contacts.R;
import com.android.contacts.activities.ActionBarAdapter.TabState;
import com.android.contacts.common.ContactsUtils;
@@ -92,7 +97,7 @@ import com.android.contacts.list.OnContactsUnavailableActionListener;
import com.android.contacts.list.ProviderStatusWatcher;
import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
import com.android.contacts.common.list.ViewPagerTabs;
-import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.list.PluginContactBrowseListFragment;
import com.android.contacts.preference.ContactsPreferenceActivity;
import com.android.contacts.common.SimContactsConstants;
import com.android.contacts.common.util.AccountFilterUtil;
@@ -104,11 +109,16 @@ import com.android.contacts.common.vcard.ExportVCardActivity;
import com.android.contacts.common.vcard.VCardCommonArguments;
import com.android.contacts.util.DialogManager;
import com.android.contactsbind.HelpUtils;
+import com.android.phone.common.incall.CallMethodHelper;
+import com.android.phone.common.incall.CallMethodInfo;
+import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -126,9 +136,11 @@ public class PeopleActivity extends ContactsActivity implements
JoinContactsListener {
private static final String TAG = "PeopleActivity";
+ private static final boolean DEBUG = false;
public static String EDITABLE_KEY = "search_contacts";
private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!";
+ private static final int INCALL_PLUGIN_LOADER_ID = 0;
// These values needs to start at 2. See {@link ContactEntryListFragment}.
private static final int SUBACTIVITY_ACCOUNT_FILTER = 2;
@@ -144,7 +156,6 @@ public class PeopleActivity extends ContactsActivity implements
private GroupDetailFragment mGroupDetailFragment;
private final GroupDetailFragmentListener mGroupDetailFragmentListener =
new GroupDetailFragmentListener();
- private View mFloatingActionButtonContainer;
private boolean wasLastFabAnimationScaleIn = false;
private ContactTileListFragment.Listener mFavoritesFragmentListener =
@@ -152,7 +163,7 @@ public class PeopleActivity extends ContactsActivity implements
private ContactListFilterController mContactListFilterController;
- private ContactsUnavailableFragment mContactsUnavailableFragment;
+ private boolean mAccountUnavailable;
private ProviderStatusWatcher mProviderStatusWatcher;
private Integer mProviderStatus;
@@ -164,16 +175,27 @@ public class PeopleActivity extends ContactsActivity implements
private MultiSelectContactsListFragment mAllFragment;
private ContactTileListFragment mFavoritesFragment;
private GroupBrowseListFragment mGroupsFragment;
+ private ContactsUnavailableFragment mAllUnavailableFragment;
+ private ContactsUnavailableFragment mFavoritesUnavailableFragment;
+ private ContactsUnavailableFragment mGroupsUnavailableFragment;
+ private List<InCallPluginInfo> mPluginTabInfo = new ArrayList<InCallPluginInfo>();
+ private int mPluginLength;
+ private int mTabStateGroup = TabState.GROUPS;
/** ViewPager for swipe */
private ViewPager mTabPager;
private ViewPagerTabs mViewPagerTabs;
private TabPagerAdapter mTabPagerAdapter;
- private String[] mTabTitles;
+ private List<TabEntry> mTabTitles;
+ private static final String CALL_METHOD_HELPER_SUBSCRIBER_ID = "PeopleActivity";
private final TabPagerListener mTabPagerListener = new TabPagerListener();
-
+ private int mPageStateCount; // total number of pages
private boolean mEnableDebugMenuOptions;
+ /* Floating action button */
+ private View mFloatingActionButtonContainer;
+ private ImageButton mFloatingActionButton;
+
private ExportToSimThread mExportThread = null;
/**
* True if this activity instance is a re-created one. i.e. set true after orientation change.
@@ -202,6 +224,17 @@ public class PeopleActivity extends ContactsActivity implements
private ArrayList<String[]> mContactList;
private BroadcastReceiver mExportToSimCompleteListener = null;
+ private BroadcastReceiver mListenForPluginUpdates = null;
+ private BroadcastReceiver mAuthUpdateListener = null;
+
+ final String FAVORITE_TAG = "tab-pager-favorite";
+ final String ALL_TAG = "tab-pager-all";
+ final String GROUPS_TAG = "tab-pager-groups";
+ final String FAVORITE_UNAVAILABLE_TAG = "tab-pager-favorite-unav";
+ final String ALL_UNAVAILABLE_TAG = "tab-pager-all-unav";
+ final String GROUPS_UNAVAILABLE_TAG = "tab-pager-groups-unav";
+ private static final String KEY_PLUGIN_INFO_LIST = "pluginInfoList";
+ SharedPreferences mPrefs;
public PeopleActivity() {
mInstanceId = sNextInstanceId.getAndIncrement();
@@ -228,33 +261,13 @@ public class PeopleActivity extends ContactsActivity implements
return ContactsUtils.areGroupWritableAccountsAvailable(this);
}
- /**
- * Initialize fragments that are (or may not be) in the layout.
- *
- * For the fragments that are in the layout, we initialize them in
- * {@link #createViewsAndFragments(Bundle)} after inflating the layout.
- *
- * However, the {@link ContactsUnavailableFragment} is a special fragment which may not
- * be in the layout, so we have to do the initialization here.
- *
- * The ContactsUnavailableFragment is always created at runtime.
- */
- @Override
- public void onAttachFragment(Fragment fragment) {
- if (fragment instanceof ContactsUnavailableFragment) {
- mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
- mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
- new ContactsUnavailableFragmentListener());
- }
- }
-
@Override
protected void onCreate(Bundle savedState) {
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
}
super.onCreate(savedState);
-
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
if (RequestPermissionsActivity.startPermissionActivity(this)) {
return;
}
@@ -276,7 +289,7 @@ public class PeopleActivity extends ContactsActivity implements
Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish");
}
getWindow().setBackgroundDrawable(null);
- registerReceiver();
+ registerReceivers();
}
@Override
@@ -286,7 +299,7 @@ public class PeopleActivity extends ContactsActivity implements
finish();
return;
}
- mActionBarAdapter.initialize(null, mRequest);
+ mActionBarAdapter.initialize(null, mRequest, mPageStateCount);
mContactListFilterController.checkFilterValidity(false);
@@ -296,7 +309,7 @@ public class PeopleActivity extends ContactsActivity implements
invalidateOptionsMenuIfNeeded();
}
- private void registerReceiver() {
+ private void registerReceivers() {
mExportToSimCompleteListener = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@@ -306,6 +319,7 @@ public class PeopleActivity extends ContactsActivity implements
}
}
};
+
IntentFilter exportCompleteFilter = new IntentFilter(
SimContactsConstants.INTENT_EXPORT_COMPLETE);
registerReceiver(mExportToSimCompleteListener, exportCompleteFilter);
@@ -344,19 +358,58 @@ public class PeopleActivity extends ContactsActivity implements
private void createViewsAndFragments(Bundle savedState) {
// Disable the ActionBar so that we can use a Toolbar. This needs to be called before
// setContentView().
+
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.people_activity);
+ // Configure action button, need to initialize early before ViewPager sets the
+ // visibility depending on the fragment type
+ mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
+ mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
+ mFloatingActionButton.setOnClickListener(this);
+ mFloatingActionButtonController = new FloatingActionButtonController(this,
+ mFloatingActionButtonContainer, mFloatingActionButton);
+
final FragmentManager fragmentManager = getFragmentManager();
// Hide all tabs (the current tab will later be reshown once a tab is selected)
final FragmentTransaction transaction = fragmentManager.beginTransaction();
- mTabTitles = new String[TabState.COUNT];
- mTabTitles[TabState.FAVORITES] = getString(R.string.favorites_tab_label);
- mTabTitles[TabState.ALL] = getString(R.string.all_contacts_tab_label);
- mTabTitles[TabState.GROUPS] = getString(R.string.contacts_groups_label);
+ mTabTitles = new LinkedList<TabEntry>();
+ mTabTitles.add(TabState.FAVORITES, new TabEntry(FAVORITE_TAG, getString(R.string
+ .favorites_tab_label)));
+ mTabTitles.add(TabState.ALL, new TabEntry(ALL_TAG, getString(R.string
+ .all_contacts_tab_label)));
+ mTabTitles.add(TabState.GROUPS,new TabEntry(GROUPS_TAG, getString(R.string
+ .contacts_groups_label)));
+
+ if (savedState != null) {
+ // Reconstruct the plugin info list
+ List<InCallPluginInfo> restoreList = savedState.getParcelableArrayList
+ (KEY_PLUGIN_INFO_LIST);
+ if (restoreList != null) {
+ mPluginTabInfo = restoreList;
+ }
+ mPluginLength = mPluginTabInfo.size();
+ for (int i = 0; i < mPluginLength; i++) {
+ InCallPluginInfo pluginInfo = mPluginTabInfo.get(i);
+ mTabTitles.add(TabState.GROUPS + i, new TabEntry(pluginInfo.mTabTag,
+ pluginInfo.mCallMethodInfo.mName));
+ pluginInfo.mFragment = (PluginContactBrowseListFragment) fragmentManager
+ .findFragmentByTag(pluginInfo.mTabTag);
+ pluginInfo.mFragment.setOnContactListActionListener
+ (new PluginContactBrowserActionListener());
+ pluginInfo.mFragment.updateInCallPluginInfo(pluginInfo);
+ }
+ } else {
+ // Init plugin info list
+ mPluginTabInfo.clear();
+ mPluginLength = 0;
+ }
+ mPageStateCount = TabState.COUNT + mPluginLength;
+ mTabStateGroup = TabState.GROUPS + mPluginLength;
+
mTabPager = getView(R.id.tab_pager);
mTabPagerAdapter = new TabPagerAdapter();
mTabPager.setAdapter(mTabPagerAdapter);
@@ -393,15 +446,36 @@ public class PeopleActivity extends ContactsActivity implements
fragmentManager.findFragmentByTag(ALL_TAG);
mGroupsFragment = (GroupBrowseListFragment)
fragmentManager.findFragmentByTag(GROUPS_TAG);
+ mFavoritesUnavailableFragment = (ContactsUnavailableFragment) fragmentManager
+ .findFragmentByTag(FAVORITE_UNAVAILABLE_TAG);
+ mAllUnavailableFragment = (ContactsUnavailableFragment) fragmentManager
+ .findFragmentByTag(ALL_UNAVAILABLE_TAG);
+ mGroupsUnavailableFragment = (ContactsUnavailableFragment) fragmentManager
+ .findFragmentByTag(GROUPS_UNAVAILABLE_TAG);
+
if (mFavoritesFragment == null) {
mFavoritesFragment = new ContactTileListFragment();
mAllFragment = new MultiSelectContactsListFragment();
mGroupsFragment = new GroupBrowseListFragment();
+ mFavoritesUnavailableFragment = new ContactsUnavailableFragment();
+ mAllUnavailableFragment = new ContactsUnavailableFragment();
+ mGroupsUnavailableFragment = new ContactsUnavailableFragment();
+ mFavoritesUnavailableFragment.setOnContactsUnavailableActionListener(
+ new ContactsUnavailableFragmentListener());
+ mAllUnavailableFragment.setOnContactsUnavailableActionListener(
+ new ContactsUnavailableFragmentListener());
+ mGroupsUnavailableFragment.setOnContactsUnavailableActionListener(
+ new ContactsUnavailableFragmentListener());
transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG);
transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG);
transaction.add(R.id.tab_pager, mGroupsFragment, GROUPS_TAG);
+ transaction.add(R.id.tab_pager, mFavoritesUnavailableFragment,
+ FAVORITE_UNAVAILABLE_TAG);
+ transaction.add(R.id.tab_pager, mAllUnavailableFragment, ALL_UNAVAILABLE_TAG);
+ transaction.add(R.id.tab_pager, mGroupsUnavailableFragment, GROUPS_UNAVAILABLE_TAG);
+
}
mFavoritesFragment.setListener(mFavoritesFragmentListener);
@@ -416,29 +490,28 @@ public class PeopleActivity extends ContactsActivity implements
transaction.hide(mFavoritesFragment);
transaction.hide(mAllFragment);
transaction.hide(mGroupsFragment);
+ transaction.hide(mFavoritesUnavailableFragment);
+ transaction.hide(mAllUnavailableFragment);
+ transaction.hide(mGroupsUnavailableFragment);
transaction.commitAllowingStateLoss();
fragmentManager.executePendingTransactions();
+ mFavoritesUnavailableFragment.setMessageText(R.string.listTotalAllContactsZeroStarred, -1);
+ mGroupsUnavailableFragment.setMessageText(R.string.noGroups,
+ areGroupWritableAccountsAvailable() ? -1 : R.string.noAccounts);
+ mAllUnavailableFragment.setMessageText(R.string.noContacts, -1);
+
// Setting Properties after fragment is created
mFavoritesFragment.setDisplayType(DisplayType.STREQUENT);
mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar(),
portraitViewPagerTabs, landscapeViewPagerTabs, toolbar);
- mActionBarAdapter.initialize(savedState, mRequest);
-
+ mActionBarAdapter.initialize(savedState, mRequest, mPageStateCount);
+ initializeFabVisibility();
// Add shadow under toolbar
ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources());
- // Configure floating action button
- mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
- final ImageButton floatingActionButton
- = (ImageButton) findViewById(R.id.floating_action_button);
- floatingActionButton.setOnClickListener(this);
- mFloatingActionButtonController = new FloatingActionButtonController(this,
- mFloatingActionButtonContainer, floatingActionButton);
- initializeFabVisibility();
-
invalidateOptionsMenuIfNeeded();
}
@@ -468,6 +541,7 @@ public class PeopleActivity extends ContactsActivity implements
@Override
protected void onPause() {
+ InCallPluginHelper.unsubscribe(CALL_METHOD_HELPER_SUBSCRIBER_ID);
mOptionsMenuContactsAvailable = false;
mProviderStatusWatcher.stop();
super.onPause();
@@ -479,6 +553,14 @@ public class PeopleActivity extends ContactsActivity implements
protected void onResume() {
super.onResume();
+ onResumeInit();
+ if (InCallPluginHelper.subscribe(CALL_METHOD_HELPER_SUBSCRIBER_ID,
+ pluginsUpdatedReceiver)) {
+ InCallPluginHelper.refreshDynamicItems();
+ }
+ }
+
+ private synchronized void onResumeInit() {
mProviderStatusWatcher.start();
updateViewConfiguration(true);
@@ -510,6 +592,7 @@ public class PeopleActivity extends ContactsActivity implements
if (mExportToSimCompleteListener != null) {
unregisterReceiver(mExportToSimCompleteListener);
}
+
super.onDestroy();
}
@@ -550,14 +633,14 @@ public class PeopleActivity extends ContactsActivity implements
tabToOpen = TabState.ALL;
break;
case ContactsRequest.ACTION_GROUP:
- tabToOpen = TabState.GROUPS;
+ tabToOpen = mTabStateGroup;
break;
default:
tabToOpen = -1;
break;
}
if (tabToOpen != -1) {
- mActionBarAdapter.setCurrentTab(tabToOpen);
+ mActionBarAdapter.setCurrentTab(tabToOpen, mPageStateCount);
}
if (filter != null) {
@@ -675,7 +758,7 @@ public class PeopleActivity extends ContactsActivity implements
* Updates the fragment/view visibility according to the current mode, such as
* {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}.
*/
- private void updateFragmentsVisibility() {
+ private synchronized void updateFragmentsVisibility() {
int tab = mActionBarAdapter.getCurrentTab();
if (mActionBarAdapter.isSearchMode() || mActionBarAdapter.isSelectionMode()) {
@@ -693,34 +776,11 @@ public class PeopleActivity extends ContactsActivity implements
mAllFragment.displayCheckBoxes(false);
}
invalidateOptionsMenu();
- showEmptyStateForTab(tab);
- if (tab == TabState.GROUPS) {
+ if (tab == mTabStateGroup) {
mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
}
}
- private void showEmptyStateForTab(int tab) {
- if (mContactsUnavailableFragment != null) {
- switch (getTabPositionForTextDirection(tab)) {
- case TabState.FAVORITES:
- mContactsUnavailableFragment.setMessageText(
- R.string.listTotalAllContactsZeroStarred, -1);
- break;
- case TabState.GROUPS:
- mContactsUnavailableFragment.setMessageText(R.string.noGroups,
- areGroupWritableAccountsAvailable() ? -1 : R.string.noAccounts);
- break;
- case TabState.ALL:
- mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1);
- break;
- }
- // When using the mContactsUnavailableFragment the ViewPager doesn't contain two views.
- // Therefore, we have to trick the ViewPagerTabs into thinking we have changed tabs
- // when the mContactsUnavailableFragment changes. Otherwise the tab strip won't move.
- mViewPagerTabs.onPageScrolled(tab, 0, 0);
- }
- }
-
private class TabPagerListener implements ViewPager.OnPageChangeListener {
// This package-protected constructor is here because of a possible compiler bug.
@@ -755,10 +815,9 @@ public class PeopleActivity extends ContactsActivity implements
public void onPageSelected(int position) {
// Make sure not in the search mode, in which case position != TabState.ordinal().
if (!mTabPagerAdapter.areTabsHidden()) {
- mActionBarAdapter.setCurrentTab(position, false);
+ mActionBarAdapter.setCurrentTab(position, mPageStateCount, false);
mViewPagerTabs.onPageSelected(position);
- showEmptyStateForTab(position);
- if (position == TabState.GROUPS) {
+ if (position == mTabStateGroup) {
mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
}
invalidateOptionsMenu();
@@ -802,7 +861,7 @@ public class PeopleActivity extends ContactsActivity implements
@Override
public int getCount() {
- return mAreTabsHiddenInTabPager ? 1 : TabState.COUNT;
+ return mAreTabsHiddenInTabPager ? 1 : mPageStateCount;
}
/** Gets called when the number of items changes. */
@@ -813,14 +872,22 @@ public class PeopleActivity extends ContactsActivity implements
return 0; // Only 1 page in search mode
}
} else {
- if (object == mFavoritesFragment) {
+ if ((!mAccountUnavailable && object == mFavoritesFragment) ||
+ (mAccountUnavailable && object == mFavoritesUnavailableFragment)) {
return getTabPositionForTextDirection(TabState.FAVORITES);
}
- if (object == mAllFragment) {
+ if ((!mAccountUnavailable && object == mAllFragment) || (mAccountUnavailable &&
+ object == mAllUnavailableFragment)) {
return getTabPositionForTextDirection(TabState.ALL);
}
- if (object == mGroupsFragment) {
- return TabState.GROUPS;
+ for (int i = 0; i < mPluginLength; i++) {
+ if (object == mPluginTabInfo.get(i).mFragment) {
+ return getTabPositionForTextDirection(TabState.GROUPS + i);
+ }
+ }
+ if ((!mAccountUnavailable && object == mGroupsFragment) || (mAccountUnavailable &&
+ object == mGroupsUnavailableFragment)) {
+ return mTabStateGroup;
}
}
return POSITION_NONE;
@@ -842,11 +909,16 @@ public class PeopleActivity extends ContactsActivity implements
return mAllFragment;
} else {
if (position == TabState.FAVORITES) {
- return mFavoritesFragment;
+ return mAccountUnavailable ? mFavoritesUnavailableFragment : mFavoritesFragment;
} else if (position == TabState.ALL) {
- return mAllFragment;
- } else if (position == TabState.GROUPS) {
- return mGroupsFragment;
+ return mAccountUnavailable ? mAllUnavailableFragment : mAllFragment;
+ } else if (position == (mPageStateCount - 1)) {
+ return mAccountUnavailable ? mGroupsUnavailableFragment : mGroupsFragment;
+ } else {
+ int pluginOffset = position - TabState.GROUPS;
+ if (pluginOffset >= 0 && pluginOffset < mPluginTabInfo.size()) {
+ return mPluginTabInfo.get(pluginOffset).mFragment;
+ }
}
}
throw new IllegalArgumentException("position: " + position);
@@ -912,7 +984,7 @@ public class PeopleActivity extends ContactsActivity implements
@Override
public CharSequence getPageTitle(int position) {
- return mTabTitles[position];
+ return mTabTitles.get(position).mTitle;
}
}
@@ -970,18 +1042,13 @@ public class PeopleActivity extends ContactsActivity implements
&& (mProviderStatus.equals(providerStatus))) return;
mProviderStatus = providerStatus;
- View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view);
-
if (mProviderStatus.equals(ProviderStatus.STATUS_NORMAL)) {
// Ensure that the mTabPager is visible; we may have made it invisible below.
- contactsUnavailableView.setVisibility(View.GONE);
- if (mTabPager != null) {
- mTabPager.setVisibility(View.VISIBLE);
- }
-
+ mAccountUnavailable = false;
if (mAllFragment != null) {
mAllFragment.setEnabled(true);
}
+ mTabPagerAdapter.notifyDataSetChanged();
} else {
// If there are no accounts on the device and we should show the "no account" prompt
// (based on {@link SharedPreferences}), then launch the account setup activity so the
@@ -998,33 +1065,46 @@ public class PeopleActivity extends ContactsActivity implements
AccountPromptUtils.launchAccountPrompt(this);
return;
}
+ mAccountUnavailable = true;
// Otherwise, continue setting up the page so that the user can still use the app
// without an account.
if (mAllFragment != null) {
mAllFragment.setEnabled(false);
}
- if (mContactsUnavailableFragment == null) {
- mContactsUnavailableFragment = new ContactsUnavailableFragment();
- mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
- new ContactsUnavailableFragmentListener());
- getFragmentManager().beginTransaction()
- .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment)
- .commitAllowingStateLoss();
- }
- mContactsUnavailableFragment.updateStatus(mProviderStatus);
+ mAllUnavailableFragment.updateStatus(mProviderStatus);
+ mFavoritesUnavailableFragment.updateStatus(mProviderStatus);
+ mGroupsUnavailableFragment.updateStatus(mProviderStatus);
+ mTabPagerAdapter.notifyDataSetChanged();
+ }
- // Show the contactsUnavailableView, and hide the mTabPager so that we don't
- // see it sliding in underneath the contactsUnavailableView at the edges.
- contactsUnavailableView.setVisibility(View.VISIBLE);
- if (mTabPager != null) {
- mTabPager.setVisibility(View.GONE);
- }
+ invalidateOptionsMenuIfNeeded();
+ }
+ private final class PluginContactBrowserActionListener implements
+ OnContactBrowserActionListener {
+ PluginContactBrowserActionListener() {}
+ @Override
+ public void onSelectionChange() {}
- showEmptyStateForTab(mActionBarAdapter.getCurrentTab());
+ @Override
+ public void onViewContactAction(Uri contactLookupUri) {
+ final Intent intent = ImplicitIntentsUtil.composeQuickContactIntent(contactLookupUri,
+ QuickContactActivity.MODE_FULLY_EXPANDED);
+ ImplicitIntentsUtil.startActivityInApp(PeopleActivity.this, intent);
}
- invalidateOptionsMenuIfNeeded();
+ @Override
+ public void onDeleteContactAction(Uri contactUri) {
+ ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
+ }
+
+ @Override
+ public void onFinishAction() {
+ onBackPressed();
+ }
+
+ @Override
+ public void onInvalidSelection() {}
}
private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
@@ -1245,6 +1325,7 @@ public class PeopleActivity extends ContactsActivity implements
final boolean isSearchOrSelectionMode = mActionBarAdapter.isSearchMode()
|| mActionBarAdapter.isSelectionMode();
+ boolean isPlugin = false;
if (isSearchOrSelectionMode) {
addGroupMenu.setVisible(false);
contactsFilterMenu.setVisible(false);
@@ -1252,18 +1333,16 @@ public class PeopleActivity extends ContactsActivity implements
helpMenu.setVisible(false);
makeMenuItemVisible(menu, R.id.menu_delete, false);
} else {
- switch (getTabPositionForTextDirection(mActionBarAdapter.getCurrentTab())) {
- case TabState.FAVORITES:
- addGroupMenu.setVisible(false);
- contactsFilterMenu.setVisible(false);
- clearFrequentsMenu.setVisible(hasFrequents());
- break;
- case TabState.ALL:
- addGroupMenu.setVisible(false);
- contactsFilterMenu.setVisible(true);
- clearFrequentsMenu.setVisible(false);
- break;
- case TabState.GROUPS:
+ int tabPosition = getTabPositionForTextDirection(mActionBarAdapter.getCurrentTab());
+ if (tabPosition == TabState.FAVORITES) {
+ addGroupMenu.setVisible(false);
+ contactsFilterMenu.setVisible(false);
+ clearFrequentsMenu.setVisible(hasFrequents());
+ } else if (tabPosition == TabState.ALL) {
+ addGroupMenu.setVisible(false);
+ contactsFilterMenu.setVisible(true);
+ clearFrequentsMenu.setVisible(false);
+ } else if (tabPosition == mTabStateGroup) {
// Do not display the "new group" button if no accounts are available
if (areGroupWritableAccountsAvailable()) {
addGroupMenu.setVisible(true);
@@ -1272,10 +1351,38 @@ public class PeopleActivity extends ContactsActivity implements
}
contactsFilterMenu.setVisible(false);
clearFrequentsMenu.setVisible(false);
+ } else if (mPluginLength > 0 && tabPosition >= TabState.GROUPS){
+ // plugin tab
+ int pluginIndex = tabPosition - TabState.GROUPS;
+ InCallPluginInfo pluginInfo = mPluginTabInfo.get(pluginIndex);
+ // floating button state
+ if (pluginInfo.mCallMethodInfo.mIsAuthenticated) {
+ mFloatingActionButtonContainer.setVisibility(View.VISIBLE);
+ } else {
+ mFloatingActionButtonContainer.setVisibility(View.GONE);
+ }
+ // menu
+ addGroupMenu.setVisible(false);
+ contactsFilterMenu.setVisible(false);
+ clearFrequentsMenu.setVisible(false);
+ makeMenuItemVisible(menu, R.id.menu_delete, false);
+ isPlugin = true;
+ }
+ if (!isPlugin) {
+ mFloatingActionButtonContainer.setVisibility(View.VISIBLE);
}
+
helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
}
final boolean showMiscOptions = !isSearchOrSelectionMode;
+ if (!isPlugin) {
+ makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
+ mFloatingActionButton.setImageDrawable(getResources().getDrawable(R.drawable
+ .ic_person_add_24dp));
+ } else {
+ mFloatingActionButton.setImageDrawable(getResources().getDrawable(R.drawable
+ .ic_add));
+ }
makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
@@ -1642,6 +1749,10 @@ public class PeopleActivity extends ContactsActivity implements
if (mTabPager != null) {
mTabPager.setOnPageChangeListener(null);
}
+ if (mPluginLength > 0) {
+ outState.putParcelableArrayList(KEY_PLUGIN_INFO_LIST, (ArrayList<InCallPluginInfo>)
+ mPluginTabInfo);
+ }
}
@Override
@@ -1664,16 +1775,31 @@ public class PeopleActivity extends ContactsActivity implements
public void onClick(View view) {
switch (view.getId()) {
case R.id.floating_action_button:
- Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- intent.putExtras(extras);
- }
- try {
- ImplicitIntentsUtil.startActivityInApp(PeopleActivity.this, intent);
- } catch (ActivityNotFoundException ex) {
- Toast.makeText(PeopleActivity.this, R.string.missing_app,
- Toast.LENGTH_SHORT).show();
+ int tabPosition = getTabPositionForTextDirection(mActionBarAdapter.getCurrentTab());
+ if (mPluginLength > 0 && tabPosition >= TabState.GROUPS &&
+ tabPosition != mTabStateGroup) {
+ // plugin tab
+ int pluginIndex = tabPosition - TabState.GROUPS;
+ InCallPluginInfo pluginInfo = mPluginTabInfo.get(pluginIndex);
+ if (pluginInfo.mCallMethodInfo.mDefaultDirectorySearchIntent != null) {
+ try {
+ pluginInfo.mCallMethodInfo.mDefaultDirectorySearchIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "directory search exception: ", e);
+ }
+ }
+ } else {
+ Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ try {
+ ImplicitIntentsUtil.startActivityInApp(PeopleActivity.this, intent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(PeopleActivity.this, R.string.missing_app,
+ Toast.LENGTH_SHORT).show();
+ }
}
break;
default:
@@ -1686,8 +1812,161 @@ public class PeopleActivity extends ContactsActivity implements
*/
private int getTabPositionForTextDirection(int position) {
if (isRTL()) {
- return TabState.COUNT - 1 - position;
+ return mPageStateCount - 1 - position;
}
return position;
}
+
+ private class TabEntry {
+ public String mTag;
+ public String mTitle;
+
+ public TabEntry(String tag, String title) {
+ mTag = tag;
+ mTitle = title;
+ }
+ }
+
+ /*
+ * peforms a look up using the tab tag among the ViewPagerTabs
+ */
+ private int lookupTabTag(String tabTag) {
+ int size = mTabTitles.size();
+ for (int i = 0; i < size; i++) {
+ TabEntry tab = mTabTitles.get(i);
+ if (TextUtils.equals(tab.mTag, tabTag)) {
+ return i;
+ }
+ }
+ return TabState.ALL;
+ }
+
+ private CallMethodHelper.CallMethodReceiver pluginsUpdatedReceiver =
+ new CallMethodHelper.CallMethodReceiver() {
+ @Override
+ public void onChanged(HashMap<ComponentName, CallMethodInfo> callMethodInfos) {
+ updatePlugins(callMethodInfos);
+ }
+ };
+
+ // Global CallMethod map that keeps track of the currently displayed plugins
+ HashMap<ComponentName, CallMethodInfo> mCallMethodMap =
+ new HashMap<ComponentName, CallMethodInfo>();
+
+ private InCallPluginInfo getPluginInfo(ComponentName cm) {
+ for (int i = 0; i < mPluginTabInfo.size(); i++) {
+ if (mPluginTabInfo.get(i).mCallMethodInfo.mComponent.equals(cm)) {
+ return mPluginTabInfo.get(i);
+ }
+ }
+ return null;
+ }
+
+ private void removePluginInfo(ComponentName cn) {
+ for (int i = 0; i < mPluginTabInfo.size(); i++) {
+ if (mPluginTabInfo.get(i).mCallMethodInfo.mComponent.equals(cn)) {
+ mPluginTabInfo.remove(i);
+ break;
+ }
+ }
+ }
+
+ private void removeTabTitle(ComponentName cn) {
+ for (int i = 0; i < mTabTitles.size(); i++) {
+ if (mTabTitles.get(i).mTag.equals(cn.toShortString())) {
+ mTabTitles.remove(i);
+ return;
+ }
+ }
+ }
+
+ private synchronized void updatePlugins(HashMap<ComponentName, CallMethodInfo>
+ callMethodInfo) {
+ HashMap<ComponentName, CallMethodInfo> newCmMap = (HashMap<ComponentName,
+ CallMethodInfo>) CallMethodHelper.getAllEnabledCallMethods().clone();
+ if (DEBUG) Log.d(TAG, "updatePlugins newCmMap size:" + newCmMap.size());
+ boolean updateTabs = false;
+ String lastSelectedTabTag = mTabTitles.get(mActionBarAdapter.getCurrentTab()).mTag;
+ boolean executeFragTransact = false;
+ FragmentTransaction transaction;
+ FragmentManager fragmentManager = getFragmentManager();
+
+ for (ComponentName cn : mCallMethodMap.keySet()) {
+ CallMethodInfo cm = mCallMethodMap.get(cn);
+ if (newCmMap.containsKey(cn)) {
+ // Check if update needed
+ CallMethodInfo newCm = newCmMap.remove(cn);
+ if (!newCm.equals(cm) || newCm.mIsAuthenticated != cm.mIsAuthenticated) {
+ InCallPluginInfo pluginInfo = getPluginInfo(cn);
+ pluginInfo.mCallMethodInfo = newCm;
+ pluginInfo.mFragment.updateInCallPluginInfo(pluginInfo);
+ mCallMethodMap.put(cn, newCm);
+ }
+ } else {
+ // Remove the tab associated with a plugin that's no longer available
+ updateTabs = true;
+ removeTabTitle(cn);
+ removePluginInfo(cn);
+ InCallPluginInfo removePlugin = getPluginInfo(cn);
+ if (removePlugin != null) {
+ transaction = fragmentManager.beginTransaction();
+ transaction.remove(removePlugin.mFragment);
+ transaction.commitAllowingStateLoss();
+ executeFragTransact = true;
+ }
+ }
+ }
+ // add newly added tab from newCmMap (existing tab already removed in the logic above)
+ for (ComponentName cn : newCmMap.keySet()) {
+ InCallPluginInfo newInfo = new InCallPluginInfo();
+ newInfo.mTabTag = cn.toShortString();
+ PluginContactBrowseListFragment frag = (PluginContactBrowseListFragment)
+ getFragmentManager().findFragmentByTag(newInfo.mTabTag);
+ if (frag == null) {
+ newInfo.mCallMethodInfo = newCmMap.get(cn);
+ mCallMethodMap.put(cn, newInfo.mCallMethodInfo);
+ mPluginTabInfo.add(0, newInfo);
+ newInfo.mFragment = new PluginContactBrowseListFragment();
+ transaction = fragmentManager.beginTransaction();
+ transaction.add(R.id.tab_pager, newInfo.mFragment, newInfo.mTabTag);
+ transaction.hide(newInfo.mFragment);
+ transaction.commitAllowingStateLoss();
+ newInfo.mFragment.updateInCallPluginInfo(newInfo);
+ newInfo.mFragment.setOnContactListActionListener
+ (new PluginContactBrowserActionListener());
+ mTabTitles.add(TabState.GROUPS,
+ new TabEntry(newInfo.mTabTag, newInfo.mCallMethodInfo.mName));
+ updateTabs = true;
+ executeFragTransact = true;
+ }
+ }
+
+ if (executeFragTransact) {
+ fragmentManager.executePendingTransactions();
+ }
+ // update holders
+ if (updateTabs) {
+ mPluginLength = mPluginTabInfo.size();
+ mPageStateCount = TabState.COUNT + mPluginLength;
+ mTabStateGroup = TabState.GROUPS + mPluginLength;
+ // update ViewPager
+ mActionBarAdapter.setListener(null);
+ if (mTabPager != null) {
+ mTabPager.setOnPageChangeListener(null);
+ }
+
+ mTabPagerAdapter.notifyDataSetChanged();
+ // re-add all the pages to ViewPagerTabs, including the new plugin tabs
+ mViewPagerTabs.setViewPager(mTabPager);
+ // restore last tab by unique tab tag, if it exsits in the tabs
+ mActionBarAdapter.setCurrentTab(lookupTabTag(lastSelectedTabTag), mPageStateCount,
+ false);
+ // need to force the ViewPagerTabs to refocus on the right tab, since setViewPager
+ // above causes a state loss
+ mViewPagerTabs.onPageSelected(mActionBarAdapter.getCurrentTab());
+ // force the tabs' underline to be cleared and redrawn
+ mViewPagerTabs.onPageScrolled(mActionBarAdapter.getCurrentTab(), 0, 0);
+ onResumeInit();
+ }
+ }
}
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index 724f37bf8..cb866c408 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -226,7 +226,7 @@ public class GroupDetailFragment extends Fragment implements OnScrollListener {
}
@Override
- public void onCallNumberDirectly(String phoneNumber) {
+ public void onCallNumberDirectly(String phoneNumber, String mimeType) {
// No need to call phone number directly from People app.
Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
}
diff --git a/src/com/android/contacts/incall/InCallPluginHelper.java b/src/com/android/contacts/incall/InCallPluginHelper.java
new file mode 100644
index 000000000..f7fe3b6c7
--- /dev/null
+++ b/src/com/android/contacts/incall/InCallPluginHelper.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+package com.android.contacts.incall;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.phone.common.ambient.AmbientConnection;
+import com.android.phone.common.incall.CallMethodHelper;
+import com.android.phone.common.incall.CallMethodInfo;
+import com.cyanogen.ambient.common.api.ResultCallback;
+import com.cyanogen.ambient.discovery.util.NudgeKey;
+import com.cyanogen.ambient.incall.extension.InCallContactInfo;
+import com.cyanogen.ambient.incall.InCallServices;
+import com.cyanogen.ambient.incall.results.InstalledPluginsResult;
+
+public class InCallPluginHelper extends CallMethodHelper {
+ private static final String TAG = InCallPluginHelper.class.getSimpleName();
+
+ private static final int EXPECTED_CALL_BACKS = 11;
+
+ protected static synchronized InCallPluginHelper getInstance() {
+ if (sInstance == null) {
+ sInstance = new InCallPluginHelper();
+ }
+ return (InCallPluginHelper) sInstance;
+ }
+
+ public static void init(Context context) {
+ InCallPluginHelper helper = getInstance();
+ helper.expectedCallbacks = EXPECTED_CALL_BACKS;
+ helper.mContext = context;
+ helper.mClient = AmbientConnection.CLIENT.get(context);
+ helper.mInCallApi = InCallServices.getInstance();
+ helper.mMainHandler = new Handler(context.getMainLooper());
+
+ refresh();
+ }
+
+ public static void refresh() {
+ updateCallPlugins();
+ }
+
+ public static void refreshDynamicItems() {
+ for (ComponentName cn : mCallMethodInfos.keySet()) {
+ getCallMethodAuthenticated(cn, true);
+ }
+ }
+
+ public static void refreshPendingIntents(InCallContactInfo contactInfo) {
+ for (ComponentName cn : mCallMethodInfos.keySet()) {
+ getInviteIntent(cn, contactInfo);
+ getDirectorySearchIntent(cn, contactInfo.mLookupUri);
+ }
+ }
+
+ protected static void updateCallPlugins() {
+ if (DEBUG) Log.d(TAG, "+++updateCallPlugins");
+ getInstance().mInCallApi.getInstalledPlugins(getInstance().mClient)
+ .setResultCallback(new ResultCallback<InstalledPluginsResult>() {
+ @Override
+ public void onResult(InstalledPluginsResult installedPluginsResult) {
+ // got installed components
+ mInstalledPlugins = installedPluginsResult.components;
+
+ synchronized (mCallMethodInfos) {
+ mCallMethodInfos.clear();
+ }
+
+ if (mInstalledPlugins.size() == 0) {
+ broadcast(false);
+ }
+
+ for (ComponentName cn : mInstalledPlugins) {
+ mCallMethodInfos.put(cn, new CallMethodInfo());
+ getCallMethodInfo(cn);
+ getCallMethodMimeType(cn);
+ getCallMethodStatus(cn);
+ getCallMethodVideoCallableMimeType(cn);
+ getCallMethodImMimeType(cn);
+ getCallMethodAuthenticated(cn, false);
+ getLoginIntent(cn);
+ getNudgeConfiguration(cn, NudgeKey.INCALL_CONTACT_FRAGMENT_LOGIN);
+ getNudgeConfiguration(cn, NudgeKey.INCALL_CONTACT_CARD_LOGIN);
+ getNudgeConfiguration(cn, NudgeKey.INCALL_CONTACT_CARD_DOWNLOAD);
+ getDefaultDirectorySearchIntent(cn);
+ // If you add any more callbacks, be sure to update
+ // EXPECTED_RESULT_CALLBACKS
+ // and EXPECTED_DYNAMIC_RESULT_CALLBACKS if the callback is dynamic
+ // with the proper count.
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/contacts/incall/InCallPluginInfo.java b/src/com/android/contacts/incall/InCallPluginInfo.java
new file mode 100644
index 000000000..26ba0fc2e
--- /dev/null
+++ b/src/com/android/contacts/incall/InCallPluginInfo.java
@@ -0,0 +1,102 @@
+package com.android.contacts.incall;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.list.PluginContactBrowseListFragment;
+import com.android.phone.common.incall.CallMethodInfo;
+
+import java.util.Objects;
+
+
+public class InCallPluginInfo implements Parcelable {
+ private static String TAG = InCallPluginInfo.class.getSimpleName();
+ private boolean DEBUG = false;
+
+ public CallMethodInfo mCallMethodInfo = new CallMethodInfo();
+ public String mTabTag; // uniquely identifies a ViewPagerTab and also serves as fragment ID
+ public PluginContactBrowseListFragment mFragment; // This reference will not be saved
+
+
+ public static final Parcelable.Creator<InCallPluginInfo> CREATOR
+ = new Parcelable.Creator<InCallPluginInfo>() {
+ public InCallPluginInfo createFromParcel(Parcel in) {
+ return new InCallPluginInfo(in);
+ }
+
+ public InCallPluginInfo[] newArray(int size) {
+ return new InCallPluginInfo[size];
+ }
+ };
+
+ public InCallPluginInfo() {}
+
+ private InCallPluginInfo(Parcel in) {
+ mCallMethodInfo.mAccountType = in.readString();
+ mCallMethodInfo.mAccountHandle = in.readString();
+ mCallMethodInfo.mIsAuthenticated = in.readInt() == 1;
+ mCallMethodInfo.mStatus = in.readInt();
+ mCallMethodInfo.mDirectorySearchIntent =
+ in.readParcelable(PendingIntent.class.getClassLoader());
+ mCallMethodInfo.mLoginIntent = in.readParcelable(PendingIntent.class.getClassLoader());
+ mCallMethodInfo.mName = in.readString();
+ mCallMethodInfo.mBrandIconId = in.readInt();
+ mCallMethodInfo.mLoginIconId = in.readInt();
+ mTabTag = in.readString();
+ mCallMethodInfo.mComponent = in.readParcelable(ComponentName.class.getClassLoader());
+ mCallMethodInfo.mNudgeComponent = in.readParcelable(ComponentName.class.getClassLoader());
+ mCallMethodInfo.mDependentPackage = in.readString();
+ mCallMethodInfo.mMimeType = in.readString();
+ mCallMethodInfo.mImMimeType = in.readString();
+ mCallMethodInfo.mVideoCallableMimeType = in.readString();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mCallMethodInfo.mAccountType);
+ dest.writeString(mCallMethodInfo.mAccountHandle);
+ dest.writeInt(mCallMethodInfo.mIsAuthenticated ? 1 : 0);
+ dest.writeInt(mCallMethodInfo.mStatus);
+ dest.writeParcelable(mCallMethodInfo.mDirectorySearchIntent, flags);
+ dest.writeParcelable(mCallMethodInfo.mLoginIntent, flags);
+ dest.writeString(mCallMethodInfo.mName);
+ dest.writeInt(mCallMethodInfo.mBrandIconId);
+ dest.writeInt(mCallMethodInfo.mLoginIconId);
+ dest.writeString(mTabTag);
+ dest.writeString(mTabTag);
+ dest.writeParcelable(mCallMethodInfo.mComponent, flags);
+ dest.writeParcelable(mCallMethodInfo.mNudgeComponent, flags);
+ dest.writeString(mCallMethodInfo.mDependentPackage);
+ dest.writeString(mCallMethodInfo.mMimeType);
+ dest.writeString(mCallMethodInfo.mImMimeType);
+ dest.writeString(mCallMethodInfo.mVideoCallableMimeType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (!(other instanceof InCallPluginInfo)) {
+ return false;
+ }
+ InCallPluginInfo otherObj = (InCallPluginInfo) other;
+ return this.mCallMethodInfo.equals(otherObj.mCallMethodInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ return mTabTag.hashCode();
+ }
+}
diff --git a/src/com/android/contacts/incall/InCallPluginUtils.java b/src/com/android/contacts/incall/InCallPluginUtils.java
new file mode 100644
index 000000000..2b8a15f82
--- /dev/null
+++ b/src/com/android/contacts/incall/InCallPluginUtils.java
@@ -0,0 +1,201 @@
+package com.android.contacts.incall;
+
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.util.UriUtils;
+import com.android.phone.common.ambient.AmbientConnection;
+import com.android.phone.common.incall.CallMethodHelper;
+import com.android.phone.common.incall.CallMethodInfo;
+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.InCallContactInfo;
+import com.cyanogen.ambient.incall.extension.OriginCodes;
+import com.cyanogen.ambient.incall.extension.StartCallRequest;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class InCallPluginUtils {
+ private static final String TAG = InCallPluginUtils.class.getSimpleName();
+ private static boolean DEBUG = false;
+
+ // key to store data id for InCall plugin callable entries
+ public static final String KEY_DATA_ID = InCallPluginUtils.class.getPackage().getName() +
+ ".data_id";
+ public static final String KEY_MIMETYPE = InCallPluginUtils.class.getPackage().getName() +
+ ".mimetype";
+ public static final String KEY_COMPONENT = InCallPluginUtils.class.getPackage().getName() +
+ ".component";
+ public static final String KEY_NAME = InCallPluginUtils.class.getPackage().getName() + ".name";
+ public static final String KEY_NUMBER = InCallPluginUtils.class.getPackage().getName() +
+ ".number";
+ public static final String KEY_NUDGE_KEY = InCallPluginUtils.class.getPackage().getName() + ""
+ + ".nudge_key";
+
+ public static Drawable getDrawable(Context context, int resourceId,
+ ComponentName componentName) {
+ Resources pluginRes = null;
+ Drawable drawable = null;
+
+ if (resourceId == 0) {
+ return null;
+ }
+ try {
+ pluginRes = context.getPackageManager().getResourcesForApplication(
+ componentName.getPackageName());
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Plugin not installed: " + componentName, e);
+ return null;
+ }
+ try {
+ drawable = pluginRes.getDrawable(resourceId);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Plugin does not define login icon: " + componentName, e);
+ }
+ return drawable;
+ }
+
+ public static Intent getVoiceMimeIntent(String mimeType, DataItem dataItem, CallMethodInfo cmi,
+ String contactAccountHandle) {
+ Intent intent = new Intent();
+ intent.putExtra(KEY_DATA_ID, dataItem.getId());
+ intent.putExtra(KEY_MIMETYPE, mimeType);
+ intent.putExtra(KEY_COMPONENT, cmi.mComponent.flattenToString());
+ intent.putExtra(KEY_NAME, cmi.mName);
+ intent.putExtra(KEY_NUMBER, contactAccountHandle);
+ return intent;
+ }
+
+ public static Intent getImMimeIntent(String mimeType, RawContact rawContact) {
+ DataItem imDataItem = null;
+ long dataId;
+ for (DataItem dataItem : rawContact.getDataItems()) {
+ if (dataItem.getMimeType().equals(mimeType)) {
+ imDataItem = dataItem;
+ break;
+ }
+ }
+ dataId = imDataItem == null ? -1 : imDataItem.getId();
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.putExtra(KEY_DATA_ID, dataId);
+ intent.setDataAndType(ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI,
+ dataId), mimeType);
+ return intent;
+ }
+
+ public static class PresenceInfo {
+ public Drawable mPresenceIcon;
+ public String mStatusMsg;
+ }
+ // targetRawContact is the raw contact with the same account type as the InCall plugin
+ public static PresenceInfo lookupPresenceInfo(Context context, Contact contact, RawContact
+ targetRawContact) {
+ // When ContactLoader loads a list of raw contact in the ImmutableList, each raw contact's
+ // associated presence data is placed in the ImmutableMap in the same order. ImmutableMap
+ // iterates in the same order items were originally placed in it.
+ PresenceInfo presenceInfo = new PresenceInfo();
+ ImmutableList<RawContact> rawContacts = contact.getRawContacts();
+ ImmutableMap<Long, DataStatus> statusMap = contact.getStatuses();
+ if (rawContacts == null || statusMap == null) {
+ return presenceInfo;
+ }
+ // Look for the position of the raw contact that corresponds to the plugin
+ int index = rawContacts.indexOf(targetRawContact);
+ if (index < 0 || rawContacts.size() != statusMap.size()) {
+ // target raw contact entry not found or the list and map sizes mismatch (we may risk
+ // the iterator not finding the matching presence data entry)
+ return presenceInfo;
+ }
+ // move the iterator to the same offset as the raw contact to retrieve the presence data
+ Iterator it = statusMap.entrySet().iterator();
+ ImmutableMap.Entry<Long, DataStatus> mapEntry = null;
+ while (it.hasNext()) {
+ if (index == 0) {
+ mapEntry = (ImmutableMap.Entry<Long, DataStatus>)it.next();
+ break;
+ }
+ index--;
+ }
+ if (mapEntry == null) {
+ return presenceInfo;
+ }
+ DataStatus presenceStatus = mapEntry.getValue();
+ int presence = presenceStatus == null ? ContactsContract.StatusUpdates.OFFLINE :
+ presenceStatus.getPresence();
+ presenceInfo.mPresenceIcon = ContactPresenceIconUtil.getPresenceIcon(context, presence);
+ presenceInfo.mStatusMsg = ContactStatusUtil.getStatusString(context, presence);
+ return presenceInfo;
+ }
+
+ public static ArrayList<ComponentName> gatherPluginHistory() {
+ ArrayList<ComponentName> cnList = new ArrayList<ComponentName>();
+ HashMap<ComponentName, CallMethodInfo> plugins = InCallPluginHelper
+ .getAllEnabledCallMethods();
+ for (ComponentName cn : plugins.keySet()) {
+ cnList.add(cn);
+ }
+ return cnList;
+ }
+
+ // Retrieve the first contact data entry
+ public static String lookupContactData(Contact contact, String dataMimeType, String dataKind) {
+ String dataValue = "";
+
+ if (TextUtils.isEmpty(dataMimeType) || TextUtils.isEmpty(dataKind)) {
+ return null;
+ }
+ for (RawContact raw: contact.getRawContacts()) {
+ for (DataItem dataItem : raw.getDataItems()) {
+ final ContentValues entryValues = dataItem.getContentValues();
+ final String mimeType = dataItem.getMimeType();
+
+ if (mimeType == null) continue;
+
+ if (mimeType.equals(dataMimeType)) {
+ return entryValues.getAsString(dataKind);
+ }
+ }
+ }
+ return dataValue;
+ }
+
+ public static InCallContactInfo getInCallContactInfo(Contact contact) {
+ String phoneNumber = InCallPluginUtils.lookupContactData(contact,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
+ ContactsContract.CommonDataKinds.Phone.NUMBER);
+ Uri lookupUri = contact.getLookupUri();
+ if (UriUtils.isEncodedContactUri(lookupUri)) {
+ lookupUri = Uri.parse(TextUtils.isEmpty(contact.getDisplayName()) ? phoneNumber :
+ contact.getDisplayName());
+ }
+ return new InCallContactInfo(contact.getDisplayName(),
+ phoneNumber, lookupUri);
+
+ }
+}
diff --git a/src/com/android/contacts/interactions/CallLogInteraction.java b/src/com/android/contacts/interactions/CallLogInteraction.java
index 3464c0f95..4dd3e366b 100644
--- a/src/com/android/contacts/interactions/CallLogInteraction.java
+++ b/src/com/android/contacts/interactions/CallLogInteraction.java
@@ -16,9 +16,13 @@
package com.android.contacts.interactions;
import com.android.contacts.R;
+import com.android.contacts.common.CallUtil;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.incall.InCallPluginUtils;
+import com.android.contacts.quickcontact.QuickContactActivity;
+import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -27,11 +31,16 @@ import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
import android.text.BidiFormatter;
import android.text.Spannable;
import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import com.android.contacts.incall.InCallPluginUtils;
+import com.cyanogen.ambient.incall.CallLogConstants;
/**
* Represents a call log event interaction, wrapping the columns in
* {@link android.provider.CallLog.Calls}.
@@ -52,6 +61,9 @@ public class CallLogInteraction implements ContactInteraction {
private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
private ContentValues mValues;
+ private Drawable mIcon;
+ private int mIconResourceId = 0;
+ private String mPluginName;
public CallLogInteraction(ContentValues values) {
mValues = values;
@@ -60,8 +72,22 @@ public class CallLogInteraction implements ContactInteraction {
@Override
public Intent getIntent() {
String number = getNumber();
- return number == null ? null : new Intent(Intent.ACTION_CALL).setData(
- Uri.parse(URI_TARGET_PREFIX + number));
+ Intent intent;
+ if (TextUtils.isEmpty(getPluginPkgName())) {
+ // regular PSTN
+ intent = CallUtil.getCallIntent(getNumber());
+ } else {
+ // plugin
+ intent = new Intent();
+ intent.putExtra(InCallPluginUtils.KEY_DATA_ID,
+ QuickContactActivity.CARD_ENTRY_ID_INCALL_PLUGIN_CALL);
+ intent.putExtra(InCallPluginUtils.KEY_COMPONENT, getPluginPkgName());
+ intent.putExtra(InCallPluginUtils.KEY_NAME, getPluginName());
+ intent.putExtra(InCallPluginUtils.KEY_NUMBER, number);
+ intent.putExtra(InCallPluginUtils.KEY_MIMETYPE, PhoneNumberUtils.isGlobalPhoneNumber
+ (number) ? ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE : "");
+ }
+ return intent;
}
@Override
@@ -94,7 +120,12 @@ public class CallLogInteraction implements ContactInteraction {
@Override
public Drawable getIcon(Context context) {
- return context.getResources().getDrawable(CALL_LOG_ICON_RES);
+ if (mIcon != null) {
+ // it's a plugin interaction
+ return mIcon;
+ } else {
+ return context.getResources().getDrawable(CALL_LOG_ICON_RES);
+ }
}
@Override
@@ -212,6 +243,29 @@ public class CallLogInteraction implements ContactInteraction {
@Override
public int getIconResourceId() {
- return CALL_LOG_ICON_RES;
+ if (mIconResourceId != 0) {
+ return mIconResourceId;
+ } else {
+ return CALL_LOG_ICON_RES;
+ }
+ }
+
+ public void setPluginInfo(Context context, int resourceId, String pluginName) {
+ ComponentName compName = ComponentName.unflattenFromString(getPluginPkgName());
+ mIcon = InCallPluginUtils.getDrawable(context, resourceId, compName);
+ mIconResourceId = resourceId;
+ mPluginName = pluginName;
+ }
+
+ public String getPluginPkgName() {
+ return mValues.getAsString(CallLogConstants.PLUGIN_PACKAGE_NAME);
+ }
+
+ public String getPluginUserHandle() {
+ return mValues.getAsString(CallLogConstants.PLUGIN_USER_HANDLE);
+ }
+
+ public String getPluginName() {
+ return mPluginName;
}
}
diff --git a/src/com/android/contacts/interactions/CallLogInteractionsLoader.java b/src/com/android/contacts/interactions/CallLogInteractionsLoader.java
index 2340d3f29..c64421c84 100644
--- a/src/com/android/contacts/interactions/CallLogInteractionsLoader.java
+++ b/src/com/android/contacts/interactions/CallLogInteractionsLoader.java
@@ -16,6 +16,7 @@
package com.android.contacts.interactions;
import android.content.AsyncTaskLoader;
+import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -26,25 +27,31 @@ import android.provider.CallLog.Calls;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import com.android.contacts.incall.InCallPluginHelper;
import com.google.common.annotations.VisibleForTesting;
import com.android.contacts.common.util.PermissionsUtil;
+import com.android.phone.common.incall.CallMethodInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
-
+ private final Context mContext;
private final String[] mPhoneNumbers;
private final int mMaxToRetrieve;
private List<ContactInteraction> mData;
+ private HashMap<ComponentName, List<String>> mPluginAccountsMap;
- public CallLogInteractionsLoader(Context context, String[] phoneNumbers,
- int maxToRetrieve) {
+ public CallLogInteractionsLoader(Context context, String[] phoneNumbers, HashMap<ComponentName,
+ List<String>> pluginAccountsMap, int maxToRetrieve) {
super(context);
+ mContext = context;
mPhoneNumbers = phoneNumbers;
+ mPluginAccountsMap = pluginAccountsMap;
mMaxToRetrieve = maxToRetrieve;
}
@@ -53,13 +60,31 @@ public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInter
if (!PermissionsUtil.hasPhonePermissions(getContext())
|| !getContext().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
- || mPhoneNumbers == null || mPhoneNumbers.length <= 0 || mMaxToRetrieve <= 0) {
+ || (mPhoneNumbers == null || mPhoneNumbers.length <= 0 || mMaxToRetrieve <= 0)) {
return Collections.emptyList();
}
final List<ContactInteraction> interactions = new ArrayList<>();
- for (String number : mPhoneNumbers) {
- interactions.addAll(getCallLogInteractions(number));
+ if (mPhoneNumbers != null) {
+ for (String number : mPhoneNumbers) {
+ interactions.addAll(getCallLogInteractions(number, null));
+ }
+ }
+ // add plugin entries
+ if (InCallPluginHelper.infoReady()) {
+ HashMap<ComponentName, CallMethodInfo> inCallPlugins = InCallPluginHelper
+ .getAllEnabledCallMethods();
+ if (inCallPlugins != null) {
+ for (ComponentName cn : inCallPlugins.keySet()) {
+ List<String> accountList = mPluginAccountsMap.get(cn);
+ CallMethodInfo cmi = inCallPlugins.get(cn);
+ if (cmi == null) continue;
+ if (accountList == null || cmi == null) continue;
+ for (int i = 0; i < accountList.size(); i++) {
+ interactions.addAll(getCallLogInteractions(accountList.get(i), cmi));
+ }
+ }
+ }
}
// Sort the call log interactions by date for duplicate removal
Collections.sort(interactions, new Comparator<ContactInteraction>() {
@@ -75,7 +100,7 @@ public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInter
}
});
// Duplicates only occur because of fuzzy matching. No need to dedupe a single number.
- if (mPhoneNumbers.length == 1) {
+ if (interactions.size() == 1) {
return interactions;
}
return pruneDuplicateCallLogInteractions(interactions, mMaxToRetrieve);
@@ -106,10 +131,13 @@ public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInter
return subsetInteractions;
}
- private List<ContactInteraction> getCallLogInteractions(String phoneNumber) {
+ private List<ContactInteraction> getCallLogInteractions(String phoneNumber, CallMethodInfo
+ cmi) {
// TODO: the phone number added to the ContactInteractions result should retain their
// original formatting since TalkBack is not reading the normalized number correctly
- final String normalizedNumber = PhoneNumberUtils.normalizeNumber(phoneNumber);
+ String pluginComponent = cmi == null ? "" : cmi.mComponent.flattenToString();
+ final String normalizedNumber = TextUtils.isEmpty(pluginComponent) ?
+ PhoneNumberUtils.normalizeNumber(phoneNumber) : phoneNumber;
// If the number contains only symbols, we can skip it
if (TextUtils.isEmpty(normalizedNumber)) {
return Collections.emptyList();
@@ -131,7 +159,27 @@ public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInter
while (cursor.moveToNext()) {
final ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(cursor, values);
- interactions.add(new CallLogInteraction(values));
+ CallLogInteraction interaction = new CallLogInteraction(values);
+ // loadInBackground calls this function twice
+ // First pass: argument phoneNumber: PSTN number, pluginComponent: null
+ // (if the PSTN number was dialed through a plugin, the queried cursor entry should
+ // contain the plugin component in the "plugin_package_name" column)
+ // Second pass: argument phoneNumber: plugin user handle, pluginComponent: valid
+ if ((TextUtils.isEmpty(pluginComponent) &&
+ !TextUtils.isEmpty(interaction.getPluginPkgName())) ||
+ (!TextUtils.isEmpty(pluginComponent) &&
+ TextUtils.equals(interaction.getPluginPkgName(), pluginComponent)))
+ {
+ // PSTN dialed through a plugin
+ if (cmi == null) {
+ cmi = InCallPluginHelper.getCallMethod(ComponentName.unflattenFromString
+ (interaction.getPluginPkgName()));
+ }
+ // No matching plugin, skip
+ if (cmi == null) continue;
+ interaction.setPluginInfo(mContext, cmi.mBrandIconId, cmi.mName);
+ }
+ interactions.add(interaction);
}
return interactions;
} finally {
diff --git a/src/com/android/contacts/list/ContactTileListFragment.java b/src/com/android/contacts/list/ContactTileListFragment.java
index 189cfd375..33506f200 100644
--- a/src/com/android/contacts/list/ContactTileListFragment.java
+++ b/src/com/android/contacts/list/ContactTileListFragment.java
@@ -232,7 +232,7 @@ public class ContactTileListFragment extends Fragment {
}
@Override
- public void onCallNumberDirectly(String phoneNumber) {
+ public void onCallNumberDirectly(String phoneNumber, String mimeType) {
if (mListener != null) {
mListener.onCallNumberDirectly(phoneNumber);
}
diff --git a/src/com/android/contacts/list/PluginContactBrowseListFragment.java b/src/com/android/contacts/list/PluginContactBrowseListFragment.java
new file mode 100644
index 000000000..3a5aeaa6e
--- /dev/null
+++ b/src/com/android/contacts/list/PluginContactBrowseListFragment.java
@@ -0,0 +1,765 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.list;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.*;
+import android.provider.ContactsContract;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.common.widget.CompositeCursorAdapter;
+import com.android.contacts.common.list.AutoScrollListView;
+import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.common.list.DirectoryPartition;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.incall.InCallPluginInfo;
+import com.android.contacts.R;
+import com.android.contacts.common.list.ContactListAdapter;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.DefaultContactListAdapter;
+import com.android.contacts.common.list.ProfileAndContactsLoader;
+import com.android.contacts.incall.InCallPluginUtils;
+
+import java.util.List;
+
+public class PluginContactBrowseListFragment extends ContactEntryListFragment<ContactListAdapter>
+ implements View.OnClickListener {
+ private static final String TAG = PluginContactBrowseListFragment.class.getSimpleName();
+ private boolean DEBUG = false;
+ private Context mContext;
+ private InCallPluginInfo mInCallPluginInfo;
+ private View mLayout;
+ private View mListView;
+ private View mLoginView;
+ private Button mLoginBtn;
+ private ImageView mLoginIconView;
+ private TextView mLoginMsg;
+ private TextView mEmptyView;
+ private static final String KEY_SELECTED_URI = "selectedUri";
+ private static final String KEY_SELECTION_VERIFIED = "selectionVerified";
+ private static final String KEY_FILTER = "filter";
+ private static final String KEY_LAST_SELECTED_POSITION = "lastSelected";
+ private static final String KEY_SHARED_PREFS_FILE_NAME = "prefsFileName";
+
+ private static final String PERSISTENT_SELECTION_PREFIX = "defaultContactBrowserSelection";
+ private static final String PREFS_FILE_PREFIX = "plugin.";
+ private SharedPreferences mPrefs;
+ private String mPrefsFileName;
+
+ private boolean mStartedLoading;
+ private boolean mSelectionToScreenRequested;
+ private boolean mSmoothScrollRequested;
+ private boolean mSelectionPersistenceRequested;
+ private Uri mSelectedContactUri;
+ private long mSelectedContactDirectoryId;
+ private String mSelectedContactLookupKey;
+ private long mSelectedContactId;
+ private boolean mSelectionVerified;
+ private int mLastSelectedPosition = -1;
+ private boolean mRefreshingContactUri;
+ private ContactListFilter mFilter;
+ private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX;
+
+ protected OnContactBrowserActionListener mListener;
+ private ContactLookupTask mContactLookupTask;
+
+ private final class ContactLookupTask extends AsyncTask<Void, Void, Uri> {
+
+ private final Uri mUri;
+ private boolean mIsCancelled;
+
+ public ContactLookupTask(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ protected Uri doInBackground(Void... args) {
+ Cursor cursor = null;
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mUri);
+ cursor = resolver.query(uriCurrentFormat,
+ new String[] { ContactsContract.Contacts._ID,
+ ContactsContract.Contacts.LOOKUP_KEY }, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ final long contactId = cursor.getLong(0);
+ final String lookupKey = cursor.getString(1);
+ if (contactId != 0 && !TextUtils.isEmpty(lookupKey)) {
+ return ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+ }
+ }
+
+ Log.e(TAG, "Error: No contact ID or lookup key for contact " + mUri);
+ return null;
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading the contact: " + mUri, e);
+ return null;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ public void cancel() {
+ super.cancel(true);
+ // Use a flag to keep track of whether the {@link AsyncTask} was cancelled or not in
+ // order to ensure onPostExecute() is not executed after the cancel request. The flag is
+ // necessary because {@link AsyncTask} still calls onPostExecute() if the cancel request
+ // came after the worker thread was finished.
+ mIsCancelled = true;
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ // Make sure the {@link Fragment} is at least still attached to the {@link Activity}
+ // before continuing. Null URIs should still be allowed so that the list can be
+ // refreshed and a default contact can be selected (i.e. the case of deleted
+ // contacts).
+ if (mIsCancelled || !isAdded()) {
+ return;
+ }
+ onContactUriQueryFinished(uri);
+ }
+ }
+
+ public PluginContactBrowseListFragment() {
+ if (DEBUG) Log.d(TAG, "Constructor");
+ setPhotoLoaderEnabled(true);
+ setQuickContactEnabled(false);
+ setSectionHeaderDisplayEnabled(true);
+ setVisibleScrollbarEnabled(true);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (DEBUG) Log.d(TAG, "onAttach");
+ }
+
+ // This method is called in parent class of onCreateView
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ mLayout = inflater.inflate(R.layout.plugin_contact_list_content, null);
+ return mLayout;
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ if (DEBUG) Log.d(TAG, "onCreateView");
+ super.onCreateView(inflater, container);
+ mLoginView = mLayout.findViewById(R.id.plugin_login_layout);
+ mListView = mLayout.findViewById(android.R.id.list);
+ mLoginBtn = (Button) mLayout.findViewById(R.id.plugin_login_button);
+ mLoginBtn.setOnClickListener(this);
+ mLoginIconView = (ImageView) mLayout.findViewById(R.id.plugin_login_icon);
+ mLoginMsg = (TextView) mLayout.findViewById(R.id.plugin_login_msg);
+ mEmptyView = (TextView) mLayout.findViewById(android.R.id.empty);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onActivityCreated");
+ super.onActivityCreated(savedInstanceState);
+ mPrefs = getActivity().getSharedPreferences(mPrefsFileName, Context.MODE_PRIVATE);
+ updatePluginView(); // this is execute again reflect change in plugin info
+ restoreFilter();
+ restoreSelectedUri(false);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ final SharedPreferences oldFragPrefs = getActivity().getSharedPreferences(mPrefsFileName,
+ Context.MODE_PRIVATE);
+ oldFragPrefs.edit().clear().commit();
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ setFilter(filter, true);
+ }
+
+ public void setFilter(ContactListFilter filter, boolean restoreSelectedUri) {
+ if (mFilter == null && filter == null) {
+ return;
+ }
+
+ if (mFilter != null && mFilter.equals(filter)) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "New filter: " + filter);
+
+ mFilter = filter;
+ mLastSelectedPosition = -1;
+ saveFilter();
+ if (restoreSelectedUri) {
+ mSelectedContactUri = null;
+ restoreSelectedUri(true);
+ }
+ reloadData();
+ }
+
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public void restoreSavedState(Bundle savedState) {
+ super.restoreSavedState(savedState);
+ if (savedState == null) {
+ if (DEBUG) Log.d(TAG, "restoreSavedState null");
+ return;
+ }
+
+ mFilter = savedState.getParcelable(KEY_FILTER);
+ mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI);
+ mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED);
+ mLastSelectedPosition = savedState.getInt(KEY_LAST_SELECTED_POSITION);
+ mPrefsFileName = savedState.getString(KEY_SHARED_PREFS_FILE_NAME, "");
+ if (DEBUG) Log.d(TAG, "restoreSavedState mPrefsFileName :" + mPrefsFileName);
+ if (!mPrefsFileName.equals("")) {
+ mPrefs = getActivity().getSharedPreferences(mPrefsFileName, Context.MODE_PRIVATE);
+ }
+ parseSelectedContactUri();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_FILTER, mFilter);
+ outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri);
+ outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified);
+ outState.putInt(KEY_LAST_SELECTED_POSITION, mLastSelectedPosition);
+ outState.putString(KEY_SHARED_PREFS_FILE_NAME, mPrefsFileName);
+ }
+
+ protected void refreshSelectedContactUri() {
+ if (mContactLookupTask != null) {
+ mContactLookupTask.cancel();
+ }
+
+ if (!isSelectionVisible()) {
+ return;
+ }
+
+ mRefreshingContactUri = true;
+
+ if (mSelectedContactUri == null) {
+ onContactUriQueryFinished(null);
+ return;
+ }
+
+ if (mSelectedContactDirectoryId != ContactsContract.Directory.DEFAULT
+ && mSelectedContactDirectoryId != ContactsContract.Directory.LOCAL_INVISIBLE) {
+ onContactUriQueryFinished(mSelectedContactUri);
+ } else {
+ mContactLookupTask = new ContactLookupTask(mSelectedContactUri);
+ mContactLookupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
+ }
+ }
+
+ protected void onContactUriQueryFinished(Uri uri) {
+ mRefreshingContactUri = false;
+ mSelectedContactUri = uri;
+ parseSelectedContactUri();
+ checkSelection();
+ }
+
+ public InCallPluginInfo getPluginInfo() {
+ return mInCallPluginInfo;
+ }
+
+ public Uri getSelectedContactUri() {
+ return mSelectedContactUri;
+ }
+
+ /**
+ * Sets the new selection for the list.
+ */
+ public void setSelectedContactUri(Uri uri) {
+ setSelectedContactUri(uri, true, false /* no smooth scroll */, true, false);
+ }
+
+ /**
+ * Sets the new contact selection.
+ *
+ * @param uri the new selection
+ * @param required if true, we need to check if the selection is present in
+ * the list and if not notify the listener so that it can load a
+ * different list
+ * @param smoothScroll if true, the UI will roll smoothly to the new
+ * selection
+ * @param persistent if true, the selection will be stored in shared
+ * preferences.
+ * @param willReloadData if true, the selection will be remembered but not
+ * actually shown, because we are expecting that the data will be
+ * reloaded momentarily
+ */
+ private void setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll,
+ boolean persistent, boolean willReloadData) {
+ mSmoothScrollRequested = smoothScroll;
+ mSelectionToScreenRequested = true;
+
+ if ((mSelectedContactUri == null && uri != null)
+ || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) {
+ mSelectionVerified = false;
+ mSelectionPersistenceRequested = persistent;
+ mSelectedContactUri = uri;
+ parseSelectedContactUri();
+
+ if (!willReloadData) {
+ // Configure the adapter to show the selection based on the
+ // lookup key extracted from the URI
+ ContactListAdapter adapter = getAdapter();
+ if (adapter != null) {
+ adapter.setSelectedContact(mSelectedContactDirectoryId,
+ mSelectedContactLookupKey, mSelectedContactId);
+ getListView().invalidateViews();
+ }
+ }
+
+ // Also, launch a loader to pick up a new lookup URI in case it has changed
+ refreshSelectedContactUri();
+ }
+ }
+
+ private void parseSelectedContactUri() {
+ if (mSelectedContactUri != null) {
+ String directoryParam =
+ mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam) ?
+ ContactsContract.Directory.DEFAULT : Long.parseLong(directoryParam);
+ if (mSelectedContactUri.toString().startsWith(
+ ContactsContract.Contacts.CONTENT_LOOKUP_URI.toString())) {
+ List<String> pathSegments = mSelectedContactUri.getPathSegments();
+ mSelectedContactLookupKey = Uri.encode(pathSegments.get(2));
+ if (pathSegments.size() == 4) {
+ mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
+ }
+ } else if (mSelectedContactUri.toString().startsWith(
+ ContactsContract.Contacts.CONTENT_URI.toString()) &&
+ mSelectedContactUri.getPathSegments().size() >= 2) {
+ mSelectedContactLookupKey = null;
+ mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
+ } else {
+ Log.e(TAG, "Unsupported contact URI: " + mSelectedContactUri);
+ mSelectedContactLookupKey = null;
+ mSelectedContactId = 0;
+ }
+
+ } else {
+ mSelectedContactDirectoryId = ContactsContract.Directory.DEFAULT;
+ mSelectedContactLookupKey = null;
+ mSelectedContactId = 0;
+ }
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ ContactListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return;
+ }
+
+ if (mFilter != null) {
+ adapter.setFilter(mFilter);
+ }
+
+ adapter.setIncludeProfile(false); // TODO: do not need profile
+ }
+
+ @Override
+ public CursorLoader createCursorLoader(Context context) {
+ return new ProfileAndContactsLoader(context);
+ }
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ super.onLoadFinished(loader, data);
+ mSelectionVerified = false;
+
+ // Refresh the currently selected lookup in case it changed while we were sleeping
+ refreshSelectedContactUri();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ super.onLoaderReset(loader);
+ }
+
+ private void checkSelection() {
+ if (mSelectionVerified) {
+ return;
+ }
+
+ if (mRefreshingContactUri) {
+ return;
+ }
+
+ if (isLoadingDirectoryList()) {
+ return;
+ }
+
+ ContactListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return;
+ }
+
+ boolean directoryLoading = true;
+ int count = adapter.getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ CompositeCursorAdapter.Partition partition = adapter.getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directory = (DirectoryPartition) partition;
+ if (directory.getDirectoryId() == mSelectedContactDirectoryId) {
+ directoryLoading = directory.isLoading();
+ break;
+ }
+ }
+ }
+
+ if (directoryLoading) {
+ return;
+ }
+
+ adapter.setSelectedContact(
+ mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId);
+
+ final int selectedPosition = adapter.getSelectedContactPosition();
+ if (selectedPosition != -1) {
+ mLastSelectedPosition = selectedPosition;
+ } else {
+ saveSelectedUri(null);
+ selectDefaultContact();
+ }
+
+ mSelectionVerified = true;
+
+ if (mSelectionPersistenceRequested) {
+ saveSelectedUri(mSelectedContactUri);
+ mSelectionPersistenceRequested = false;
+ }
+
+ if (mSelectionToScreenRequested) {
+ requestSelectionToScreen(selectedPosition);
+ }
+
+ getListView().invalidateViews();
+
+ if (mListener != null) {
+ mListener.onSelectionChange();
+ }
+ }
+
+ protected void selectDefaultContact() {
+ Uri contactUri = null;
+ ContactListAdapter adapter = getAdapter();
+ if (mLastSelectedPosition != -1) {
+ int count = adapter.getCount();
+ int pos = mLastSelectedPosition;
+ if (pos >= count && count > 0) {
+ pos = count - 1;
+ }
+ contactUri = adapter.getContactUri(pos);
+ }
+
+ if (contactUri == null) {
+ contactUri = adapter.getFirstContactUri();
+ }
+
+ setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false);
+ }
+
+ protected void requestSelectionToScreen(int selectedPosition) {
+ if (selectedPosition != -1) {
+ AutoScrollListView listView = (AutoScrollListView)getListView();
+ listView.requestPositionToScreen(
+ selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested);
+ mSelectionToScreenRequested = false;
+ }
+ }
+
+ @Override
+ public boolean isLoading() {
+ return mRefreshingContactUri || super.isLoading();
+ }
+
+ @Override
+ protected void startLoading() {
+ mStartedLoading = true;
+ mSelectionVerified = false;
+ super.startLoading();
+ }
+
+ public void reloadDataAndSetSelectedUri(Uri uri) {
+ setSelectedContactUri(uri, true, true, true, true);
+ reloadData();
+ }
+
+ @Override
+ public void reloadData() {
+ if (mStartedLoading) {
+ mSelectionVerified = false;
+ mLastSelectedPosition = -1;
+ super.reloadData();
+ }
+ }
+
+ public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
+ mListener = listener;
+ }
+
+ public void viewContact(Uri contactUri) {
+ setSelectedContactUri(contactUri, false, false, true, false);
+ if (mListener != null) mListener.onViewContactAction(contactUri);
+ }
+
+ public void deleteContact(Uri contactUri) {
+ if (mListener != null) mListener.onDeleteContactAction(contactUri);
+ }
+
+ private void notifyInvalidSelection() {
+ if (mListener != null) mListener.onInvalidSelection();
+ }
+
+ @Override
+ protected void finish() {
+ super.finish();
+ if (mListener != null) mListener.onFinishAction();
+ }
+
+ private void saveSelectedUri(Uri contactUri) {
+ if (isSearchMode()) {
+ return;
+ }
+
+ ContactListFilter.storeToPreferences(mPrefs, mFilter);
+
+ SharedPreferences.Editor editor = mPrefs.edit();
+ if (contactUri == null) {
+ editor.remove(getPersistentSelectionKey());
+ } else {
+ editor.putString(getPersistentSelectionKey(), contactUri.toString());
+ }
+ editor.apply();
+ }
+
+ private void restoreSelectedUri(boolean willReloadData) {
+ String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null);
+ if (selectedUri == null) {
+ setSelectedContactUri(null, false, false, false, willReloadData);
+ } else {
+ setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData);
+ }
+ }
+
+ private void saveFilter() {
+ ContactListFilter.storeToPreferences(mPrefs, mFilter);
+ }
+
+ private void restoreFilter() {
+ if (mPrefs != null) {
+ mFilter = ContactListFilter.restoreDefaultPreferences(mPrefs);
+ }
+ }
+
+ private String getPersistentSelectionKey() {
+ if (mFilter == null) {
+ return mPersistentSelectionPrefix;
+ } else {
+ return mPersistentSelectionPrefix + "-" + mFilter.getId();
+ }
+ }
+
+ public boolean isOptionsMenuChanged() {
+ // This fragment does not have an option menu of its own
+ return false;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ final Uri uri = getAdapter().getContactUri(position);
+ if (uri == null) {
+ return;
+ }
+ viewContact(uri);
+ }
+
+ @Override
+ protected ContactListAdapter createListAdapter() {
+ DefaultContactListAdapter adapter = new DefaultContactListAdapter(getContext());
+ adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
+ adapter.setDisplayPhotos(true);
+ adapter.setPhotoPosition(
+ ContactListItemView.getDefaultPhotoPosition(/* opposite = */ false));
+
+ return adapter;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mInCallPluginInfo != null) {
+ try {
+ if (view == mLoginBtn) {
+ if (mInCallPluginInfo.mCallMethodInfo.mLoginIntent != null) {
+ mInCallPluginInfo.mCallMethodInfo.mLoginIntent.send();
+ }
+ } else if (view == mEmptyView) {
+ if (mInCallPluginInfo.mCallMethodInfo.mDefaultDirectorySearchIntent != null) {
+ mInCallPluginInfo.mCallMethodInfo.mDefaultDirectorySearchIntent.send();
+ }
+ }
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "PendingIntent exception", e);
+ }
+ }
+ }
+
+ public synchronized void updateInCallPluginInfo(InCallPluginInfo pluginInfo) {
+ mInCallPluginInfo = pluginInfo;
+ updatePluginView();
+ }
+
+ public synchronized void updatePluginView() {
+ if (mInCallPluginInfo != null) {
+ if (DEBUG) Log.d(TAG, "updatePluginView: " + mInCallPluginInfo.mCallMethodInfo.mName);
+ if (mListView != null && mLoginView != null) {
+ if (mInCallPluginInfo.mCallMethodInfo.mIsAuthenticated) {
+ // Show list view
+ mLoginView.setVisibility(View.GONE);
+ mListView.setVisibility(View.VISIBLE);
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ ((ListView) mListView).setEmptyView(mEmptyView);
+ initEmptyView();
+ }
+ } else {
+ // Show login view
+ ((ListView)mListView).setEmptyView(null);
+ mListView.setVisibility(View.GONE);
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ mLoginView.setVisibility(View.VISIBLE);
+ if (mLoginIconView != null) {
+ if (mInCallPluginInfo.mCallMethodInfo.mLoginIconId == 0) {
+ // plugin does not provide a valid icon
+ mLoginIconView.setVisibility(View.GONE);
+ } else {
+ // plugin provides a valid icon, this method is called from the main
+ // thread so needs to start an AsyncTask to look up the Drawable from
+ // resource Id
+ if (mInCallPluginInfo.mCallMethodInfo.mLoginIcon != null) {
+ // TODO: may need to self load in the case of restore before plugin
+ // update
+ mLoginIconView.setImageDrawable(
+ mInCallPluginInfo.mCallMethodInfo.mLoginIcon);
+ } else {
+ if (mInCallPluginInfo.mCallMethodInfo.mLoginIconId != 0) {
+ new GetDrawableAsyncTask().execute();
+ }
+ }
+ }
+ }
+ if (mLoginMsg != null) {
+ if (!TextUtils.isEmpty(mInCallPluginInfo.mCallMethodInfo.mLoginSubtitle)) {
+ mLoginMsg.setText(mInCallPluginInfo.mCallMethodInfo.mLoginSubtitle);
+ } else {
+ mLoginMsg.setText(getResources().getString(R.string.plugin_login_msg,
+ mInCallPluginInfo.mCallMethodInfo.mName));
+ }
+ }
+ }
+ }
+ mPrefsFileName = PREFS_FILE_PREFIX +
+ mInCallPluginInfo.mCallMethodInfo.mComponent.getClassName();
+ if (mPrefs != null) {
+ setFilter(ContactListFilter.createAccountFilter(
+ mInCallPluginInfo.mCallMethodInfo.mAccountType,
+ mInCallPluginInfo.mCallMethodInfo.mAccountHandle,
+ null,
+ null));
+ }
+ }
+ }
+
+ private class GetDrawableAsyncTask extends AsyncTask<Void, Void, Drawable> {
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ Drawable loginIcon = null;
+ if (mInCallPluginInfo.mCallMethodInfo.mLoginIconId != 0) {
+ loginIcon = InCallPluginUtils.getDrawable
+ (PluginContactBrowseListFragment.this.getActivity(),
+ mInCallPluginInfo.mCallMethodInfo.mLoginIconId,
+ mInCallPluginInfo.mCallMethodInfo.mComponent);
+ }
+ return loginIcon;
+ }
+
+ @Override
+ protected void onPostExecute(Drawable icon) {
+ if (icon != null) {
+ mLoginIconView.setImageDrawable(icon);
+ }
+ }
+ }
+
+ private void initEmptyView() {
+ if (mEmptyView != null) {
+ Resources rc = getResources();
+ mEmptyView.setText(rc.getString(R.string.plugin_empty_list_text,
+ TextUtils.isEmpty(mInCallPluginInfo.mCallMethodInfo.mName) ? "" :
+ mInCallPluginInfo.mCallMethodInfo.mName),
+ TextView.BufferType.SPANNABLE);
+ Spannable actionText = new SpannableString(
+ rc.getString(R.string.plugin_empty_list_action_text));
+ actionText.setSpan(new ForegroundColorSpan(rc.getColor(R.color.primary_color)), 0,
+ actionText.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ actionText.setSpan(new StyleSpan(Typeface.BOLD), 0, actionText.length(), Spannable
+ .SPAN_INCLUSIVE_EXCLUSIVE);
+ mEmptyView.append(actionText);
+ mEmptyView.setOnClickListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/contacts/quickcontact/ExpandingEntryCardView.java b/src/com/android/contacts/quickcontact/ExpandingEntryCardView.java
index ba4021b6c..47a62f96a 100644
--- a/src/com/android/contacts/quickcontact/ExpandingEntryCardView.java
+++ b/src/com/android/contacts/quickcontact/ExpandingEntryCardView.java
@@ -29,7 +29,10 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.widget.CardView;
import android.text.Spannable;
+import android.text.SpannableString;
import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
@@ -54,6 +57,8 @@ import android.widget.TextView;
import com.android.contacts.R;
import com.android.contacts.common.dialog.CallSubjectDialog;
+import com.android.phone.common.incall.CallMethodInfo;
+
import java.util.ArrayList;
import java.util.List;
@@ -97,27 +102,34 @@ public class ExpandingEntryCardView extends CardView {
// Button action will open the call with subject dialog.
public static final int ACTION_CALL_WITH_SUBJECT = 3;
- private final int mId;
- private final Drawable mIcon;
- private final String mHeader;
- private final String mSubHeader;
- private final Drawable mSubHeaderIcon;
- private final String mText;
- private final Drawable mTextIcon;
+ private int mId;
+ private Drawable mIcon;
+ private String mHeader;
+ private String mSubHeader;
+ private Drawable mSubHeaderIcon;
+ private String mActionText;
+ private String mText;
+ private Drawable mTextIcon;
private Spannable mPrimaryContentDescription;
- private final Intent mIntent;
- private final Drawable mAlternateIcon;
- private final Intent mAlternateIntent;
- private final String mAlternateContentDescription;
- private final boolean mShouldApplyColor;
- private final boolean mIsEditable;
- private final EntryContextMenuInfo mEntryContextMenuInfo;
- private final Drawable mThirdIcon;
- private final Intent mThirdIntent;
- private final String mThirdContentDescription;
- private final int mIconResourceId;
- private final int mThirdAction;
- private final Bundle mThirdExtras;
+ private Intent mIntent;
+ private Drawable mAlternateIcon;
+ private Intent mAlternateIntent;
+ private String mAlternateContentDescription;
+ private boolean mShouldApplyColor;
+ private boolean mIsEditable;
+ private EntryContextMenuInfo mEntryContextMenuInfo;
+ private String mThirdText;
+ private Drawable mThirdIcon;
+ private Intent mThirdIntent;
+ private String mThirdContentDescription;
+ private int mThirdAction;
+ private Bundle mThirdExtras;
+ private int mIconResourceId;
+ private CallMethodInfo mCallMethodInfo;
+ private List<Entry> mContainerList;
+ private List<List<Entry>> mParentList;
+
+ public Entry () {}
public Entry(int id, Drawable mainIcon, String header, String subHeader,
Drawable subHeaderIcon, String text, Drawable textIcon,
@@ -132,6 +144,7 @@ public class ExpandingEntryCardView extends CardView {
mHeader = header;
mSubHeader = subHeader;
mSubHeaderIcon = subHeaderIcon;
+ mActionText = null;
mText = text;
mTextIcon = textIcon;
mPrimaryContentDescription = primaryContentDescription;
@@ -142,6 +155,7 @@ public class ExpandingEntryCardView extends CardView {
mShouldApplyColor = shouldApplyColor;
mIsEditable = isEditable;
mEntryContextMenuInfo = entryContextMenuInfo;
+ mThirdText = null;
mThirdIcon = thirdIcon;
mThirdIntent = thirdIntent;
mThirdContentDescription = thirdContentDescription;
@@ -150,6 +164,39 @@ public class ExpandingEntryCardView extends CardView {
mIconResourceId = iconResourceId;
}
+ public Entry(int id, Drawable mainIcon, String header, String subHeader, Drawable
+ subHeaderIcon, String actionText, Intent intent, Drawable alternateIcon, Intent
+ alternateIntent, String thirdText, Drawable thirdIcon, Intent thirdIntent, int
+ iconResourceId, CallMethodInfo cmi, List<Entry> containerList,
+ List<List<Entry>> parentList) {
+ mId = id;
+ mIcon = mainIcon;
+ mHeader = header;
+ mSubHeader = subHeader;
+ mSubHeaderIcon = subHeaderIcon;
+ mActionText = actionText;
+ mText = null;
+ mTextIcon = null;
+ mPrimaryContentDescription = null;
+ mIntent = intent;
+ mAlternateIcon = alternateIcon;
+ mAlternateIntent = alternateIntent;
+ mAlternateContentDescription = null;
+ mShouldApplyColor = false;
+ mIsEditable = false;
+ mEntryContextMenuInfo = null;
+ mThirdText = thirdText;
+ mThirdIcon = thirdIcon;
+ mThirdIntent = thirdIntent;
+ mThirdContentDescription = null;
+ mThirdAction = -1;
+ mThirdExtras = null;
+ mIconResourceId = iconResourceId;
+ mCallMethodInfo = cmi;
+ mContainerList = containerList;
+ mParentList = parentList;
+ }
+
Drawable getIcon() {
return mIcon;
}
@@ -166,6 +213,10 @@ public class ExpandingEntryCardView extends CardView {
return mSubHeaderIcon;
}
+ public String getActionText() {
+ return mActionText;
+ }
+
public String getText() {
return mText;
}
@@ -210,6 +261,10 @@ public class ExpandingEntryCardView extends CardView {
return mEntryContextMenuInfo;
}
+ String getThirdText() {
+ return mThirdText;
+ }
+
Drawable getThirdIcon() {
return mThirdIcon;
}
@@ -233,6 +288,18 @@ public class ExpandingEntryCardView extends CardView {
public Bundle getThirdExtras() {
return mThirdExtras;
}
+
+ CallMethodInfo getCallMethodInfo() {
+ return mCallMethodInfo;
+ }
+
+ List<List<Entry>> getParentList() {
+ return mParentList;
+ }
+
+ List<Entry> getContainerList() {
+ return mContainerList;
+ }
}
public interface ExpandingEntryCardViewListener {
@@ -667,12 +734,17 @@ public class ExpandingEntryCardView extends CardView {
Drawable alternateIcon = entry.getAlternateIcon();
if (alternateIcon != null) {
alternateIcon.mutate();
- alternateIcon.setColorFilter(mThemeColorFilter);
+ if (entry.getCallMethodInfo() == null) {
+ // not from a plugin
+ alternateIcon.setColorFilter(mThemeColorFilter);
+ }
}
Drawable thirdIcon = entry.getThirdIcon();
if (thirdIcon != null) {
thirdIcon.mutate();
- thirdIcon.setColorFilter(mThemeColorFilter);
+ if (entry.getCallMethodInfo() == null) {
+ thirdIcon.setColorFilter(mThemeColorFilter);
+ }
}
}
}
@@ -708,7 +780,11 @@ public class ExpandingEntryCardView extends CardView {
final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
if (!TextUtils.isEmpty(entry.getSubHeader())) {
- subHeader.setText(entry.getSubHeader());
+ if (entry.getActionText() == null) {
+ subHeader.setText(entry.getSubHeader());
+ } else {
+ subHeader.setText(entry.getSubHeader(), TextView.BufferType.SPANNABLE);
+ }
} else {
subHeader.setVisibility(View.GONE);
}
@@ -720,6 +796,13 @@ public class ExpandingEntryCardView extends CardView {
subHeaderIcon.setVisibility(View.GONE);
}
+ if (!TextUtils.isEmpty(entry.getActionText())) {
+ Spannable actionText = new SpannableString(" " + entry.getActionText());
+ actionText.setSpan(new ForegroundColorSpan(mThemeColor), 0, actionText.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ subHeader.append(actionText);
+ }
+
final TextView text = (TextView) view.findViewById(R.id.text);
if (!TextUtils.isEmpty(entry.getText())) {
text.setText(entry.getText());
@@ -736,7 +819,7 @@ public class ExpandingEntryCardView extends CardView {
if (entry.getIntent() != null) {
view.setOnClickListener(mOnClickListener);
- view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
+ view.setTag(new EntryTag(entry.getId(), entry.getIntent(), entry));
}
if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
@@ -774,11 +857,12 @@ public class ExpandingEntryCardView extends CardView {
final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
+ final TextView thirdTextView = (TextView) view.findViewById(R.id.third_text);
if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
alternateIcon.setImageDrawable(entry.getAlternateIcon());
alternateIcon.setOnClickListener(mOnClickListener);
- alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
+ alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent(), entry));
alternateIcon.setVisibility(View.VISIBLE);
alternateIcon.setContentDescription(entry.getAlternateContentDescription());
}
@@ -787,7 +871,7 @@ public class ExpandingEntryCardView extends CardView {
thirdIcon.setImageDrawable(entry.getThirdIcon());
if (entry.getThirdAction() == Entry.ACTION_INTENT) {
thirdIcon.setOnClickListener(mOnClickListener);
- thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
+ thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent(), entry));
} else if (entry.getThirdAction() == Entry.ACTION_CALL_WITH_SUBJECT) {
thirdIcon.setOnClickListener(new View.OnClickListener() {
@Override
@@ -809,6 +893,16 @@ public class ExpandingEntryCardView extends CardView {
thirdIcon.setContentDescription(entry.getThirdContentDescription());
}
+ if (!TextUtils.isEmpty(entry.getThirdText()) && entry.getThirdIntent() != null) {
+ thirdTextView.setText(entry.getThirdText());
+ thirdTextView.setOnClickListener(mOnClickListener);
+ thirdTextView.setTag(new EntryTag(entry.getId(), entry.getThirdIntent(), entry));
+ thirdTextView.setTextColor(mThemeColor);
+ thirdTextView.setVisibility(View.VISIBLE);
+ } else {
+ thirdTextView.setVisibility(View.GONE);
+ }
+
// Set a custom touch listener for expanding the extra icon touch areas
view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
@@ -842,8 +936,10 @@ public class ExpandingEntryCardView extends CardView {
&& mEntries.get(0).size() > 1) {
numberOfMimeTypesShown--;
}
- // Inflate badges if not yet created
+ // Inflate badges if not yet created or not up to date
if (mBadges.size() < mEntries.size() - numberOfMimeTypesShown) {
+ mBadges.clear();
+ mBadgeIds.clear();
for (int i = numberOfMimeTypesShown; i < mEntries.size(); i++) {
Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
int badgeResourceId = mEntries.get(i).get(0).getIconResourceId();
@@ -1105,10 +1201,18 @@ public class ExpandingEntryCardView extends CardView {
static final class EntryTag {
private final int mId;
private final Intent mIntent;
+ private final Entry mEntry;
public EntryTag(int id, Intent intent) {
mId = id;
mIntent = intent;
+ mEntry = null;
+ }
+
+ public EntryTag(int id, Intent intent, Entry entry) {
+ mId = id;
+ mIntent = intent;
+ mEntry = entry;
}
public int getId() {
@@ -1118,6 +1222,10 @@ public class ExpandingEntryCardView extends CardView {
public Intent getIntent() {
return mIntent;
}
+
+ public Entry getEntry() {
+ return mEntry;
+ }
}
/**
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 65c4d3cf7..d2ce66ee5 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -23,14 +23,16 @@ import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
+import android.app.PendingIntent;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.ContentValues;
import android.content.ContentUris;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
@@ -47,9 +49,11 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Trace;
+import android.preference.PreferenceManager;
import android.provider.CalendarContract;
import android.os.Handler;
import android.os.Message;
+import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
@@ -61,7 +65,7 @@ import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
@@ -139,8 +143,6 @@ import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
import com.android.contacts.common.model.dataitem.WebsiteDataItem;
import com.android.contacts.common.util.BlockContactHelper;
import com.android.contacts.common.util.ImplicitIntentsUtil;
-import com.android.contacts.common.MoreContactUtils;
-import com.android.contacts.common.SimContactsConstants;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.DateUtils;
import com.android.contacts.common.util.MaterialColorMapUtils;
@@ -150,7 +152,10 @@ import com.android.contacts.common.util.ViewUtil;
import com.android.contacts.detail.ContactDisplayUtils;
import com.android.contacts.editor.ContactEditorFragment;
import com.android.contacts.editor.EditorIntents;
+import com.android.contacts.incall.InCallPluginHelper;
+import com.android.contacts.incall.InCallPluginUtils;
import com.android.contacts.interactions.CalendarInteractionsLoader;
+import com.android.contacts.interactions.CallLogInteraction;
import com.android.contacts.interactions.CallLogInteractionsLoader;
import com.android.contacts.interactions.ContactDeletionInteraction;
import com.android.contacts.interactions.ContactInteraction;
@@ -168,6 +173,10 @@ import com.android.contacts.util.StructuredPostalUtils;
import com.android.contacts.widget.MultiShrinkScroller;
import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
import com.android.contacts.widget.QuickContactImageView;
+import com.android.phone.common.incall.CallMethodHelper;
+import com.android.phone.common.incall.CallMethodInfo;
+import com.cyanogen.ambient.incall.extension.OriginCodes;
+import com.cyanogen.ambient.plugin.PluginStatus;
import com.android.contactsbind.HelpUtils;
import com.cyanogen.lookup.phonenumber.provider.LookupProviderImpl;
@@ -183,9 +192,12 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
@@ -203,6 +215,7 @@ public class QuickContactActivity extends ContactsActivity implements
public static final int MODE_FULLY_EXPANDED = 4;
private static final String TAG = "QuickContact";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String KEY_THEME_COLOR = "theme_color";
@@ -215,6 +228,16 @@ public class QuickContactActivity extends ContactsActivity implements
/** This is the Intent action to install a shortcut in the launcher. */
private static final String ACTION_INSTALL_SHORTCUT =
"com.android.launcher.action.INSTALL_SHORTCUT";
+ public static final String ACTION_INCALL_PLUGIN_LOGIN =
+ "com.android.contacts.quickcontact.INCALL_PLUGIN_LOGIN";
+ public static final String ACTION_INCALL_PLUGIN_INVITE =
+ "com.android.contacts.quickcontact.INCALL_PLUGIN_INVITE";
+ public static final String ACTION_INCALL_PLUGIN_DIRECTORY_SEARCH =
+ "com.android.contacts.quickcontact.INCALL_PLUGIN_DIRECTORY_SEARCH";
+ public static final String ACTION_INCALL_PLUGIN_INSTALL =
+ "com.android.contacts.quickcontact.ACTION_INCALL_PLUGIN_INSTALL";
+ public static final String ACTION_INCALL_PLUGIN_DISMISS_NUDGE =
+ "com.android.contacts.quickcontact.ACTION_INCALL_PLUGIN_DISMISS_NUDGE";
@SuppressWarnings("deprecation")
private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
@@ -250,8 +273,11 @@ public class QuickContactActivity extends ContactsActivity implements
private ExpandingEntryCardView mAboutCard;
private MultiShrinkScroller mScroller;
private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
- private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask;
+ private AsyncTask<Contact, Void, Cp2DataCardModel> mEntriesAndActionsTask;
private AsyncTask<Void, Void, Void> mRecentDataTask;
+ private AtomicBoolean mIsUpdating;
+ private boolean mNeedPluginUpdate = false;
+ private static final String CALL_METHOD_SUBSCRIBER_ID = TAG;
/**
* The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}.
*/
@@ -283,7 +309,7 @@ public class QuickContactActivity extends ContactsActivity implements
private Target mContactBitmapTarget;
private BlockContactHelper mBlockContactHelper;
-
+ private Object mLock = new Object();
/**
* {@link #LEADING_MIMETYPES} is used to sort MIME-types.
*
@@ -307,11 +333,23 @@ public class QuickContactActivity extends ContactsActivity implements
Identity.CONTENT_ITEM_TYPE,
Note.CONTENT_ITEM_TYPE);
+ // Common mime types that are loaded and present to users before plugins load to avoid delays
+ private static final List<String> COMMON_MIMETYPES = Lists.newArrayList(
+ Email.CONTENT_ITEM_TYPE,
+ Nickname.CONTENT_ITEM_TYPE,
+ Phone.CONTENT_ITEM_TYPE,
+ SipAddress.CONTENT_ITEM_TYPE,
+ StructuredName.CONTENT_ITEM_TYPE,
+ StructuredPostal.CONTENT_ITEM_TYPE);
+
private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
/** Id for the background contact loader */
private static final int LOADER_CONTACT_ID = 0;
+ private static final String KEY_LOADER_EXTRA_PLUGIN_INFO =
+ QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PLUGIN_INFO";
+
private static final String KEY_LOADER_EXTRA_PHONES =
QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
@@ -336,7 +374,8 @@ public class QuickContactActivity extends ContactsActivity implements
private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2;
-
+ private static final int CARD_ENTRY_ID_INCALL_PLUGIN = -3;
+ public static final int CARD_ENTRY_ID_INCALL_PLUGIN_CALL = -4;
private static final int MAX_NUM_LENGTH = 3; // add limit length to show IP call item
private static final int[] mRecentLoaderIds = new int[]{
@@ -363,13 +402,28 @@ public class QuickContactActivity extends ContactsActivity implements
}
final EntryTag entryTag = (EntryTag) entryTagObject;
final Intent intent = entryTag.getIntent();
- final int dataId = entryTag.getId();
+ int dataId = entryTag.getId();
if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) {
editContact();
return;
}
+ // InCall plugin invite or nudges entries
+ if (dataId == CARD_ENTRY_ID_INCALL_PLUGIN) {
+ handleInCallPluginAction(entryTag);
+ return;
+ }
+
+ boolean isPlugin = false;
+ // InCall plugin callable entries, need to retrieve the data id
+ if (dataId == CARD_ENTRY_ID_INCALL_PLUGIN_CALL) {
+ dataId = (int) intent.getLongExtra(InCallPluginUtils.KEY_DATA_ID, -1);
+ if (intent.getAction() == null) {
+ isPlugin = true;
+ }
+ }
+
// Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id
// so the exact usage type is not necessary in all cases
String usageType = DataUsageFeedback.USAGE_TYPE_CALL;
@@ -414,7 +468,29 @@ public class QuickContactActivity extends ContactsActivity implements
mHasIntentLaunched = true;
try {
- ImplicitIntentsUtil.startActivityInAppIfPossible(QuickContactActivity.this, intent);
+ if (isPlugin) {
+ // it's an entry from plugin, route to this call
+ CallMethodInfo cmi = null;
+ if (entryTag.getEntry() == null ||
+ entryTag.getEntry().getCallMethodInfo() == null) {
+ cmi = InCallPluginHelper.getCallMethod(ComponentName.unflattenFromString(
+ intent.getStringExtra(InCallPluginUtils.KEY_COMPONENT)));
+ cmi.placeCall(OriginCodes.CONTACTS_CARD,
+ intent.getStringExtra(InCallPluginUtils.KEY_NUMBER),
+ getBaseContext(), false, false,
+ intent.getStringExtra(InCallPluginUtils.KEY_MIMETYPE));
+ } else {
+ cmi = entryTag.getEntry().getCallMethodInfo();
+ cmi.placeCall(OriginCodes.CONTACTS_CARD,
+ intent.getStringExtra(InCallPluginUtils.KEY_NUMBER),
+ getBaseContext(), false,
+ TextUtils.isEmpty(intent.getStringExtra(InCallPluginUtils
+ .KEY_MIMETYPE)), null);
+ }
+ } else {
+ ImplicitIntentsUtil
+ .startActivityInAppIfPossible(QuickContactActivity.this, intent);
+ }
} catch (SecurityException ex) {
Toast.makeText(QuickContactActivity.this, R.string.missing_app,
Toast.LENGTH_SHORT).show();
@@ -872,6 +948,7 @@ public class QuickContactActivity extends ContactsActivity implements
}
});
}
+ mIsUpdating = new AtomicBoolean(false);
processIntent(getIntent());
mBlockContactHelper = new BlockContactHelper(this, new LookupProviderImpl(this));
if (mContactData != null) {
@@ -930,7 +1007,18 @@ public class QuickContactActivity extends ContactsActivity implements
mLookupUri = lookupUri;
mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
+ Contact contact = null;
+ if (mLookupUri == null) {
+ // See if a URI has been attached as an extra
+ mLookupUri = intent.getParcelableExtra(Contact.CONTACT_URI_EXTRA);
+ if (mLookupUri != null) {
+ contact = ContactLoader.parseEncodedContactEntity(mLookupUri,
+ ContactLoader.EncodedContactEntitySchemaVersion.ENHANCED_CALLER_META_DATA);
+ }
+ }
+
if (mLookupUri == null) {
+ if (DEBUG) Log.d(TAG, "mLookupUri null!");
finish();
return;
}
@@ -943,7 +1031,8 @@ public class QuickContactActivity extends ContactsActivity implements
}
if (contact != null) {
- bindContactData(contact);
+ // looked up encoded contact
+ checkAndBindContactData(contact, false, false);
} else if (oldLookupUri == null) {
mContactLoader = (ContactLoader) getLoaderManager().initLoader(
LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
@@ -1076,36 +1165,58 @@ public class QuickContactActivity extends ContactsActivity implements
Trace.endSection();
- mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() {
+ mEntriesAndActionsTask = new EntriesAndActionTask().execute(data);
+ }
- @Override
- protected Cp2DataCardModel doInBackground(
- Void... params) {
- return generateDataModelFromContact(data);
+ private class EntriesAndActionTask extends AsyncTask<Contact, Void, Cp2DataCardModel> {
+ private Contact mData;
+
+ @Override
+ protected Cp2DataCardModel doInBackground(Contact... params) {
+ // gather plugin information
+ if (DEBUG) Log.d(TAG, "+++doInBackground");
+ if (isCancelled()) {
+ // onCancelled will be called upon return
+ if (DEBUG) Log.d(TAG, "+++doInBackground cancelled");
+ return null;
}
+ mData = params[0];
+ Cp2DataCardModel model = generateDataModelFromContact(mData);
+ if (DEBUG) Log.d(TAG, "---doInBackground");
+ return model;
+ }
- @Override
- protected void onPostExecute(Cp2DataCardModel cardDataModel) {
- super.onPostExecute(cardDataModel);
- // Check that original AsyncTask parameters are still valid and the activity
- // is still running before binding to UI. A new intent could invalidate
- // the results, for example.
- if (data == mContactData && !isCancelled()) {
- bindDataToCards(cardDataModel);
- showActivity();
- }
+ @Override
+ protected void onPostExecute(Cp2DataCardModel cardDataModel) {
+ super.onPostExecute(cardDataModel);
+ // Check that original AsyncTask parameters are still valid and the activity
+ // is still running before binding to UI. A new intent could invalidate
+ // the results, for example.
+ if (DEBUG) Log.d(TAG, "+++onPostExecute");
+ if (mData == mContactData && !isCancelled() && cardDataModel != null) {
+ bindDataToCards(cardDataModel);
+ showActivity();
+ mIsUpdating.set(false);
}
- };
- mEntriesAndActionsTask.execute();
+ if (mNeedPluginUpdate) {
+ mNeedPluginUpdate = false;
+ updatePlugins(null);
+ }
+ if (DEBUG) Log.d(TAG, "---onPostExecute");
+ }
}
private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
+ if (DEBUG) Log.d(TAG, "+++bindDataToCards");
startInteractionLoaders(cp2DataCardModel);
populateContactAndAboutCard(cp2DataCardModel);
+ if (DEBUG) Log.d(TAG, "---bindDataToCards");
}
private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) {
final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap;
+ final Map<ComponentName, List<String>> pluginAccountsMap = cp2DataCardModel
+ .pluginAccountsMap;
final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
if (phoneDataItems != null && phoneDataItems.size() == 1) {
mOnlyOnePhoneNumber = true;
@@ -1119,7 +1230,7 @@ public class QuickContactActivity extends ContactsActivity implements
}
final Bundle phonesExtraBundle = new Bundle();
phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
-
+ phonesExtraBundle.putSerializable(KEY_LOADER_EXTRA_PLUGIN_INFO, (HashMap)pluginAccountsMap);
Trace.beginSection("start sms loader");
getLoaderManager().initLoader(
LOADER_SMS_ID,
@@ -1190,6 +1301,12 @@ public class QuickContactActivity extends ContactsActivity implements
@Override
protected void onResume() {
super.onResume();
+ if (InCallPluginHelper.subscribe(CALL_METHOD_SUBSCRIBER_ID, pluginsUpdatedReceiver)) {
+ if (DEBUG) Log.d(TAG, "InCallPluginHelper infoReady");
+ InCallPluginHelper.refreshDynamicItems();
+ } else {
+ if (DEBUG) Log.d(TAG, "InCallPluginHelper info NOT Ready");
+ }
// If returning from a launched activity, repopulate the contact and about card
if (mHasIntentLaunched) {
mHasIntentLaunched = false;
@@ -1205,7 +1322,14 @@ public class QuickContactActivity extends ContactsActivity implements
}
}
- private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) {
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ InCallPluginHelper.unsubscribe(CALL_METHOD_SUBSCRIBER_ID);
+ }
+
+ private synchronized void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) {
mCachedCp2DataCardModel = cp2DataCardModel;
if (mHasIntentLaunched || cp2DataCardModel == null) {
return;
@@ -1283,7 +1407,7 @@ public class QuickContactActivity extends ContactsActivity implements
mExpandingEntryCardViewListener,
mScroller);
- if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) {
+ if ((contactCardEntries.size() == 0 && aboutCardEntries.size() == 0)) {
initializeNoContactDetailCard();
} else {
mNoContactDetailsCard.setVisibility(View.GONE);
@@ -1379,13 +1503,31 @@ public class QuickContactActivity extends ContactsActivity implements
* amongst mimetype. The map goes from mimetype string to the sorted list of data items within
* mimetype
*/
- private Cp2DataCardModel generateDataModelFromContact(
- Contact data) {
+ private Cp2DataCardModel generateDataModelFromContact(Contact data) {
Trace.beginSection("Build data items map");
final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
final ResolveCache cache = ResolveCache.getInstance(this);
+ Set<String> pluginMimeExcluded;
+ Set<String> pluginMimeIncluded;
+ if (InCallPluginHelper.infoReady()) {
+ mCallMethodMap = (HashMap<ComponentName, CallMethodInfo>)
+ InCallPluginHelper.getAllEnabledAndHiddenCallMethods();
+ pluginMimeExcluded = InCallPluginHelper.getAllEnabledVideoImMimeSet();
+ pluginMimeIncluded = InCallPluginHelper.getAllEnabledVoiceMimeSet();
+ if (DEBUG) {
+ Log.d(TAG, "plugins size:" + mCallMethodMap.size());
+ Log.d(TAG, "mimeExcluded size:" + pluginMimeExcluded.size());
+ Log.d(TAG, "mimeIncluded size:" + pluginMimeIncluded.size());
+ }
+ } else {
+ mCallMethodMap = new HashMap<ComponentName, CallMethodInfo>();
+ pluginMimeExcluded = new HashSet<String>();
+ pluginMimeIncluded = new HashSet<String>();
+ }
+
+ HashMap<String, RawContact> pluginRawContactsMap = new HashMap<String, RawContact>();
for (RawContact rawContact : data.getRawContacts()) {
for (DataItem dataItem : rawContact.getDataItems()) {
dataItem.setRawContactId(rawContact.getId());
@@ -1402,6 +1544,11 @@ public class QuickContactActivity extends ContactsActivity implements
final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
dataKind));
+ // the mime type has been consolidated in the plugin entry, skip
+ if (pluginMimeExcluded.contains(mimeType)) continue;
+ if (pluginMimeIncluded.contains(mimeType)) {
+ pluginRawContactsMap.put(mimeType, rawContact);
+ }
if (isMimeExcluded(mimeType) || !hasData) continue;
@@ -1443,13 +1590,25 @@ public class QuickContactActivity extends ContactsActivity implements
final List<List<Entry>> contactCardEntries = new ArrayList<>();
final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap);
final MutableString aboutCardName = new MutableString();
-
+ HashMap<ComponentName, List<String>> pluginAccountsMap = new HashMap<ComponentName,
+ List<String>>();
for (int i = 0; i < dataItemsList.size(); ++i) {
final List<DataItem> dataItemsByMimeType = dataItemsList.get(i);
final DataItem topDataItem = dataItemsByMimeType.get(0);
if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
// About card mimetypes are built in buildAboutCardEntries, skip here
continue;
+ } else if (pluginRawContactsMap.containsKey(topDataItem.getMimeType())) {
+ List<Entry> pluginEntries = incallPluginDataItemsToEntries(dataItemsByMimeType,
+ data, pluginRawContactsMap.get(topDataItem.getMimeType()),
+ contactCardEntries, pluginAccountsMap);
+ if (pluginEntries.size() > 0) {
+ if (DEBUG) {
+ Log.d(TAG, "pluginEntries added to contactCardEntries:" +
+ pluginEntries.size());
+ }
+ contactCardEntries.add(pluginEntries);
+ }
} else {
List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i),
aboutCardName);
@@ -1458,6 +1617,10 @@ public class QuickContactActivity extends ContactsActivity implements
}
}
}
+ if (!mContactData.isUserProfile() && InCallPluginHelper.infoReady() && mCallMethodMap
+ .size() > 0) {
+ addAllInCallPluginOtherEntries(contactCardEntries, pluginRawContactsMap);
+ }
Trace.endSection();
@@ -1466,6 +1629,7 @@ public class QuickContactActivity extends ContactsActivity implements
dataModel.aboutCardEntries = aboutCardEntries;
dataModel.contactCardEntries = contactCardEntries;
dataModel.dataItemsMap = dataItemsMap;
+ dataModel.pluginAccountsMap = pluginAccountsMap;
return dataModel;
}
@@ -1479,9 +1643,11 @@ public class QuickContactActivity extends ContactsActivity implements
* are in sorted order using mWithinMimeTypeDataItemComparator.
*/
public Map<String, List<DataItem>> dataItemsMap;
+ public Map<String, List<DataItem>> hideDataItemsMap;
public List<List<Entry>> aboutCardEntries;
public List<List<Entry>> contactCardEntries;
public String customAboutCardName;
+ public Map<ComponentName, List<String>> pluginAccountsMap;
}
private static class MutableString {
@@ -2220,7 +2386,18 @@ public class QuickContactActivity extends ContactsActivity implements
if (interaction == null) {
continue;
}
- entries.add(new Entry(/* id = */ -1,
+ boolean applyColor = true;
+ int dataId = -1;
+ // only CallLogInteraction classes can be from plugin (not SmsInteraction
+ // and CalendarInteraction)
+ if (CallLogInteraction.class.isInstance(interaction) && !TextUtils.isEmpty(
+ ((CallLogInteraction)interaction).getPluginPkgName())) {
+ applyColor = false;
+ dataId = CARD_ENTRY_ID_INCALL_PLUGIN_CALL;
+ }
+
+ // need to associate number as well
+ entries.add(new Entry(dataId,
interaction.getIcon(this),
interaction.getViewHeader(this),
interaction.getViewBody(this),
@@ -2232,7 +2409,7 @@ public class QuickContactActivity extends ContactsActivity implements
/* alternateIcon = */ null,
/* alternateIntent = */ null,
/* alternateContentDescription = */ null,
- /* shouldApplyColor = */ true,
+ applyColor,
/* isEditable = */ false,
/* EntryContextMenuInfo = */ null,
/* thirdIcon = */ null,
@@ -2276,10 +2453,7 @@ public class QuickContactActivity extends ContactsActivity implements
finish();
return;
}
- mBlockContactHelper.setContactInfo(data);
- mBlockContactHelper.gatherDataInBackground();
- bindContactData(data);
-
+ checkAndBindContactData(data, true, false);
} finally {
Trace.endSection();
}
@@ -2298,6 +2472,33 @@ public class QuickContactActivity extends ContactsActivity implements
}
};
+ private synchronized void checkAndBindContactData(Contact contact, boolean withBlockHelper,
+ boolean onlyStartAsyncTask) {
+ if (DEBUG) Log.d(TAG, "checkAndBindContactData," + withBlockHelper + " " +
+ onlyStartAsyncTask);
+ // Update pending Intents
+ InCallPluginHelper.refreshPendingIntents(InCallPluginUtils.getInCallContactInfo(contact));
+
+ if (mIsUpdating.get() && mEntriesAndActionsTask != null && !mEntriesAndActionsTask
+ .isCancelled()) {
+ mEntriesAndActionsTask.cancel(true);
+ }
+ mIsUpdating.set(true);
+ if (withBlockHelper) {
+ destroyInteractionLoaders();
+ mBlockContactHelper.setContactInfo(contact);
+ mBlockContactHelper.gatherDataInBackground();
+ bindContactData(contact);
+ } else {
+ destroyInteractionLoaders();
+ if (onlyStartAsyncTask) {
+ mEntriesAndActionsTask = new EntriesAndActionTask().execute(contact);
+ } else {
+ bindContactData(contact);
+ }
+ }
+ }
+
@Override
public void onBackPressed() {
if (mScroller != null) {
@@ -2351,6 +2552,7 @@ public class QuickContactActivity extends ContactsActivity implements
loader = new CallLogInteractionsLoader(
QuickContactActivity.this,
args.getStringArray(KEY_LOADER_EXTRA_PHONES),
+ (HashMap) args.getSerializable(KEY_LOADER_EXTRA_PLUGIN_INFO),
MAX_CALL_LOG_RETRIEVE);
}
return loader;
@@ -2461,6 +2663,7 @@ public class QuickContactActivity extends ContactsActivity implements
protected void onStop() {
super.onStop();
+ mIsUpdating.set(false);
if (mEntriesAndActionsTask != null) {
// Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
// results on the UI thread. In some circumstances Activities are killed without
@@ -3245,4 +3448,286 @@ public class QuickContactActivity extends ContactsActivity implements
return true;
return false;
}
+
+ private void addAllInCallPluginOtherEntries(List<List<Entry>> parentList,
+ HashMap<String, RawContact> pluginRawContactMap) {
+ for (ComponentName cn : mCallMethodMap.keySet()) {
+ CallMethodInfo cmi = mCallMethodMap.get(cn);
+ addInCallPluginEntries(cmi, parentList, pluginRawContactMap.containsKey(cmi.mMimeType));
+ }
+ }
+
+ private void addInCallPluginEntries(CallMethodInfo cmi, List<List<Entry>> parentList,
+ boolean hasPluginAccount) {
+ final Resources res = getResources();
+ List<Entry> containerList;
+ Entry entry;
+ if (cmi.mStatus == PluginStatus.HIDDEN) {
+ if (cmi.mLoginNudgeEnable) {
+ containerList = new ArrayList<Entry>();
+ // install nudge
+ entry = new Entry(CARD_ENTRY_ID_INCALL_PLUGIN,
+ cmi.mBrandIcon,
+ null,
+ cmi.mInstallNudgeSubtitle,
+ null,
+ cmi.mInstallNudgeActionText,
+ new Intent(ACTION_INCALL_PLUGIN_INSTALL),
+ res.getDrawable(R.drawable.ic_close),
+ new Intent(ACTION_INCALL_PLUGIN_DISMISS_NUDGE),
+ null,
+ null,
+ null,
+ cmi.mBrandIconId,
+ cmi,
+ containerList, parentList);
+ if (DEBUG) Log.d(TAG, "Adding INSTALL NUDGE");
+ containerList.add(entry);
+ parentList.add(containerList);
+ }
+ } else if (cmi.mStatus == PluginStatus.ENABLED) {
+ if (!hasPluginAccount) {
+ if (cmi.mIsAuthenticated) {
+ // Invite
+ containerList = new ArrayList<Entry>();
+ entry = new Entry(CARD_ENTRY_ID_INCALL_PLUGIN,
+ cmi.mBrandIcon,
+ res.getString(R.string.incall_plugin_directory_search, cmi.mName),
+ null,
+ null,
+ null,
+ new Intent(ACTION_INCALL_PLUGIN_DIRECTORY_SEARCH),
+ null,
+ null,
+ res.getString(R.string.incall_plugin_invite),
+ null,
+ new Intent(ACTION_INCALL_PLUGIN_INVITE),
+ cmi.mBrandIconId,
+ cmi,
+ containerList, parentList);
+ if (DEBUG) Log.d(TAG, "Adding INVITE ENTRY");
+ containerList.add(entry);
+ parentList.add(containerList);
+ } else {
+ // login nudge
+ if (cmi.mLoginNudgeEnable) {
+ containerList = new ArrayList<Entry>();
+ entry = new Entry(CARD_ENTRY_ID_INCALL_PLUGIN,
+ cmi.mBrandIcon,
+ null,
+ cmi.mInstallNudgeSubtitle,
+ null,
+ cmi.mInstallNudgeActionText,
+ new Intent(ACTION_INCALL_PLUGIN_LOGIN),
+ res.getDrawable(R.drawable.ic_close),
+ new Intent(ACTION_INCALL_PLUGIN_DISMISS_NUDGE),
+ null,
+ null,
+ null,
+ cmi.mBrandIconId,
+ cmi,
+ containerList, parentList);
+ if (DEBUG) Log.d(TAG, "Adding LOGIN NUDGE");
+ containerList.add(entry);
+ parentList.add(containerList);
+ }
+ }
+ }
+ }
+ }
+
+ // Create new Entries
+ private List<Entry> incallPluginDataItemsToEntries(List<DataItem> dataItems, Contact contact,
+ RawContact rawContact, List<List<Entry>> parentList, HashMap<ComponentName,
+ List<String>> pluginAccountMap) {
+ List<Entry> entries = new ArrayList<Entry>();
+ for (DataItem dataItem : dataItems) {
+ CallMethodInfo cmi =
+ InCallPluginHelper.getMethodForMimeType(dataItem.getMimeType(), true);
+ Entry entry;
+ String contactAccountHandle = rawContact.getSourceId();
+ if (cmi.mIsAuthenticated) {
+ // user signed in, consolidate entries
+ InCallPluginUtils.PresenceInfo presenceInfo = InCallPluginUtils.lookupPresenceInfo(
+ this, contact, rawContact);
+ Intent callIntent = InCallPluginUtils.getVoiceMimeIntent(cmi.mMimeType, dataItem,
+ cmi, contactAccountHandle);
+ entry = new Entry(CARD_ENTRY_ID_INCALL_PLUGIN_CALL,
+ cmi.mBrandIcon,
+ contactAccountHandle,
+ presenceInfo.mStatusMsg,
+ presenceInfo.mPresenceIcon,
+ null,
+ callIntent,
+ cmi.mImIcon,
+ InCallPluginUtils.getImMimeIntent(cmi.mImMimeType, rawContact),
+ null,
+ cmi.mVoiceIcon,
+ callIntent,
+ cmi.mBrandIconId,
+ cmi,
+ entries, parentList);
+ if (DEBUG) Log.d(TAG, "Adding CALL ENTRY");
+ } else {
+ // user signed out, list the contact's account name, and action set to
+ // launch sign in
+ final Resources res = getResources();
+ entry = new Entry(CARD_ENTRY_ID_INCALL_PLUGIN,
+ cmi.mBrandIcon,
+ contactAccountHandle,
+ res.getString(R.string.incall_plugin_account_subheader, cmi.mName),
+ null, null,
+ new Intent(ACTION_INCALL_PLUGIN_LOGIN),
+ null, null, null, null,
+ null, cmi.mBrandIconId,
+ cmi, entries, parentList);
+ if (DEBUG) Log.d(TAG, "Adding ACCOUNT ENTRY");
+ }
+ entries.add(entry);
+ // gather account list for interactions
+ List<String> accountsList;
+ if (!pluginAccountMap.containsKey(cmi.mComponent)) {
+ accountsList = new ArrayList<String>();
+ pluginAccountMap.put(cmi.mComponent, accountsList);
+ } else {
+ accountsList = pluginAccountMap.get(cmi.mComponent);
+ }
+ accountsList.add(contactAccountHandle);
+
+ }
+ return entries;
+ }
+
+ private void handleInCallPluginAction(EntryTag tag) {
+ if (tag == null) {
+ return;
+ }
+ Entry entry = tag.getEntry();
+ if (entry == null) {
+ return;
+ }
+ CallMethodInfo cmiStored = entry.getCallMethodInfo();
+ CallMethodInfo cmi = InCallPluginHelper.getCallMethod(cmiStored.mComponent);
+ Intent intent = tag.getIntent();
+ if (cmi == null || intent == null) {
+ return;
+ }
+ try {
+ if(intent.getAction().equals(ACTION_INCALL_PLUGIN_INSTALL)) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" +
+ cmi.mDependentPackage)));
+ } else if (intent.getAction().equals(ACTION_INCALL_PLUGIN_DISMISS_NUDGE)) {
+ String nudgeKey = intent.getStringExtra(InCallPluginUtils
+ .KEY_NUDGE_KEY);
+ dismissNudge(tag, nudgeKey);
+ } else if (intent.getAction().equals(ACTION_INCALL_PLUGIN_LOGIN)) {
+ if (cmi.mLoginIntent != null) {
+ cmi.mLoginIntent.send();
+ }
+ } else if (intent.getAction().equals(ACTION_INCALL_PLUGIN_INVITE)) {
+ if (cmi.mInviteIntent != null) {
+ cmi.mInviteIntent.send();
+ } else {
+ cmi.mInviteIntent = InCallPluginHelper.getInviteIntentSync(cmi.mComponent,
+ InCallPluginUtils.getInCallContactInfo(mContactData));
+ if (cmi.mInviteIntent != null) {
+ cmi.mInviteIntent.send();
+ }
+ }
+ } else if (intent.getAction().equals(ACTION_INCALL_PLUGIN_DIRECTORY_SEARCH)) {
+ if (cmi.mDirectorySearchIntent != null) {
+ cmi.mDirectorySearchIntent.send();
+ } else {
+ cmi.mDirectorySearchIntent =
+ InCallPluginHelper.getDirectorySearchIntentSync(cmi.mComponent,
+ InCallPluginUtils.getInCallContactInfo(mContactData).mLookupUri);
+ if (cmi.mDirectorySearchIntent != null) {
+ cmi.mDirectorySearchIntent.send();
+ }
+ }
+ }
+ } catch (PendingIntent.CanceledException e) {
+ if (DEBUG) Log.d(TAG, "handleInCallPluginAction ", e);
+ }
+ }
+
+ private void dismissNudge(EntryTag tag, String nudgeKey) {
+ // Write the dimissal to preferences
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = sp.edit();
+ if (tag == null) {
+ return;
+ }
+ Entry entry = tag.getEntry();
+ CallMethodInfo cmi = entry.getCallMethodInfo();
+ final List<List<Entry>> cardEntries = entry.getParentList();
+ List<Entry> containerList = entry.getContainerList();
+
+ // disable the nudge type
+ editor.putBoolean(cmi.mComponent.getClassName() + "." + nudgeKey, false);
+ editor.apply();
+ // Find the entry and remove it
+ cardEntries.remove(containerList);
+
+ // Repopulate the view
+ mContactCard.initialize(cardEntries,
+ MIN_NUM_CONTACT_ENTRIES_SHOWN,
+ mContactCard.isExpanded(),
+ false,
+ mExpandingEntryCardViewListener,
+ mScroller);
+ mContactCard.setVisibility(View.VISIBLE);
+ }
+
+ private CallMethodHelper.CallMethodReceiver pluginsUpdatedReceiver =
+ new CallMethodHelper.CallMethodReceiver() {
+ @Override
+ public void onChanged(HashMap<ComponentName, CallMethodInfo> callMethodInfos) {
+ updatePlugins(callMethodInfos);
+ }
+ };
+
+ // Global CallMethod map that keeps track of the currently displayed plugins
+ HashMap<ComponentName, CallMethodInfo> mCallMethodMap = new HashMap<ComponentName, CallMethodInfo>();
+
+ private void updatePlugins(HashMap<ComponentName, CallMethodInfo> callMethods) {
+ if (DEBUG) Log.d(TAG, "+++updatePlugins");
+ HashMap<ComponentName, CallMethodInfo> newCmMap = (HashMap<ComponentName, CallMethodInfo>)
+ InCallPluginHelper.getAllEnabledAndHiddenCallMethods();
+ boolean updateNeeded = false;
+ if (mContactData == null) {
+ return;
+ }
+ if (mIsUpdating.get() || mCachedCp2DataCardModel == null) {
+ if (DEBUG) Log.d(TAG, "---updatePlugins null return");
+ checkAndBindContactData(mContactData, false, true);
+ return;
+ }
+ // Check if update or removal is needed
+ for (ComponentName cn : mCallMethodMap.keySet()) {
+ CallMethodInfo cmi = mCallMethodMap.get(cn);
+ if (newCmMap.containsKey(cn)) {
+ // Check if update needed
+ CallMethodInfo newCmi = newCmMap.remove(cn);
+ if (!newCmi.equals(cmi) || newCmi.mIsAuthenticated != cmi.mIsAuthenticated) {
+ updateNeeded = true;
+ }
+ } else {
+ // need to remove plugins
+ if (DEBUG) Log.d(TAG, "updatePlugins removed");
+ updateNeeded = true;
+ }
+ }
+ // Check if need to add new plugins
+ if (newCmMap.size() > 0) {
+ if (DEBUG) Log.d(TAG, "updatePlugins new plugins");
+ updateNeeded = true;
+ }
+
+ if (updateNeeded) {
+ if (DEBUG) Log.d(TAG, "updatePlugins updateNeeded");
+ checkAndBindContactData(mContactData, false, true);
+ }
+ if (DEBUG) Log.d(TAG, "---updatePlugins return");
+ }
}