summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLinux Build Service Account <lnxbuild@localhost>2014-12-23 18:22:37 -0800
committerGerrit - the friendly Code Review server <code-review@localhost>2014-12-23 18:22:37 -0800
commit6f539d101434b7bab4826d8bb3283c92cce8802b (patch)
tree29d8e041210c19577e8eb600d7733384aee28d15
parent2b13656c19836dd1843ba3d13544264267952e98 (diff)
parent3a3d43023d6c85884b980739e6e9737dc5751530 (diff)
downloadandroid_packages_apps_ContactsCommon-6f539d101434b7bab4826d8bb3283c92cce8802b.tar.gz
android_packages_apps_ContactsCommon-6f539d101434b7bab4826d8bb3283c92cce8802b.tar.bz2
android_packages_apps_ContactsCommon-6f539d101434b7bab4826d8bb3283c92cce8802b.zip
Merge "Merge commit 'b5ad7c99c3e990868042b13cd31c532b6102e0cd' into HEAD"
-rw-r--r--res/drawable-hdpi/ic_action_call.pngbin0 -> 1094 bytes
-rw-r--r--res/drawable-hdpi/ic_check_wht_24dp.pngbin0 -> 341 bytes
-rw-r--r--res/drawable-mdpi/ic_action_call.pngbin0 -> 741 bytes
-rw-r--r--res/drawable-mdpi/ic_check_wht_24dp.pngbin0 -> 295 bytes
-rw-r--r--res/drawable-xhdpi/ic_action_call.pngbin0 -> 1489 bytes
-rw-r--r--res/drawable-xhdpi/ic_check_wht_24dp.pngbin0 -> 402 bytes
-rw-r--r--res/drawable-xxhdpi/ic_action_call.pngbin0 -> 2245 bytes
-rw-r--r--res/drawable-xxhdpi/ic_check_wht_24dp.pngbin0 -> 449 bytes
-rw-r--r--res/drawable/call_background_activated.xml10
-rw-r--r--res/drawable/call_background_holo.xml10
-rw-r--r--res/drawable/ic_action_call_background.xml27
-rw-r--r--res/values/attrs.xml4
-rw-r--r--res/values/strings.xml4
-rw-r--r--src/com/android/contacts/common/CallUtil.java41
-rwxr-xr-xsrc/com/android/contacts/common/interactions/ImportSIMContactsDialogFragment.java36
-rwxr-xr-x[-rw-r--r--]src/com/android/contacts/common/list/ContactEntryListAdapter.java9
-rwxr-xr-xsrc/com/android/contacts/common/list/ContactEntryListFragment.java9
-rwxr-xr-x[-rw-r--r--]src/com/android/contacts/common/list/ContactListAdapter.java44
-rwxr-xr-x[-rw-r--r--]src/com/android/contacts/common/list/ContactListItemView.java183
-rwxr-xr-xsrc/com/android/contacts/common/list/DefaultContactListAdapter.java3
-rw-r--r--src/com/android/contacts/common/preference/ContactsPreferences.java28
-rw-r--r--src/com/android/contacts/common/widget/CheckableFlipDrawable.java220
-rw-r--r--src/com/android/contacts/common/widget/CheckableImageView.java103
-rw-r--r--src/com/android/contacts/common/widget/CheckableQuickContactBadge.java103
-rw-r--r--src/com/android/contacts/common/widget/FlipDrawable.java276
25 files changed, 1098 insertions, 12 deletions
diff --git a/res/drawable-hdpi/ic_action_call.png b/res/drawable-hdpi/ic_action_call.png
new file mode 100644
index 00000000..7493a607
--- /dev/null
+++ b/res/drawable-hdpi/ic_action_call.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_check_wht_24dp.png b/res/drawable-hdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..12ce8e0d
--- /dev/null
+++ b/res/drawable-hdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_call.png b/res/drawable-mdpi/ic_action_call.png
new file mode 100644
index 00000000..a5a7c371
--- /dev/null
+++ b/res/drawable-mdpi/ic_action_call.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_check_wht_24dp.png b/res/drawable-mdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..c7de7050
--- /dev/null
+++ b/res/drawable-mdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_call.png b/res/drawable-xhdpi/ic_action_call.png
new file mode 100644
index 00000000..e16ef951
--- /dev/null
+++ b/res/drawable-xhdpi/ic_action_call.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_wht_24dp.png b/res/drawable-xhdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..e34b73e5
--- /dev/null
+++ b/res/drawable-xhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_action_call.png b/res/drawable-xxhdpi/ic_action_call.png
new file mode 100644
index 00000000..c4cfdac8
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_action_call.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_check_wht_24dp.png b/res/drawable-xxhdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..4c6a653f
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable/call_background_activated.xml b/res/drawable/call_background_activated.xml
new file mode 100644
index 00000000..9fe8d3f6
--- /dev/null
+++ b/res/drawable/call_background_activated.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient android:startColor="#33b5e5" android:endColor="#33b5e5"
+ android:angle="270" />
+ <corners android:radius="2dp" />
+ <stroke android:color="#33b5e5" android:width="1dp" />
+ <padding android:top="5dp" android:bottom="5dp" android:left="5dp"
+ android:right="5dp" />
+</shape> \ No newline at end of file
diff --git a/res/drawable/call_background_holo.xml b/res/drawable/call_background_holo.xml
new file mode 100644
index 00000000..b947b20a
--- /dev/null
+++ b/res/drawable/call_background_holo.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient android:startColor="#00000000"
+ android:endColor="#00000000"
+ android:angle="270" />
+ <corners android:radius="2dp" />
+ <padding android:top="5dp" android:bottom="5dp" android:left="5dp"
+ android:right="5dp" />
+</shape> \ No newline at end of file
diff --git a/res/drawable/ic_action_call_background.xml b/res/drawable/ic_action_call_background.xml
new file mode 100644
index 00000000..71ac2a9b
--- /dev/null
+++ b/res/drawable/ic_action_call_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true"
+ android:state_pressed="true"
+ android:drawable="@drawable/call_background_activated" />
+ <item android:state_focused="false"
+ android:state_pressed="true"
+ android:drawable="@drawable/call_background_activated"/>
+ <item android:state_focused="false"
+ android:state_pressed="false"
+ android:drawable="@drawable/call_background_holo"/>
+</selector> \ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 64397caf..921469ff 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -17,6 +17,7 @@
<resources>
<declare-styleable name="Theme">
<attr name="android:textColorSecondary" />
+ <attr name="android:colorPrimary" />
</declare-styleable>
<declare-styleable name="ContactsDataKind">
@@ -62,6 +63,9 @@
<attr name="list_item_text_offset_top" format="dimension"/>
<attr name="list_item_data_width_weight" format="integer"/>
<attr name="list_item_label_width_weight" format="integer"/>
+ <attr name="list_item_quick_call_view_source" format="reference" />
+ <attr name="list_item_quick_call_view_background" format="reference" />
+ <attr name="list_item_quick_call_size" format="dimension" />
</declare-styleable>
<declare-styleable name="ContactBrowser">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6e49a5f0..9695179e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -836,4 +836,8 @@ a ren't members of any other group. [CHAR LIMIT=25] -->
<string name="sd_card">SD card</string>
<string name="phone_storage">Phone storage</string>
<string name="select_sim">Select SIM</string>
+
+ <string name="import_contacts_sim">Import contacts from SIM?</string>
+ <string name="import_contacts_sim_confirm">Import</string>
+ <string name="import_contacts_sim_cancel">Cancel</string>
</resources>
diff --git a/src/com/android/contacts/common/CallUtil.java b/src/com/android/contacts/common/CallUtil.java
index 20a65f72..afb04e99 100644
--- a/src/com/android/contacts/common/CallUtil.java
+++ b/src/com/android/contacts/common/CallUtil.java
@@ -24,6 +24,7 @@ import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
import com.android.contacts.common.util.PhoneNumberHelper;
import com.android.phone.common.PhoneConstants;
@@ -160,6 +161,46 @@ public class CallUtil {
/**
* Return Uri with an appropriate scheme, accepting both SIP and usual phone call
+ * Checks whether two phone numbers resolve to the same phone.
+ */
+ public static boolean phoneNumbersEqual(String number1, String number2) {
+ if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) {
+ return sipAddressesEqual(number1, number2);
+ } else {
+ return PhoneNumberUtils.compare(number1, number2);
+ }
+ }
+
+ private static boolean sipAddressesEqual(String number1, String number2) {
+ if (number1 == null || number2 == null) return number1 == number2;
+
+ int index1 = number1.indexOf('@');
+ final String userinfo1;
+ final String rest1;
+ if (index1 != -1) {
+ userinfo1 = number1.substring(0, index1);
+ rest1 = number1.substring(index1);
+ } else {
+ userinfo1 = number1;
+ rest1 = "";
+ }
+
+ int index2 = number2.indexOf('@');
+ final String userinfo2;
+ final String rest2;
+ if (index2 != -1) {
+ userinfo2 = number2.substring(0, index2);
+ rest2 = number2.substring(index2);
+ } else {
+ userinfo2 = number2;
+ rest2 = "";
+ }
+
+ return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+ }
+
+ /**
+ * Return Uri with an appropriate scheme, accepting Voicemail, SIP, and usual phone call
* numbers.
*/
public static Uri getCallUri(String number) {
diff --git a/src/com/android/contacts/common/interactions/ImportSIMContactsDialogFragment.java b/src/com/android/contacts/common/interactions/ImportSIMContactsDialogFragment.java
new file mode 100755
index 00000000..5cf1d08a
--- /dev/null
+++ b/src/com/android/contacts/common/interactions/ImportSIMContactsDialogFragment.java
@@ -0,0 +1,36 @@
+package com.android.contacts.common.interactions;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import com.android.contacts.common.R;
+
+/**
+ * An dialog invoked to import/export contacts.
+ */
+public class ImportSIMContactsDialogFragment extends DialogFragment {
+ public static final String TAG = "ImportSIMContactsDialogFragment";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Use the Builder class for convenient dialog construction
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.import_contacts_sim)
+ .setPositiveButton(R.string.import_contacts_sim_confirm,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ ImportExportDialogFragment.show(getFragmentManager(),
+ false, ImportSIMContactsDialogFragment.class);
+ }})
+ .setNegativeButton(R.string.import_contacts_sim_cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // Nothing to do
+ dismiss();
+ }});
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+}
diff --git a/src/com/android/contacts/common/list/ContactEntryListAdapter.java b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
index 2fc57352..2f3e25e8 100644..100755
--- a/src/com/android/contacts/common/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -63,6 +63,7 @@ public abstract class ContactEntryListAdapter extends IndexerListAdapter {
private boolean mCircularPhotos = true;
private boolean mQuickContactEnabled;
private boolean mAdjustSelectionBoundsEnabled;
+ private boolean mQuickCallButtonEnabled;
/**
* indicates if contact queries include profile
@@ -347,6 +348,10 @@ public abstract class ContactEntryListAdapter extends IndexerListAdapter {
return mQuickContactEnabled;
}
+ public boolean isQuickCallButtonEnabled() {
+ return mQuickCallButtonEnabled;
+ }
+
public void setQuickContactEnabled(boolean quickContactEnabled) {
mQuickContactEnabled = quickContactEnabled;
}
@@ -359,6 +364,10 @@ public abstract class ContactEntryListAdapter extends IndexerListAdapter {
mAdjustSelectionBoundsEnabled = enabled;
}
+ public void setQuickCallButtonEnabled(boolean quickCallButtonEnabled) {
+ mQuickCallButtonEnabled = quickCallButtonEnabled;
+ }
+
public boolean shouldIncludeProfile() {
return mIncludeProfile;
}
diff --git a/src/com/android/contacts/common/list/ContactEntryListFragment.java b/src/com/android/contacts/common/list/ContactEntryListFragment.java
index 4e2cc4e7..4af18305 100755
--- a/src/com/android/contacts/common/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -76,6 +76,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED =
"adjustSelectionBoundsEnabled";
+ private static final String KEY_QUICK_CALL_BUTTON_ENABLED = "quickCallButtonEnabled";
private static final String KEY_INCLUDE_PROFILE = "includeProfile";
private static final String KEY_SEARCH_MODE = "searchMode";
private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
@@ -101,6 +102,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
private boolean mPhotoLoaderEnabled;
private boolean mQuickContactEnabled = true;
private boolean mAdjustSelectionBoundsEnabled = true;
+ private boolean mQuickCallButtonEnabled = false;
private boolean mIncludeProfile;
private boolean mSearchMode;
private boolean mVisibleScrollbarEnabled;
@@ -242,6 +244,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
+ outState.putBoolean(KEY_QUICK_CALL_BUTTON_ENABLED, mQuickCallButtonEnabled);
outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
@@ -283,6 +286,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
+ mQuickCallButtonEnabled = savedState.getBoolean(KEY_QUICK_CALL_BUTTON_ENABLED);
mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
@@ -589,6 +593,10 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
mAdjustSelectionBoundsEnabled = flag;
}
+ public void setQuickCallButtonEnabled(boolean flag) {
+ this.mQuickCallButtonEnabled = flag;
+ }
+
public void setIncludeProfile(boolean flag) {
mIncludeProfile = flag;
if(mAdapter != null) {
@@ -811,6 +819,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
mAdapter.setQuickContactEnabled(mQuickContactEnabled);
mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
+ mAdapter.setQuickCallButtonEnabled(mQuickCallButtonEnabled);
mAdapter.setIncludeProfile(mIncludeProfile);
mAdapter.setQueryString(mQueryString);
mAdapter.setDirectorySearchMode(mDirectorySearchMode);
diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java
index c1c0f02e..a3eb8cbe 100644..100755
--- a/src/com/android/contacts/common/list/ContactListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -17,6 +17,7 @@ package com.android.contacts.common.list;
import android.accounts.Account;
import android.content.Context;
+import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
@@ -29,10 +30,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
+import android.widget.Toast;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
import com.android.contacts.common.R;
import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.model.Contact;
/**
* A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
@@ -53,6 +56,7 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
Contacts.IS_USER_PROFILE, // 7
RawContacts.ACCOUNT_TYPE, // 8
RawContacts.ACCOUNT_NAME, // 9
+ Contacts.HAS_PHONE_NUMBER, // 10
};
private static final String[] CONTACT_PROJECTION_ALTERNATIVE = new String[] {
@@ -66,6 +70,7 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
Contacts.IS_USER_PROFILE, // 7
RawContacts.ACCOUNT_TYPE, // 8
RawContacts.ACCOUNT_NAME, // 9
+ Contacts.HAS_PHONE_NUMBER, // 10
};
private static final String[] FILTER_PROJECTION_PRIMARY = new String[] {
@@ -79,7 +84,8 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
Contacts.IS_USER_PROFILE, // 7
RawContacts.ACCOUNT_TYPE, // 8
RawContacts.ACCOUNT_NAME, // 9
- SearchSnippets.SNIPPET, // 10
+ Contacts.HAS_PHONE_NUMBER, // 10
+ SearchSnippets.SNIPPET, // 11
};
private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
@@ -93,7 +99,8 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
Contacts.IS_USER_PROFILE, // 7
RawContacts.ACCOUNT_TYPE, // 8
RawContacts.ACCOUNT_NAME, // 9
- SearchSnippets.SNIPPET, // 10
+ Contacts.HAS_PHONE_NUMBER, // 10
+ SearchSnippets.SNIPPET, // 11
};
public static final int CONTACT_ID = 0;
@@ -106,7 +113,8 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
public static final int CONTACT_IS_USER_PROFILE = 7;
public static final int CONTACT_ACCOUNT_TYPE = 8;
public static final int CONTACT_ACCOUNT_NAME = 9;
- public static final int CONTACT_SNIPPET = 10;
+ public static final int CONTACT_HAS_NUMBER = 10;
+ public static final int CONTACT_SNIPPET = 11;
}
private CharSequence mUnknownNameText;
@@ -212,6 +220,7 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
view.setUnknownNameText(mUnknownNameText);
view.setQuickContactEnabled(isQuickContactEnabled());
view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+ view.setQuickCallButtonEnabled(isQuickCallButtonEnabled());
view.setActivatedStateSupported(isSelectionVisible());
if (mPhotoPosition != null) {
view.setPhotoPosition(mPhotoPosition);
@@ -219,6 +228,29 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
return view;
}
+ private View.OnClickListener mClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String lookup = ((ContactListItemView) view.getParent()).getQuickCallLookup();
+ Cursor cursor = mContext.getContentResolver().query(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ new String[] { ContactsContract.CommonDataKinds.Phone.NUMBER,
+ ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY},
+ ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY + "=?",
+ new String[] { lookup }, null);
+
+ if (cursor != null) {
+ if (cursor.moveToNext()) {
+ String phoneNumber = cursor.getString(0);
+ Uri uri = Uri.parse("tel: " + phoneNumber);
+ Intent intent = new Intent(Intent.ACTION_CALL, uri);
+ mContext.startActivity(intent);
+ }
+ cursor.close();
+ }
+ }
+ };
+
protected void bindSectionHeaderAndDivider(ContactListItemView view, int position,
Cursor cursor) {
view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
@@ -274,6 +306,12 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter {
bindViewId(view, cursor, ContactQuery.CONTACT_ID);
}
+ protected void bindQuickCallView(final ContactListItemView view, Cursor cursor) {
+ view.showQuickCallView(cursor, ContactQuery.CONTACT_HAS_NUMBER,
+ ContactQuery.CONTACT_LOOKUP_KEY);
+ view.setOnQuickCallClickListener(mClickListener);
+ }
+
protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
view.showPresenceAndStatusMessage(cursor, ContactQuery.CONTACT_PRESENCE_STATUS,
ContactQuery.CONTACT_CONTACT_STATUS);
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index fb007c4c..91f063ff 100644..100755
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -26,6 +26,7 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
@@ -51,6 +52,8 @@ import com.android.contacts.common.R;
import com.android.contacts.common.format.TextHighlighter;
import com.android.contacts.common.util.SearchUtil;
import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.common.widget.CheckableImageView;
+import com.android.contacts.common.widget.CheckableQuickContactBadge;
import com.google.common.collect.Lists;
@@ -76,7 +79,7 @@ import java.util.regex.Pattern;
*/
public class ContactListItemView extends ViewGroup
- implements SelectionBoundsAdjuster {
+ implements SelectionBoundsAdjuster, View.OnClickListener {
// Style values for layout and appearance
// The initialized values are defaults if none is provided through xml.
@@ -150,8 +153,10 @@ public class ContactListItemView extends ViewGroup
// The views inside the contact view
private boolean mQuickContactEnabled = true;
- private QuickContactBadge mQuickContact;
- private ImageView mPhotoView;
+ private boolean mQuickCallButtonEnabled = false;
+ private CheckableQuickContactBadge mQuickContact;
+ private ImageView mQuickCallView;
+ private CheckableImageView mPhotoView;
private TextView mNameTextView;
private TextView mPhoneticNameTextView;
private TextView mLabelView;
@@ -159,12 +164,15 @@ public class ContactListItemView extends ViewGroup
private TextView mSnippetView;
private TextView mStatusView;
private ImageView mPresenceIcon;
+ private String mQuickCallKey;
private ColorStateList mSecondaryTextColor;
-
+ private int mQuickCallViewImageId = 0;
+ private int mQuickCallViewBgId = 0;
private int mDefaultPhotoViewSize = 0;
+ private int mDefaultQuickCallViewSize = 0;
/**
* Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
* to align other data in this View.
@@ -176,6 +184,15 @@ public class ContactListItemView extends ViewGroup
private int mPhotoViewHeight;
/**
+ * Only effective when {@link #mQuickCallView} is null
+ */
+ private int mQuickCallViewWidth;
+ /**
+ * Only effective when {@link #mQuickCallView} is null
+ */
+ private int mQuickCallViewHeight;
+
+ /**
* Only effective when {@link #mPhotoView} is null.
* When true all the Views on the right side of the photo should have horizontal padding on
* those left assuming there is a photo.
@@ -218,6 +235,8 @@ public class ContactListItemView extends ViewGroup
private Rect mBoundsWithoutHeader = new Rect();
+ private OnClickListener mListener;
+
/** A helper used to highlight a prefix in a text field. */
private final TextHighlighter mTextHighlighter;
private CharSequence mUnknownNameText;
@@ -253,6 +272,9 @@ public class ContactListItemView extends ViewGroup
R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
mDefaultPhotoViewSize = a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+ mDefaultQuickCallViewSize = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_quick_call_size,
+ mDefaultQuickCallViewSize);
mTextIndent = a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
mTextOffsetTop = a.getDimensionPixelOffset(
@@ -267,6 +289,12 @@ public class ContactListItemView extends ViewGroup
mNameTextViewTextSize = (int) a.getDimension(
R.styleable.ContactListItemView_list_item_name_text_size,
(int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
+ mQuickCallViewImageId = a.getResourceId(
+ R.styleable.ContactListItemView_list_item_quick_call_view_source,
+ R.drawable.ic_action_call);
+ mQuickCallViewBgId = a.getResourceId(
+ R.styleable.ContactListItemView_list_item_quick_call_view_background,
+ R.drawable.ic_action_call_background);
setPaddingRelative(
a.getDimensionPixelOffset(
@@ -307,6 +335,22 @@ public class ContactListItemView extends ViewGroup
mQuickContactEnabled = flag;
}
+ public void setQuickCallButtonEnabled(boolean flag) {
+ mQuickCallButtonEnabled = flag;
+ }
+
+ public void setQuickCallLookup(String lookupKey) {
+ mQuickCallKey = lookupKey;
+ }
+
+ public void setQuickCallButtonImageResource(int resourceId) {
+ mQuickCallViewImageId = resourceId;
+ }
+
+ public void setQuickCallButtonBackgroundResource(int resourceId) {
+ mQuickCallViewBgId = resourceId;
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We will match parent's width and wrap content vertically, but make sure
@@ -323,15 +367,16 @@ public class ContactListItemView extends ViewGroup
mStatusTextViewHeight = 0;
ensurePhotoViewSize();
+ ensureQuickCallViewSize();
// Width each TextView is able to use.
int effectiveWidth;
// All the other Views will honor the photo, so available width for them may be shrunk.
if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
- - (mPhotoViewWidth + mGapBetweenImageAndText);
+ - (mPhotoViewWidth + mGapBetweenImageAndText + mQuickCallViewWidth);
} else {
- effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+ effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight() - mQuickCallViewWidth;
}
if (mIsSectionHeaderEnabled) {
@@ -449,6 +494,12 @@ public class ContactListItemView extends ViewGroup
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
}
+ if (isVisible(mQuickCallView)) {
+ mQuickCallView.measure(
+ MeasureSpec.makeMeasureSpec(mQuickCallViewWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mQuickCallViewHeight, MeasureSpec.EXACTLY));
+ }
+
setMeasuredDimension(specWidth, height);
}
@@ -550,6 +601,9 @@ public class ContactListItemView extends ViewGroup
textTopBound += mNameTextViewHeight;
}
+ // Center the photo vertically
+ final int quickCallTop = topBound + (bottomBound - topBound - mQuickCallViewHeight) / 2;
+
// Presence and status
if (isLayoutRtl) {
int statusRightBound = rightBound;
@@ -569,6 +623,13 @@ public class ContactListItemView extends ViewGroup
statusRightBound,
textTopBound + mStatusTextViewHeight);
}
+
+ if (isVisible(mQuickCallView)) {
+ mQuickCallView.layout(-width + (mQuickCallViewWidth + mPhotoViewWidth),
+ quickCallTop,
+ rightBound,
+ quickCallTop + mQuickCallViewHeight);
+ }
} else {
int statusLeftBound = leftBound;
if (isVisible(mPresenceIcon)) {
@@ -587,6 +648,13 @@ public class ContactListItemView extends ViewGroup
rightBound,
textTopBound + mStatusTextViewHeight);
}
+
+ if (isVisible(mQuickCallView)) {
+ mQuickCallView.layout(rightBound - mQuickCallView.getMeasuredWidth(),
+ quickCallTop,
+ rightBound,
+ quickCallTop + mQuickCallViewHeight);
+ }
}
if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
@@ -674,10 +742,33 @@ public class ContactListItemView extends ViewGroup
}
}
+ /**
+ * Extracts width and height from the style
+ */
+ private void ensureQuickCallViewSize() {
+ mQuickCallViewWidth = mQuickCallViewHeight = getDefaultQuickCallViewSize();
+ if (!mQuickCallButtonEnabled || mQuickCallView == null) {
+ mQuickCallViewWidth = 0;
+ mQuickCallViewHeight = 0;
+ }
+ }
+
+ protected void setDefaultPhotoViewSize(int pixels) {
+ mDefaultPhotoViewSize = pixels;
+ }
+
protected int getDefaultPhotoViewSize() {
return mDefaultPhotoViewSize;
}
+ protected void setDefaultQuickCallViewSize(int pixels) {
+ mDefaultQuickCallViewSize = pixels;
+ }
+
+ protected int getDefaultQuickCallViewSize() {
+ return mDefaultQuickCallViewSize;
+ }
+
/**
* Gets a LayoutParam that corresponds to the default photo size.
*
@@ -745,6 +836,14 @@ public class ContactListItemView extends ViewGroup
}
/**
+ * Get the quick call lookup to use with Intent.ACTION_CALL
+ * @return
+ */
+ public String getQuickCallLookup() {
+ return mQuickCallKey;
+ }
+
+ /**
* Returns the quick contact badge, creating it if necessary.
*/
public QuickContactBadge getQuickContact() {
@@ -752,7 +851,7 @@ public class ContactListItemView extends ViewGroup
throw new IllegalStateException("QuickContact is disabled for this view");
}
if (mQuickContact == null) {
- mQuickContact = new QuickContactBadge(getContext());
+ mQuickContact = new CheckableQuickContactBadge(getContext());
mQuickContact.setOverlay(null);
mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
if (mNameTextView != null) {
@@ -766,12 +865,21 @@ public class ContactListItemView extends ViewGroup
return mQuickContact;
}
+ public void setChecked(boolean checked, boolean animate) {
+ if (mQuickContact != null) {
+ mQuickContact.setChecked(checked, animate);
+ }
+ if (mPhotoView != null) {
+ mPhotoView.setChecked(checked, animate);
+ }
+ }
+
/**
* Returns the photo view, creating it if necessary.
*/
public ImageView getPhotoView() {
if (mPhotoView == null) {
- mPhotoView = new ImageView(getContext());
+ mPhotoView = new CheckableImageView(getContext());
mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
// Quick contact style used above will set a background - remove it
mPhotoView.setBackground(null);
@@ -782,6 +890,31 @@ public class ContactListItemView extends ViewGroup
}
/**
+ * Returns the quick call view, creating it if necessary.
+ */
+ public ImageView getQuickCallView() {
+ if (mQuickCallView == null) {
+ mQuickCallView = new ImageView(mContext);
+ mQuickCallView.setLayoutParams(getDefaultPhotoLayoutParams());
+ mQuickCallView.setImageResource(mQuickCallViewImageId);
+ mQuickCallView.setBackgroundResource(mQuickCallViewBgId);
+ mQuickCallView.setClickable(true);
+ addView(mQuickCallView);
+ }
+ return mQuickCallView;
+ }
+
+ /**
+ * Removes the quick call view.
+ */
+ public void removeQuickCallView() {
+ if (mQuickCallView != null) {
+ removeView(mQuickCallView);
+ mQuickCallView = null;
+ }
+ }
+
+ /**
* Removes the photo view.
*/
public void removePhotoView() {
@@ -1124,6 +1257,18 @@ public class ContactListItemView extends ViewGroup
}
}
+ public void showQuickCallView(Cursor cursor, int numberColumIndex, int lookUpKey) {
+ int hasNumber = cursor.getInt(numberColumIndex);
+ if (!(hasNumber == 0)) {
+ getQuickCallView().setVisibility(View.VISIBLE);
+ setQuickCallLookup(cursor.getString(lookUpKey));
+ } else {
+ if (mQuickCallView != null) {
+ mQuickCallView.setVisibility(View.GONE);
+ }
+ }
+ }
+
public void setDisplayName(CharSequence name, boolean highlight) {
if (!TextUtils.isEmpty(name) && highlight) {
clearHighlightSequences();
@@ -1444,7 +1589,27 @@ public class ContactListItemView extends ViewGroup
if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
return super.onTouchEvent(event);
} else {
- return true;
+ return false;
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mListener != null && mQuickCallButtonEnabled) {
+ if (view == mQuickCallView) {
+ mListener.onClick(view);
+ }
+ }
+ }
+
+ /**
+ * Set the a click listener for the quick call view
+ * @param listener
+ */
+ public void setOnQuickCallClickListener(OnClickListener listener) {
+ this.mListener = listener;
+ if (mListener != null && mQuickCallView != null) {
+ mQuickCallView.setOnClickListener(mListener);
}
}
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
index ea177eaf..c44e1e28 100755
--- a/src/com/android/contacts/common/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -257,6 +257,9 @@ public class DefaultContactListAdapter extends ContactListAdapter {
}
bindNameAndViewId(view, cursor);
+ if (isQuickCallButtonEnabled()) {
+ bindQuickCallView(view, cursor);
+ }
bindPresenceAndStatusMessage(view, cursor);
if (isSearchMode()) {
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
index 311d0075..5c07f83f 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -27,6 +27,7 @@ import android.provider.ContactsContract;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
import com.android.contacts.common.R;
/**
@@ -54,6 +55,11 @@ public final class ContactsPreferences implements OnSharedPreferenceChangeListen
public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER";
/**
+ * The values of SIMs serial numbers that have been imported
+ */
+ public static final String IMPORTED_SIMS_SNS = "android.contacts.IMPORTED_SIMS";
+
+ /**
* The value for the SORT_ORDER key corresponding to sort by family name first.
*/
public static final int SORT_ORDER_ALTERNATIVE = 2;
@@ -105,6 +111,28 @@ public final class ContactsPreferences implements OnSharedPreferenceChangeListen
editor.commit();
}
+ public String[] getImportedSims() {
+ String imported = mPreferences.getString(IMPORTED_SIMS_SNS, "");
+ if (!TextUtils.isEmpty(imported)) {
+ return imported.split("\\|");
+ } else {
+ return new String[0];
+ }
+ }
+
+ public void addImportedSims(String simSN) {
+ String imported = mPreferences.getString(IMPORTED_SIMS_SNS, "");
+ if (!TextUtils.isEmpty(imported)) {
+ imported += "|" + simSN;
+ } else {
+ imported = simSN;
+ }
+
+ final Editor editor = mPreferences.edit();
+ editor.putString(IMPORTED_SIMS_SNS, imported);
+ editor.commit();
+ }
+
public boolean isDisplayOrderUserChangeable() {
return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
}
diff --git a/src/com/android/contacts/common/widget/CheckableFlipDrawable.java b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
new file mode 100644
index 00000000..cca82886
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
@@ -0,0 +1,220 @@
+package com.android.contacts.common.widget;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableFlipDrawable extends FlipDrawable implements
+ ValueAnimator.AnimatorUpdateListener {
+
+ private final CheckmarkDrawable mCheckmarkDrawable;
+
+ private final ValueAnimator mCheckmarkScaleAnimator;
+ private final ValueAnimator mCheckmarkAlphaAnimator;
+
+ private static final int POST_FLIP_DURATION_MS = 150;
+
+ private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+ private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+ /** Must be <= 1f since the animation value is used as a percentage. */
+ private static final float END_VALUE = 1f;
+
+ public CheckableFlipDrawable(Drawable front, final Resources res,
+ final int checkBackgroundColor, final int flipDurationMs) {
+ super(front, new CheckmarkDrawable(res, checkBackgroundColor),
+ flipDurationMs, 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+ mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+ // We will create checkmark animations that are synchronized with the
+ // flipping animation. The entire delay + duration of the checkmark animation
+ // needs to equal the entire duration of the flip animation (where delay is 0).
+
+ // The checkmark animation is in effect only when the back drawable is being shown.
+ // For the flip animation duration <pre>[_][]|[][_]<post>
+ // The checkmark animation will be |--delay--|-duration-|
+
+ // Need delay to skip the first half of the flip duration.
+ final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+ // Actual duration is the second half of the flip duration.
+ final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+ mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+ mCheckmarkScaleAnimator.addUpdateListener(this);
+
+ mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+ mCheckmarkAlphaAnimator.addUpdateListener(this);
+ }
+
+ public void setFront(Drawable front) {
+ mFront.setCallback(null);
+
+ mFront = front;
+
+ mFront.setCallback(this);
+ mFront.setBounds(getBounds());
+ mFront.setAlpha(getAlpha());
+ mFront.setColorFilter(getColorFilter());
+ mFront.setLevel(getLevel());
+
+ reset();
+ invalidateSelf();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckmarkDrawable.setBackgroundColor(color);
+ invalidateSelf();
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ if (mCheckmarkScaleAnimator == null) {
+ // Call from super's constructor. Not yet initialized.
+ return;
+ }
+ mCheckmarkScaleAnimator.cancel();
+ mCheckmarkAlphaAnimator.cancel();
+ boolean side = getSideFlippingTowards();
+ mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+ mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+ }
+
+ @Override
+ public void flip() {
+ super.flip();
+ // Keep the checkmark animators in sync with the flip animator.
+ if (mCheckmarkScaleAnimator.isStarted()) {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ } else {
+ if (!getSideFlippingTowards() /* front to back */) {
+ mCheckmarkScaleAnimator.start();
+ mCheckmarkAlphaAnimator.start();
+ } else /* back to front */ {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ //noinspection ConstantConditions
+ final float value = (Float) animation.getAnimatedValue();
+
+ if (animation == mCheckmarkScaleAnimator) {
+ mCheckmarkDrawable.setScaleAnimatorValue(value);
+ } else if (animation == mCheckmarkAlphaAnimator) {
+ mCheckmarkDrawable.setAlphaAnimatorValue(value);
+ }
+ }
+
+ private static class CheckmarkDrawable extends Drawable {
+ private static Bitmap sCheckMark;
+
+ private final Paint mPaint;
+
+ private float mScaleFraction;
+ private float mAlphaFraction;
+
+ private static final Matrix sMatrix = new Matrix();
+
+ public CheckmarkDrawable(final Resources res, int backgroundColor) {
+ if (sCheckMark == null) {
+ sCheckMark = BitmapFactory.decodeResource(res, R.drawable.ic_check_wht_24dp);
+ }
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ mPaint.setColor(backgroundColor);
+ }
+
+ public void setBackgroundColor(int color) {
+ mPaint.setColor(color);
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, mPaint);
+
+ // Scale the checkmark.
+ sMatrix.reset();
+ sMatrix.setScale(mScaleFraction, mScaleFraction, sCheckMark.getWidth() / 2,
+ sCheckMark.getHeight() / 2);
+ sMatrix.postTranslate(bounds.centerX() - sCheckMark.getWidth() / 2,
+ bounds.centerY() - sCheckMark.getHeight() / 2);
+
+ // Fade the checkmark.
+ final int oldAlpha = mPaint.getAlpha();
+ // Interpolate the alpha.
+ mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+ canvas.drawBitmap(sCheckMark, sMatrix, mPaint);
+ // Restore the alpha.
+ mPaint.setAlpha(oldAlpha);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ // Always a gray background.
+ return PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setScaleAnimatorValue(final float value) {
+ final float old = mScaleFraction;
+ mScaleFraction = value;
+ if (old != mScaleFraction) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setAlphaAnimatorValue(final float value) {
+ final float old = mAlphaFraction;
+ mAlphaFraction = value;
+ if (old != mAlphaFraction) {
+ invalidateSelf();
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableImageView.java b/src/com/android/contacts/common/widget/CheckableImageView.java
new file mode 100644
index 00000000..914d4eaf
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableImageView.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+import com.android.contacts.common.R;
+
+public class CheckableImageView extends ImageView implements Checkable {
+ private boolean mChecked = false;
+ private int mCheckMarkBackgroundColor;
+ private CheckableFlipDrawable mDrawable;
+
+ public CheckableImageView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+ setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+ context.getResources().getColor(R.color.people_app_theme_color)));
+ a.recycle();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckMarkBackgroundColor = color;
+ if (mDrawable != null) {
+ mDrawable.setCheckMarkBackgroundColor(color);
+ }
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ setChecked(checked, true);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (mChecked == checked) {
+ return;
+ }
+
+ mChecked = checked;
+
+ Drawable d = getDrawable();
+ if (d instanceof CheckableFlipDrawable) {
+ CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+ cfd.flipTo(!mChecked);
+ if (!animate) {
+ cfd.reset();
+ }
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable d) {
+ if (d != null) {
+ if (mDrawable == null) {
+ mDrawable = new CheckableFlipDrawable(d, getResources(),
+ mCheckMarkBackgroundColor, 150);
+ } else {
+ int oldWidth = mDrawable.getIntrinsicWidth();
+ int oldHeight = mDrawable.getIntrinsicHeight();
+ mDrawable.setFront(d);
+ if (oldWidth != mDrawable.getIntrinsicWidth()
+ || oldHeight != mDrawable.getIntrinsicHeight()) {
+ // enforce drawable size update + layout
+ super.setImageDrawable(null);
+ }
+ }
+ d = mDrawable;
+ }
+ super.setImageDrawable(d);
+ }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
new file mode 100644
index 00000000..85160569
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableQuickContactBadge extends QuickContactBadge implements Checkable {
+ private boolean mChecked = false;
+ private int mCheckMarkBackgroundColor;
+ private CheckableFlipDrawable mDrawable;
+
+ public CheckableQuickContactBadge(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+ setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+ context.getResources().getColor(R.color.people_app_theme_color)));
+ a.recycle();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckMarkBackgroundColor = color;
+ if (mDrawable != null) {
+ mDrawable.setCheckMarkBackgroundColor(color);
+ }
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ setChecked(checked, true);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (mChecked == checked) {
+ return;
+ }
+
+ mChecked = checked;
+
+ Drawable d = getDrawable();
+ if (d instanceof CheckableFlipDrawable) {
+ CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+ cfd.flipTo(!mChecked);
+ if (!animate) {
+ cfd.reset();
+ }
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable d) {
+ if (d != null) {
+ if (mDrawable == null) {
+ mDrawable = new CheckableFlipDrawable(d, getResources(),
+ mCheckMarkBackgroundColor, 150);
+ } else {
+ int oldWidth = mDrawable.getIntrinsicWidth();
+ int oldHeight = mDrawable.getIntrinsicHeight();
+ mDrawable.setFront(d);
+ if (oldWidth != mDrawable.getIntrinsicWidth()
+ || oldHeight != mDrawable.getIntrinsicHeight()) {
+ // enforce drawable size update + layout
+ super.setImageDrawable(null);
+ }
+ }
+ d = mDrawable;
+ }
+ super.setImageDrawable(d);
+ }
+}
diff --git a/src/com/android/contacts/common/widget/FlipDrawable.java b/src/com/android/contacts/common/widget/FlipDrawable.java
new file mode 100644
index 00000000..ff14b508
--- /dev/null
+++ b/src/com/android/contacts/common/widget/FlipDrawable.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2013 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.common.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ * | | |
+ * V V V
+ * &lt;pre>&lt; flip &gt;&lt;post&gt;
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+ /**
+ * The inner drawables.
+ */
+ protected Drawable mFront;
+ protected final Drawable mBack;
+
+ protected final int mFlipDurationMs;
+ protected final int mPreFlipDurationMs;
+ protected final int mPostFlipDurationMs;
+ private final ValueAnimator mFlipAnimator;
+
+ private static final float END_VALUE = 2f;
+
+ /**
+ * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
+ * mFront is fully shown, while END_VALUE means mBack is fully shown.
+ */
+ private float mFlipFraction = 0f;
+
+ /**
+ * True if flipping towards front, false if flipping towards back.
+ */
+ private boolean mFlipToSide = true;
+
+ /**
+ * Create a new FlipDrawable. The front is fully shown by default.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ * | | |
+ * V V V
+ * &lt;pre>&lt; flip &gt;&lt;post&gt;
+ * </pre>
+ *
+ * @param front The front drawable.
+ * @param back The back drawable.
+ * @param flipDurationMs The duration of the actual flip. This duration includes both
+ * animating away one side and showing the other.
+ * @param preFlipDurationMs The duration before the actual flip begins. Subclasses can use this
+ * to add flourish.
+ * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
+ * to add flourish.
+ */
+ public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
+ final int preFlipDurationMs, final int postFlipDurationMs) {
+ if (front == null || back == null) {
+ throw new IllegalArgumentException("Front and back drawables must not be null.");
+ }
+ mFront = front;
+ mBack = back;
+
+ mFront.setCallback(this);
+ mBack.setCallback(this);
+
+ mFlipDurationMs = flipDurationMs;
+ mPreFlipDurationMs = preFlipDurationMs;
+ mPostFlipDurationMs = postFlipDurationMs;
+
+ mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
+ .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
+ mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ final float old = mFlipFraction;
+ //noinspection ConstantConditions
+ mFlipFraction = (Float) animation.getAnimatedValue();
+ if (old != mFlipFraction) {
+ invalidateSelf();
+ }
+ }
+ });
+
+ reset();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mFront.getIntrinsicWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mFront.getIntrinsicHeight();
+ }
+
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+ if (bounds.isEmpty()) {
+ mFront.setBounds(0, 0, 0, 0);
+ mBack.setBounds(0, 0, 0, 0);
+ } else {
+ mFront.setBounds(bounds);
+ mBack.setBounds(bounds);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
+
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+
+ final float scaleX;
+ if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
+ // During pre-flip.
+ scaleX = 1;
+ } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
+ // During post-flip.
+ scaleX = 1;
+ } else {
+ // During flip.
+ final float flipFraction = mFlipFraction / 2;
+ final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ final float distFraction = Math.abs(flipFraction - flipMiddle);
+ final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
+ scaleX = distFraction * multiplier;
+ }
+
+ canvas.save();
+ // The flip is a simple 1 dimensional scale.
+ canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
+ inner.draw(canvas);
+ canvas.restore();
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mFront.setAlpha(alpha);
+ mBack.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mFront.setColorFilter(cf);
+ mBack.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
+ }
+
+ @Override
+ protected boolean onLevelChange(final int level) {
+ return mFront.setLevel(level) || mBack.setLevel(level);
+ }
+
+ @Override
+ public void invalidateDrawable(final Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(final Drawable who, final Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ /**
+ * Stop animating the flip and reset to one side.
+ * @param side Pass true if reset to front, false if reset to back.
+ */
+ public void reset() {
+ final float old = mFlipFraction;
+ mFlipAnimator.cancel();
+ mFlipFraction = mFlipToSide ? 0f : 2f;
+ if (mFlipFraction != old) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Returns true if the front is shown. Returns false if the back is shown.
+ */
+ public boolean getSideShown() {
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+ final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ return mFlipFraction / 2 < middleFraction;
+ }
+
+ /**
+ * Returns true if the front is being flipped towards. Returns false if the back is being
+ * flipped towards.
+ */
+ public boolean getSideFlippingTowards() {
+ return mFlipToSide;
+ }
+
+ /**
+ * Starts an animated flip to the other side. If a flip animation is currently started,
+ * it will be reversed.
+ */
+ public void flip() {
+ mFlipToSide = !mFlipToSide;
+ if (mFlipAnimator.isStarted()) {
+ mFlipAnimator.reverse();
+ } else {
+ if (!mFlipToSide /* front to back */) {
+ mFlipAnimator.start();
+ } else /* back to front */ {
+ mFlipAnimator.reverse();
+ }
+ }
+ }
+
+ /**
+ * Start an animated flip to a side. This works regardless of whether a flip animation is
+ * currently started.
+ * @param side Pass true if flip to front, false if flip to back.
+ */
+ public void flipTo(final boolean side) {
+ if (mFlipToSide != side) {
+ flip();
+ }
+ }
+
+ /**
+ * Returns whether flipping is in progress.
+ */
+ public boolean isFlipping() {
+ return mFlipAnimator.isStarted();
+ }
+}