diff options
author | Jessica Wagantall <jessicag@codeaurora.org> | 2014-12-19 19:23:08 -0800 |
---|---|---|
committer | Jessica Wagantall <jessicag@codeaurora.org> | 2014-12-19 19:24:38 -0800 |
commit | 3a3d43023d6c85884b980739e6e9737dc5751530 (patch) | |
tree | 29d8e041210c19577e8eb600d7733384aee28d15 | |
parent | 2b13656c19836dd1843ba3d13544264267952e98 (diff) | |
parent | b5ad7c99c3e990868042b13cd31c532b6102e0cd (diff) | |
download | android_packages_apps_ContactsCommon-3a3d43023d6c85884b980739e6e9737dc5751530.tar.gz android_packages_apps_ContactsCommon-3a3d43023d6c85884b980739e6e9737dc5751530.tar.bz2 android_packages_apps_ContactsCommon-3a3d43023d6c85884b980739e6e9737dc5751530.zip |
Merge commit 'b5ad7c99c3e990868042b13cd31c532b6102e0cd' into HEAD
Change-Id: I46f460076962a51da08f61e534caad0ea07d191d
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 Binary files differnew file mode 100644 index 00000000..7493a607 --- /dev/null +++ b/res/drawable-hdpi/ic_action_call.png diff --git a/res/drawable-hdpi/ic_check_wht_24dp.png b/res/drawable-hdpi/ic_check_wht_24dp.png Binary files differnew file mode 100644 index 00000000..12ce8e0d --- /dev/null +++ b/res/drawable-hdpi/ic_check_wht_24dp.png diff --git a/res/drawable-mdpi/ic_action_call.png b/res/drawable-mdpi/ic_action_call.png Binary files differnew file mode 100644 index 00000000..a5a7c371 --- /dev/null +++ b/res/drawable-mdpi/ic_action_call.png diff --git a/res/drawable-mdpi/ic_check_wht_24dp.png b/res/drawable-mdpi/ic_check_wht_24dp.png Binary files differnew file mode 100644 index 00000000..c7de7050 --- /dev/null +++ b/res/drawable-mdpi/ic_check_wht_24dp.png diff --git a/res/drawable-xhdpi/ic_action_call.png b/res/drawable-xhdpi/ic_action_call.png Binary files differnew file mode 100644 index 00000000..e16ef951 --- /dev/null +++ b/res/drawable-xhdpi/ic_action_call.png diff --git a/res/drawable-xhdpi/ic_check_wht_24dp.png b/res/drawable-xhdpi/ic_check_wht_24dp.png Binary files differnew file mode 100644 index 00000000..e34b73e5 --- /dev/null +++ b/res/drawable-xhdpi/ic_check_wht_24dp.png diff --git a/res/drawable-xxhdpi/ic_action_call.png b/res/drawable-xxhdpi/ic_action_call.png Binary files differnew file mode 100644 index 00000000..c4cfdac8 --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_call.png diff --git a/res/drawable-xxhdpi/ic_check_wht_24dp.png b/res/drawable-xxhdpi/ic_check_wht_24dp.png Binary files differnew file mode 100644 index 00000000..4c6a653f --- /dev/null +++ b/res/drawable-xxhdpi/ic_check_wht_24dp.png 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> + * <pre>[_][]|[][_]<post> + * | | | + * V V V + * <pre>< flip ><post> + * </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> + * <pre>[_][]|[][_]<post> + * | | | + * V V V + * <pre>< flip ><post> + * </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(); + } +} |