diff options
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"); + } } |