diff options
author | Jeff Sharkey <jsharkey@android.com> | 2011-04-07 00:48:02 -0700 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2011-04-08 13:44:27 -0700 |
commit | cde7389187ebc6816bd73d4704f1ca1b4ee39ac3 (patch) | |
tree | c4455f596a83aa7277b06752c1b7e1099bec2db1 | |
parent | f6c62c03f64d14f2f84d847d97ac0657f7adf2f6 (diff) | |
download | packages_apps_Contacts-cde7389187ebc6816bd73d4704f1ca1b4ee39ac3.tar.gz packages_apps_Contacts-cde7389187ebc6816bd73d4704f1ca1b4ee39ac3.tar.bz2 packages_apps_Contacts-cde7389187ebc6816bd73d4704f1ca1b4ee39ac3.zip |
Move QuickContact to Activity instead of Window.
QuickContact has traditionally used a Window to show a bubble with
callout centered around a target area. This change moves away from
private Window APIs, and instead creates FloatingChildLayout to layout
the bubble. Using onLayout() is more flexible than a PopupWindow or
Dialog, since it gives us access to getWindowVisibleDisplayFrame() to
correctly handle system windows. (Similar to FLAG_LAYOUT_INSET_DECOR.)
Changes background Drawable to use state_first and state_last to select
above/below callout arrow. Also moves to using setLevel() to set arrow
horizontal location.
Removes recycling chiclet code, and brings in Guava library.
Bug: 3362647
Change-Id: Iae953bae71db76e91e05996fe4c0dcea38bb446f
-rw-r--r-- | Android.mk | 6 | ||||
-rw-r--r-- | AndroidManifest.xml | 9 | ||||
-rw-r--r-- | res/drawable/quickactions_arrow_left_holo_light.xml | 21 | ||||
-rw-r--r-- | res/drawable/quickactions_arrow_middle_holo_light.xml | 21 | ||||
-rw-r--r-- | res/drawable/quickactions_arrow_right_holo_light.xml | 21 | ||||
-rw-r--r-- | res/layout/quickcontact.xml | 12 | ||||
-rw-r--r-- | res/layout/quickcontact_activity.xml | 28 | ||||
-rwxr-xr-x | res/layout/quickcontact_default_item.xml | 4 | ||||
-rwxr-xr-x | res/layout/quickcontact_resolve_item.xml | 4 | ||||
-rw-r--r-- | res/values/styles.xml | 37 | ||||
-rw-r--r-- | src/com/android/contacts/quickcontact/FloatingChildLayout.java | 231 | ||||
-rw-r--r-- | src/com/android/contacts/quickcontact/QuickContactActivity.java | 121 | ||||
-rw-r--r-- | src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java | 86 | ||||
-rw-r--r-- | src/com/android/contacts/quickcontact/QuickContactRootLayout.java | 54 | ||||
-rw-r--r-- | src/com/android/contacts/quickcontact/QuickContactWindow.java | 828 | ||||
-rw-r--r-- | src/com/android/contacts/util/NotifyingAsyncQueryHandler.java | 2 |
16 files changed, 589 insertions, 896 deletions
diff --git a/Android.mk b/Android.mk index ff1b7e25b..1c9dcc475 100644 --- a/Android.mk +++ b/Android.mk @@ -5,7 +5,11 @@ LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) -LOCAL_STATIC_JAVA_LIBRARIES := com.android.phone.common com.android.vcard android-common +LOCAL_STATIC_JAVA_LIBRARIES := \ + com.android.phone.common \ + com.android.vcard \ + android-common \ + guava LOCAL_PACKAGE_NAME := Contacts LOCAL_CERTIFICATE := shared diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 20641f226..458553e01 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -326,7 +326,7 @@ <activity android:name=".activities.ShowOrCreateActivity" - android:theme="@style/FullyTranslucent"> + android:theme="@android:style/Theme.Translucent.NoTitleBar"> <intent-filter> <action android:name="com.android.contacts.action.SHOW_OR_CREATE_CONTACT" /> @@ -339,13 +339,12 @@ <!-- Used to show QuickContact window over a translucent activity, which is a temporary hack until we add better framework support. --> <activity - android:name=".quickcontact.QuickContactActivity" - android:theme="@style/FullyTranslucent.QuickContact" + android:name=".quickcontact.QuickContactWindow" + android:theme="@style/Theme.QuickContact" android:launchMode="singleTop" android:excludeFromRecents="true" android:taskAffinity="android.task.quickcontact" - android:windowSoftInputMode="stateUnchanged" - > + android:windowSoftInputMode="stateUnchanged"> <intent-filter> <action android:name="com.android.contacts.action.QUICK_CONTACT" /> diff --git a/res/drawable/quickactions_arrow_left_holo_light.xml b/res/drawable/quickactions_arrow_left_holo_light.xml new file mode 100644 index 000000000..c1e18bd1d --- /dev/null +++ b/res/drawable/quickactions_arrow_left_holo_light.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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_first="true" android:drawable="@drawable/quickactions_arrowdown_left_holo_light" /> + <item android:state_last="true" android:drawable="@drawable/quickactions_arrowup_left_holo_light" /> + <!-- TODO: provide callout-less state --> + <item android:drawable="@drawable/quickactions_arrowup_left_holo_light" /> +</selector> diff --git a/res/drawable/quickactions_arrow_middle_holo_light.xml b/res/drawable/quickactions_arrow_middle_holo_light.xml new file mode 100644 index 000000000..f88b513f3 --- /dev/null +++ b/res/drawable/quickactions_arrow_middle_holo_light.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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_first="true" android:drawable="@drawable/quickactions_arrowdown_middle_holo_light" /> + <item android:state_last="true" android:drawable="@drawable/quickactions_arrowup_middle_holo_light" /> + <!-- TODO: provide callout-less state --> + <item android:drawable="@drawable/quickactions_arrowup_middle_holo_light" /> +</selector> diff --git a/res/drawable/quickactions_arrow_right_holo_light.xml b/res/drawable/quickactions_arrow_right_holo_light.xml new file mode 100644 index 000000000..3e309fe28 --- /dev/null +++ b/res/drawable/quickactions_arrow_right_holo_light.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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_first="true" android:drawable="@drawable/quickactions_arrowdown_right_holo_light" /> + <item android:state_last="true" android:drawable="@drawable/quickactions_arrowup_right_holo_light" /> + <!-- TODO: provide callout-less state --> + <item android:drawable="@drawable/quickactions_arrowup_right_holo_light" /> +</selector> diff --git a/res/layout/quickcontact.xml b/res/layout/quickcontact.xml index a74424c7b..e2b291c8f 100644 --- a/res/layout/quickcontact.xml +++ b/res/layout/quickcontact.xml @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2009 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,13 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> - -<view +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - class="com.android.contacts.quickcontact.QuickContactRootLayout" - android:id="@+id/root" - android:layout_width="match_parent" + android:id="@android:id/content" + android:layout_width="@dimen/quick_contact_width" android:layout_height="wrap_content" + android:visibility="invisible" android:orientation="vertical"> <FrameLayout @@ -133,4 +133,4 @@ android:text="@string/quickcontact_clear_defaults_button" /> </LinearLayout> </FrameLayout> -</view> +</LinearLayout> diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml new file mode 100644 index 000000000..aced4a8eb --- /dev/null +++ b/res/layout/quickcontact_activity.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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. +--> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + class="com.android.contacts.quickcontact.FloatingChildLayout" + android:id="@+id/floating_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="true" + android:focusableInTouchMode="true" + android:descendantFocusability="afterDescendants"> + + <include layout="@layout/quickcontact" /> + +</view> diff --git a/res/layout/quickcontact_default_item.xml b/res/layout/quickcontact_default_item.xml index 25b69100e..3a918f0f9 100755 --- a/res/layout/quickcontact_default_item.xml +++ b/res/layout/quickcontact_default_item.xml @@ -28,13 +28,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textStyle="bold" - android:textAppearance="?android:attr/textAppearanceMediumInverse" /> + android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@android:id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="-4dip" - android:textAppearance="?android:attr/textAppearanceSmallInverse" /> + android:textAppearance="?android:attr/textAppearanceSmall" /> </LinearLayout> diff --git a/res/layout/quickcontact_resolve_item.xml b/res/layout/quickcontact_resolve_item.xml index 55de80e4c..280572208 100755 --- a/res/layout/quickcontact_resolve_item.xml +++ b/res/layout/quickcontact_resolve_item.xml @@ -28,13 +28,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textStyle="bold" - android:textAppearance="?android:attr/textAppearanceMediumInverse" /> + android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@android:id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="-4dip" - android:textAppearance="?android:attr/textAppearanceSmallInverse" /> + android:textAppearance="?android:attr/textAppearanceSmall" /> </LinearLayout> diff --git a/res/values/styles.xml b/res/values/styles.xml index b8e987483..c776e1d3e 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -38,38 +38,19 @@ <item name="android:windowIsFloating">true</item> </style> - <style name="FullyTranslucent" parent="android:Theme.Translucent.NoTitleBar"> - <item name="android:windowContentOverlay">@null</item> - </style> - - <style name="FullyTranslucent.QuickContact"> - <!-- This is a hack because we want to be able to animate away the - QuickContact window, and we close its containing activity at the - same time. So put in a dummy animation so this guy sticks around - while the fast track window is animating. --> - <item name="android:windowAnimationStyle">@style/DummyAnimation</item> + <style name="Theme"> </style> - <style name="QuickContact" parent="@android:Theme.Holo.Light"> - <item name="android:windowNoTitle">true</item> - <item name="android:windowFrame">@null</item> + <style name="Theme.QuickContact" parent="@android:style/Theme.Holo.Light"> <item name="android:windowBackground">@android:color/transparent</item> - <item name="android:windowIsFloating">true</item> + <item name="android:colorBackgroundCacheHint">@null</item> + <item name="android:windowFrame">@null</item> <item name="android:windowContentOverlay">@null</item> - <!-- TODO: create our own animation style in framework --> - <!-- - <item name="android:windowAnimationStyle">@*android:style/Animation.ZoomButtons</item> - --> - </style> - - <style name="QuickContactAboveAnimation"> - <item name="android:windowEnterAnimation">@anim/quickcontact_above_enter</item> - <item name="android:windowExitAnimation">@anim/quickcontact_above_exit</item> - </style> - - <style name="QuickContactBelowAnimation"> - <item name="android:windowEnterAnimation">@anim/quickcontact_below_enter</item> - <item name="android:windowExitAnimation">@anim/quickcontact_below_exit</item> + <item name="android:windowAnimationStyle">@null</item> + <item name="android:windowIsFloating">false</item> + <item name="android:backgroundDimEnabled">false</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowNoTitle">true</item> </style> <style name="ContactsSearchAnimation"> diff --git a/src/com/android/contacts/quickcontact/FloatingChildLayout.java b/src/com/android/contacts/quickcontact/FloatingChildLayout.java new file mode 100644 index 000000000..ddba609f5 --- /dev/null +++ b/src/com/android/contacts/quickcontact/FloatingChildLayout.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2011 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.quickcontact; + +import com.android.contacts.R; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.PopupWindow; + +/** + * Layout containing single child {@link View} which it attempts to center + * around {@link #setChildTargetScreen(Rect)}. + * <p> + * Updates drawable state to be {@link android.R.attr#state_first} when child is + * above target, and {@link android.R.attr#state_last} when child is below + * target. Also updates {@link Drawable#setLevel(int)} on child + * {@link View#getBackground()} to reflect horizontal center of target. + * <p> + * The reason for this approach is because target {@link Rect} is in screen + * coordinates disregarding decor insets; otherwise something like + * {@link PopupWindow} might work better. + */ +public class FloatingChildLayout extends FrameLayout { + private static final String TAG = "FloatingChild"; + + public FloatingChildLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private View mChild; + + private Rect mTargetScreen = new Rect(); + + private int mCalloutState = 0; + private int mCalloutLeft; + + @Override + protected void onFinishInflate() { + mChild = findViewById(android.R.id.content); + mChild.setDuplicateParentStateEnabled(true); + } + + public View getChild() { + return mChild; + } + + /** + * Set {@link Rect} in screen coordinates that {@link #getChild()} should be + * centered around. + */ + public void setChildTargetScreen(Rect targetScreen) { + mTargetScreen = targetScreen; + requestLayout(); + } + + /** + * Return {@link #mTargetScreen} in local window coordinates, taking any + * decor insets into account. + */ + private Rect getTargetInWindow() { + final Rect windowScreen = new Rect(); + getWindowVisibleDisplayFrame(windowScreen); + + final Rect target = new Rect(mTargetScreen); + target.offset(-windowScreen.left, -windowScreen.top); + return target; + } + + private void updateCallout(int calloutState, int calloutLeft) { + if (mCalloutState != calloutState) { + mCalloutState = calloutState; + mChild.refreshDrawableState(); + } + + final Drawable background = mChild.getBackground(); + if (background != null && mCalloutLeft != calloutLeft) { + mCalloutLeft = calloutLeft; + background.setLevel(calloutLeft); + } + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + mergeDrawableStates(drawableState, new int[] { mCalloutState }); + return drawableState; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + final View child = mChild; + final Rect target = getTargetInWindow(); + + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + // default is no callout, left-aligned, and vertically centered + int calloutState = 0; + int childLeft = target.left; + int childTop = target.centerY() - (childHeight / 2); + + // when target is wide, horizontally center instead of left-align + if (target.width() > childWidth / 2) { + childLeft = target.centerX() - (childWidth / 2); + } + + final int areaAboveTarget = target.top; + final int areaBelowTarget = getHeight() - target.bottom; + + if (areaAboveTarget >= childHeight) { + // enough room above target, place above and callout down + calloutState = android.R.attr.state_first; + childTop = target.top - childHeight; + + } else if (areaBelowTarget >= childHeight) { + // enough room below target, place below and callout up + calloutState = android.R.attr.state_last; + childTop = target.bottom; + } + + // when child is outside bounds, nudge back inside + childLeft = clampDimension(childLeft, childWidth, getWidth()); + childTop = clampDimension(childTop, childHeight, getHeight()); + + final int calloutLeft = target.centerX() - childLeft; + updateCallout(calloutState, calloutLeft); + layoutChild(child, childLeft, childTop); + + } + + private static int clampDimension(int value, int size, int max) { + // when larger than bounds, just center + if (size > max) { + return (max - size) / 2; + } + + // clamp to lower bound + value = Math.max(value, 0); + // clamp to higher bound + value = Math.min(value, max - size); + + return value; + } + + private static void layoutChild(View child, int left, int top) { + child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); + } + + /** + * Begin animating {@link #getChild()} visible. + */ + public void showChild() { + final boolean calloutAbove = mCalloutState == android.R.attr.state_first; + final Animation anim = AnimationUtils.loadAnimation(getContext(), + calloutAbove ? R.anim.quickcontact_above_enter : R.anim.quickcontact_below_enter); + mChild.startAnimation(anim); + mChild.setVisibility(View.VISIBLE); + } + + /** + * Begin animating {@link #getChild()} invisible. + */ + public void hideChild(final Runnable onAnimationEnd) { + final boolean calloutAbove = mCalloutState == android.R.attr.state_first; + final Animation anim = AnimationUtils.loadAnimation(getContext(), + calloutAbove ? R.anim.quickcontact_above_exit : R.anim.quickcontact_below_exit); + + if (onAnimationEnd != null) { + anim.setAnimationListener(new AnimationListener() { + /** {@inheritDoc} */ + public void onAnimationStart(Animation animation) { + // ignored + } + + /** {@inheritDoc} */ + public void onAnimationRepeat(Animation animation) { + // ignored + } + + /** {@inheritDoc} */ + public void onAnimationEnd(Animation animation) { + onAnimationEnd.run(); + } + }); + } + + mChild.startAnimation(anim); + mChild.setVisibility(View.INVISIBLE); + } + + private View.OnTouchListener mOutsideTouchListener; + + public void setOnOutsideTouchListener(View.OnTouchListener listener) { + mOutsideTouchListener = listener; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // at this point, touch wasn't handled by child view; assume outside + if (mOutsideTouchListener != null) { + return mOutsideTouchListener.onTouch(this, event); + } else { + return false; + } + } +} diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java deleted file mode 100644 index 503238680..000000000 --- a/src/com/android/contacts/quickcontact/QuickContactActivity.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2009 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.quickcontact; - -import com.android.contacts.ContactsActivity; - -import android.content.ContentUris; -import android.content.Intent; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.provider.ContactsContract.QuickContact; -import android.provider.ContactsContract.RawContacts; -import android.util.Log; - -/** - * Stub translucent activity that just shows {@link QuickContactWindow} floating - * above the caller. This temporary hack should eventually be replaced with - * direct framework support. - */ -public final class QuickContactActivity extends ContactsActivity - implements QuickContactWindow.OnDismissListener { - private static final String TAG = "QuickContactActivity"; - - static final boolean LOGV = false; - static final boolean FORCE_CREATE = true; - - private QuickContactWindow mQuickContact; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - if (LOGV) Log.d(TAG, "onCreate"); - - this.onNewIntent(getIntent()); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (LOGV) Log.d(TAG, "onNewIntent"); - - if (QuickContactWindow.TRACE_LAUNCH) { - android.os.Debug.startMethodTracing(QuickContactWindow.TRACE_TAG); - } - - if (mQuickContact == null || FORCE_CREATE) { - if (LOGV) Log.d(TAG, "Preparing window"); - mQuickContact = new QuickContactWindow(this, this); - } - - // Use our local window token for now - Uri lookupUri = intent.getData(); - // Check to see whether it comes from the old version. - if (android.provider.Contacts.AUTHORITY.equals(lookupUri.getAuthority())) { - final long rawContactId = ContentUris.parseId(lookupUri); - lookupUri = RawContacts.getContactLookupUri(getContentResolver(), - ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); - } - final Bundle extras = intent.getExtras(); - - // Read requested parameters for displaying - final Rect target = intent.getSourceBounds(); - final int mode = extras.getInt(QuickContact.EXTRA_MODE, QuickContact.MODE_MEDIUM); - final String[] excludeMimes = extras.getStringArray(QuickContact.EXTRA_EXCLUDE_MIMES); - - mQuickContact.show(lookupUri, target, mode, excludeMimes); - } - - /** {@inheritDoc} */ - @Override - public void onBackPressed() { - if (LOGV) Log.w(TAG, "Unexpected back captured by stub activity"); - finish(); - } - - @Override - protected void onPause() { - super.onPause(); - if (LOGV) Log.d(TAG, "onPause"); - - // Dismiss any dialog when pausing - mQuickContact.dismiss(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (LOGV) Log.d(TAG, "onDestroy"); - } - - /** {@inheritDoc} */ - @Override - public void onDismiss(QuickContactWindow dialog) { - if (LOGV) Log.d(TAG, "onDismiss"); - - if (isTaskRoot() && !FORCE_CREATE) { - // Instead of stopping, simply push this to the back of the stack. - // This is only done when running at the top of the stack; - // otherwise, we have been launched by someone else so need to - // allow the user to go back to the caller. - moveTaskToBack(false); - } else { - finish(); - } - } -} diff --git a/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java b/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java index 911848032..15311f9d5 100644 --- a/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java +++ b/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java @@ -26,37 +26,43 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; /** - * Drawable that draws three pictures for the QuickContact-Background. ColorFilter is ignored + * Background {@link Drawable} for {@link QuickContactWindow} that draws arrow + * centered around requested position. */ public class QuickContactBackgroundDrawable extends Drawable { private Drawable mLeftDrawable; private Drawable mMiddleDrawable; private Drawable mRightDrawable; - private int mRequestedX = Integer.MIN_VALUE; - private boolean mBoundsSet = false; - private int mAlpha = -1; + private int mBottomOverride = Integer.MIN_VALUE; + public QuickContactBackgroundDrawable(Resources res) { + mLeftDrawable = res.getDrawable(R.drawable.quickactions_arrow_left_holo_light); + mMiddleDrawable = res.getDrawable(R.drawable.quickactions_arrow_middle_holo_light); + mRightDrawable = res.getDrawable(R.drawable.quickactions_arrow_right_holo_light); + } + @Override public void setAlpha(int alpha) { - mAlpha = alpha; - setChildAlpha(); + mLeftDrawable.setAlpha(alpha); + mMiddleDrawable.setAlpha(alpha); + mRightDrawable.setAlpha(alpha); } /** - * Overrides the bottom bounds. This is used for the animation when the QuickContact - * expands/collapses options + * Overrides the bottom bounds. This is used for the animation when the + * QuickContact expands/collapses options */ public void setBottomOverride(int value) { mBottomOverride = value; - setChildBounds(); + onBoundsChange(getBounds()); invalidateSelf(); } public void clearBottomOverride() { mBottomOverride = Integer.MIN_VALUE; + onBoundsChange(getBounds()); invalidateSelf(); - setChildBounds(); } public float getBottomOverride() { @@ -64,62 +70,52 @@ public class QuickContactBackgroundDrawable extends Drawable { } @Override - public void setColorFilter(ColorFilter cf) { + public boolean isStateful() { + return true; } @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; + protected boolean onStateChange(int[] state) { + super.onStateChange(state); + mLeftDrawable.setState(state); + mMiddleDrawable.setState(state); + mRightDrawable.setState(state); + return true; } - public void configure(Resources resources, boolean arrowUp, int requestedX) { - mLeftDrawable = resources.getDrawable(arrowUp - ? R.drawable.quickactions_arrowup_left_holo_light - : R.drawable.quickactions_arrowdown_left_holo_light); - mMiddleDrawable = resources.getDrawable(arrowUp - ? R.drawable.quickactions_arrowup_middle_holo_light - : R.drawable.quickactions_arrowdown_middle_holo_light); - mRightDrawable = resources.getDrawable(arrowUp - ? R.drawable.quickactions_arrowup_right_holo_light - : R.drawable.quickactions_arrowdown_right_holo_light); - - mRequestedX = requestedX; - - setChildAlpha(); - setChildBounds(); + @Override + protected boolean onLevelChange(int level) { + return true; } @Override - protected void onBoundsChange(Rect bounds) { - mBoundsSet = true; - setChildBounds(); + public void setColorFilter(ColorFilter cf) { + mLeftDrawable.setColorFilter(cf); + mMiddleDrawable.setColorFilter(cf); + mRightDrawable.setColorFilter(cf); } - private void setChildAlpha() { - if (mAlpha == -1) return; - - if (mLeftDrawable != null) mLeftDrawable.setAlpha(mAlpha); - if (mMiddleDrawable != null) mMiddleDrawable.setAlpha(mAlpha); - if (mRightDrawable != null) mRightDrawable.setAlpha(mAlpha); + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; } - private void setChildBounds() { - if (mRequestedX == Integer.MIN_VALUE) return; - if (!mBoundsSet) return; + @Override + protected void onBoundsChange(Rect bounds) { + final int requestedX = getLevel(); - final Rect bounds = getBounds(); - int middleLeft = mRequestedX - mMiddleDrawable.getIntrinsicWidth() / 2; - int middleRight = mRequestedX + mMiddleDrawable.getIntrinsicWidth() / 2; + int middleLeft = requestedX - mMiddleDrawable.getIntrinsicWidth() / 2; + int middleRight = requestedX + mMiddleDrawable.getIntrinsicWidth() / 2; // ensure left drawable is not smaller than its Intrinsic Width - final int leftExtra = (middleLeft - bounds.left) - mLeftDrawable.getIntrinsicWidth(); + final int leftExtra = (middleLeft - bounds.left) - mLeftDrawable.getIntrinsicWidth(); if (leftExtra < 0) { middleLeft -= leftExtra; middleRight -= leftExtra; } // ensure right drawable is not smaller than its Intrinsic Width - final int rightExtra = (bounds.right - middleRight) - mRightDrawable.getIntrinsicWidth(); + final int rightExtra = (bounds.right - middleRight) - mRightDrawable.getIntrinsicWidth(); if (rightExtra < 0) { middleLeft += rightExtra; middleRight += rightExtra; diff --git a/src/com/android/contacts/quickcontact/QuickContactRootLayout.java b/src/com/android/contacts/quickcontact/QuickContactRootLayout.java deleted file mode 100644 index 007783a96..000000000 --- a/src/com/android/contacts/quickcontact/QuickContactRootLayout.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.contacts.quickcontact; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.widget.LinearLayout; - -/** - * Custom layout for Quick Contact. It intercepts the BACK key and - * close QC even when the soft keyboard is open. - */ -public class QuickContactRootLayout extends LinearLayout { - private Listener mListener; - - public QuickContactRootLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setListener(Listener value) { - mListener = value; - } - - /** - * Intercepts the BACK key event and dismisses QuickContact window. - */ - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - if (mListener != null) mListener.onBackPressed(); - return true; - } - return super.dispatchKeyEventPreIme(event); - } - - public interface Listener { - void onBackPressed(); - } -} diff --git a/src/com/android/contacts/quickcontact/QuickContactWindow.java b/src/com/android/contacts/quickcontact/QuickContactWindow.java index 2a6dd2f7b..5f4bcc98e 100644 --- a/src/com/android/contacts/quickcontact/QuickContactWindow.java +++ b/src/com/android/contacts/quickcontact/QuickContactWindow.java @@ -25,13 +25,15 @@ import com.android.contacts.model.DataKind; import com.android.contacts.util.Constants; import com.android.contacts.util.DataStatus; import com.android.contacts.util.NotifyingAsyncQueryHandler; -import com.android.internal.policy.PolicyManager; import com.google.android.collect.Lists; +import com.google.common.base.Preconditions; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -41,6 +43,7 @@ import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Im; @@ -54,30 +57,16 @@ import android.provider.ContactsContract.Data; import android.provider.ContactsContract.QuickContact; import android.provider.ContactsContract.RawContacts; import android.text.TextUtils; -import android.util.Log; -import android.view.ActionMode; -import android.view.ContextThemeWrapper; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewStub; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.Window; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; @@ -88,72 +77,44 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; /** - * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}. + * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads + * data asynchronously, and then shows a popup with details centered around + * {@link Intent#getSourceBounds()}. */ -public class QuickContactWindow implements Window.Callback, +public class QuickContactWindow extends Activity implements NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener, - AbsListView.OnItemClickListener, KeyEvent.Callback, OnGlobalLayoutListener, - QuickContactRootLayout.Listener { + AbsListView.OnItemClickListener { + private static final String TAG = "QuickContact"; - private static final String TAG = "QuickContactWindow"; + private static final boolean TRACE_LAUNCH = false; + private static final String TRACE_TAG = "quickcontact"; - /** - * Interface used to allow the person showing a {@link QuickContactWindow} to - * know when the window has been dismissed. - */ - public interface OnDismissListener { - public void onDismiss(QuickContactWindow dialog); - } - - private final static int ANIMATION_FADE_IN_TIME = 100; - private final static int ANIMATION_FADE_OUT_TIME = 100; - private final static int ANIMATION_EXPAND_TIME = 100; - private final static int ANIMATION_COLLAPSE_TIME = 100; - - /** - * If the anchor is wider than (quick contact width * this constant) then - * center quick contact. Otherwise, left-align. - */ - private static final double MIN_RELATIVE_ANCHOR_WIDTH_TO_CENTER = 0.5; - - private final Context mContext; - private final LayoutInflater mInflater; - private final WindowManager mWindowManager; - private Window mWindow; - private View mDecor; - private final Rect mRect = new Rect(); - - private boolean mDismissed = false; - private boolean mQuerying = false; - private boolean mShowing = false; + private static final int ANIMATION_FADE_IN_TIME = 100; + private static final int ANIMATION_FADE_OUT_TIME = 100; + private static final int ANIMATION_EXPAND_TIME = 100; + private static final int ANIMATION_COLLAPSE_TIME = 100; private NotifyingAsyncQueryHandler mHandler; - private OnDismissListener mDismissListener; private Uri mLookupUri; - private Rect mAnchor; - - private int mScreenWidth; - private int mUseableScreenHeight; - private int mRequestedY; + private int mMode; + private String[] mExcludeMimes; private boolean mHasValidSocial = false; - private int mMode; - private QuickContactRootLayout mRootView; + private FloatingChildLayout mFloatingLayout; private QuickContactBackgroundDrawable mBackground; + private View mHeader; - private HorizontalScrollView mTrackScroll; private ViewGroup mTrack; - private FrameLayout mFooter; private LinearLayout mFooterDisambig; private LinearLayout mFooterClearDefaults; + private ListView mResolveList; private CheckableImageView mLastAction; private CheckBox mSetPrimaryCheckBox; @@ -165,9 +126,6 @@ public class QuickContactWindow implements Window.Callback, */ private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); - private int mWindowRecycled = 0; - private int mActionRecycled = 0; - /** * Set of {@link Action} that are associated with the aggregate currently * displayed by this dialog, represented as a map from {@link String} @@ -176,14 +134,6 @@ public class QuickContactWindow implements Window.Callback, private ActionMultiMap mActions = new ActionMultiMap(); /** - * Pool of unused {@link CheckableImageView} that have previously been - * inflated, and are ready to be recycled through {@link #obtainView()}. - */ - private LinkedList<CheckableImageView> mActionPool = new LinkedList<CheckableImageView>(); - - private String[] mExcludeMimes; - - /** * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types. * * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog, @@ -221,82 +171,48 @@ public class QuickContactWindow implements Window.Callback, }); private static final int TOKEN_DATA = 1; - static final boolean TRACE_LAUNCH = false; - static final String TRACE_TAG = "quickcontact"; + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); - /** - * Prepare a dialog to show in the given {@link Context}. - */ - public QuickContactWindow(Context context) { - mContext = new ContextThemeWrapper(context, R.style.QuickContact); - mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - - mWindow = PolicyManager.makeNewWindow(mContext); - mWindow.setCallback(this); - mWindow.setWindowManager(mWindowManager, null, null); - mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED); - - mWindow.setContentView(R.layout.quickcontact); - - mRootView = (QuickContactRootLayout)mWindow.findViewById(R.id.root); - mRootView.setListener(this); - mRootView.setFocusable(true); - mRootView.setFocusableInTouchMode(true); - mRootView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - - mBackground = new QuickContactBackgroundDrawable(); - mRootView.setBackgroundDrawable(mBackground); - - mScreenWidth = mWindowManager.getDefaultDisplay().getWidth(); - // Status bar height - final int screenMarginBottom = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.screen_margin_bottom); - mUseableScreenHeight = mWindowManager.getDefaultDisplay().getHeight() - screenMarginBottom; - - mTrack = (ViewGroup) mWindow.findViewById(R.id.quickcontact); - mTrackScroll = (HorizontalScrollView) mWindow.findViewById(R.id.scroll); - - mFooter = (FrameLayout) mWindow.findViewById(R.id.footer); - mFooterDisambig = (LinearLayout) mWindow.findViewById(R.id.footer_disambig); - mFooterClearDefaults = (LinearLayout) mWindow.findViewById(R.id.footer_clear_defaults); - mResolveList = (ListView) mWindow.findViewById(android.R.id.list); - mSetPrimaryCheckBox = (CheckBox) mWindow.findViewById(android.R.id.checkbox); - - mDefaultsListView = (ListView) mWindow.findViewById(R.id.defaults_list); - mClearDefaultsButton = (Button) mWindow.findViewById(R.id.clear_defaults_button); - mClearDefaultsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - clearDefaults(); - } - }); + setContentView(R.layout.quickcontact_activity); - mResolveList.setOnItemClickListener(QuickContactWindow.this); + mBackground = new QuickContactBackgroundDrawable(getResources()); - mHandler = new NotifyingAsyncQueryHandler(mContext, this); - } + mFloatingLayout = findTypedViewById(R.id.floating_layout); + mFloatingLayout.getChild().setBackgroundDrawable(mBackground); + mFloatingLayout.setOnOutsideTouchListener(mOnOutsideTouchListener); - /** - * Prepare a dialog to show in the given {@link Context}, and notify the - * given {@link OnDismissListener} each time this dialog is dismissed. - */ - public QuickContactWindow(Context context, OnDismissListener dismissListener) { - this(context); - mDismissListener = dismissListener; + mTrack = findTypedViewById(R.id.quickcontact); + mFooter = findTypedViewById(R.id.footer); + mFooterDisambig = findTypedViewById(R.id.footer_disambig); + mFooterClearDefaults = findTypedViewById(R.id.footer_clear_defaults); + mResolveList = findTypedViewById(android.R.id.list); + mSetPrimaryCheckBox = findTypedViewById(android.R.id.checkbox); + + mDefaultsListView = findTypedViewById(R.id.defaults_list); + + mClearDefaultsButton = findTypedViewById(R.id.clear_defaults_button); + mClearDefaultsButton.setOnClickListener(mOnClearDefaultsClickListener); + + mResolveList.setOnItemClickListener(this); + + mHandler = new NotifyingAsyncQueryHandler(this, this); + + show(); } private View getHeaderView(int mode) { View header = null; switch (mode) { case QuickContact.MODE_SMALL: - header = mWindow.findViewById(R.id.header_small); + header = findViewById(R.id.header_small); break; case QuickContact.MODE_MEDIUM: - header = mWindow.findViewById(R.id.header_medium); + header = findViewById(R.id.header_medium); break; case QuickContact.MODE_LARGE: - header = mWindow.findViewById(R.id.header_large); + header = findViewById(R.id.header_large); break; } @@ -311,60 +227,52 @@ public class QuickContactWindow implements Window.Callback, return header; } - /** - * Start showing a dialog for the given {@link Contacts#_ID} pointing - * towards the given location. - */ - public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) { - if (mQuerying || mShowing) { - Log.w(TAG, "dismissing before showing"); - dismissInternal(); - } + private void show() { - if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) { + if (TRACE_LAUNCH) { android.os.Debug.startMethodTracing(TRACE_TAG); } - // Validate incoming parameters - final boolean validMode = (mode == QuickContact.MODE_SMALL - || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE); - if (!validMode) { - throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, " - + "MODE_MEDIUM, or MODE_SMALL"); - } + final Intent intent = getIntent(); + + Uri lookupUri = intent.getData(); - if (anchor == null) { - throw new IllegalArgumentException("Missing anchor rectangle"); + // Check to see whether it comes from the old version. + if (android.provider.Contacts.AUTHORITY.equals(lookupUri.getAuthority())) { + final long rawContactId = ContentUris.parseId(lookupUri); + lookupUri = RawContacts.getContactLookupUri(getContentResolver(), + ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); } - // Prepare header view for requested mode - mLookupUri = lookupUri; - mAnchor = new Rect(anchor); - mMode = mode; - mExcludeMimes = excludeMimes; + mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); - mHeader = getHeaderView(mode); + // Read requested parameters for displaying + final Rect targetScreen = intent.getSourceBounds(); + Preconditions.checkNotNull(targetScreen, "missing targetScreen"); + mFloatingLayout.setChildTargetScreen(targetScreen); - setHeaderText(R.id.name, R.string.quickcontact_missing_name); + mMode = intent.getIntExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_MEDIUM); + mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); + + switch (mMode) { + case QuickContact.MODE_SMALL: + case QuickContact.MODE_MEDIUM: + case QuickContact.MODE_LARGE: + break; + default: + throw new IllegalArgumentException("Unexpected mode: " + mMode); + } + // find and prepare correct header view + mHeader = getHeaderView(mMode); + setHeaderText(R.id.name, R.string.quickcontact_missing_name); setHeaderText(R.id.status, null); setHeaderText(R.id.timestamp, null); - setHeaderImage(R.id.presence, null); - resetTrack(); - - // We need to have a focused view inside the QuickContact window so - // that the BACK key event can be intercepted - mRootView.requestFocus(); - - mHasValidSocial = false; - mDismissed = false; - mQuerying = true; - // Start background query for data, but only select photo rows when they // directly match the super-primary PHOTO_ID. - final Uri dataUri = getDataUri(lookupUri); + final Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY); mHandler.cancelOperation(TOKEN_DATA); // Only request photo data when required by mode @@ -380,226 +288,85 @@ public class QuickContactWindow implements Window.Callback, } } - /** - * Build a {@link Uri} into the {@link Data} table for the requested - * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}. - */ - private Uri getDataUri(Uri lookupUri) { - return Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY); + @SuppressWarnings("unchecked") + private <T> T findTypedViewById(int id) { + return (T) super.findViewById(id); } - /** - * Creates and configures the background resource - */ - private void configureBackground(boolean arrowUp, int requestedX) { - mBackground.configure(mContext.getResources(), arrowUp, requestedX); - } - - /** - * Actual internal method to show this dialog. Called only by - * {@link #considerShowing()} when all data requirements have been met. - */ - private void showInternal() { - mDecor = mWindow.getDecorView(); - mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this); - WindowManager.LayoutParams layoutParams = mWindow.getAttributes(); - - layoutParams.width = mContext.getResources().getDimensionPixelSize( - R.dimen.quick_contact_width); - layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; - - // Try to left align with the anchor control or center if the anchor is wide - if (mAnchor.left + layoutParams.width <= mScreenWidth) { - if (mAnchor.width() > layoutParams.width * MIN_RELATIVE_ANCHOR_WIDTH_TO_CENTER) { - layoutParams.x = mAnchor.left + (mAnchor.width() - layoutParams.width) / 2; - } else { - layoutParams.x = mAnchor.left; - } - } else { - // Not enough space. Try to right align to the anchor - if (mAnchor.right - layoutParams.width >= 0) { - layoutParams.x = mAnchor.right - layoutParams.width; - } else { - // Also not enough space. Use the whole screen width available - layoutParams.x = 0; - layoutParams.width = mScreenWidth; - } + private View.OnTouchListener mOnOutsideTouchListener = new View.OnTouchListener() { + /** {@inheritDoc} */ + public boolean onTouch(View v, MotionEvent event) { + hide(true); + return true; } + }; - // Force layout measuring pass so we have baseline numbers - mDecor.measure(layoutParams.width, layoutParams.height); - final int blockHeight = mDecor.getMeasuredHeight(); - - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - - if (mUseableScreenHeight - mAnchor.bottom > blockHeight) { - // Show downwards callout when enough room, aligning block top with bottom of - // anchor area, and adjusting to inset arrow. - configureBackground(true, mAnchor.centerX() - layoutParams.x); - layoutParams.y = mAnchor.bottom; - layoutParams.windowAnimations = R.style.QuickContactBelowAnimation; - } else { - // Show upwards callout, aligning bottom block - // edge with top of anchor area, and adjusting to inset arrow. - configureBackground(false, mAnchor.centerX() - layoutParams.x); - layoutParams.y = mAnchor.top - blockHeight; - layoutParams.windowAnimations = R.style.QuickContactAboveAnimation; + private View.OnClickListener mOnClearDefaultsClickListener = new View.OnClickListener() { + /** {@inheritDoc} */ + public void onClick(View v) { + clearDefaults(); } + }; - layoutParams.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; - - mRequestedY = layoutParams.y; - mWindowManager.addView(mDecor, layoutParams); - mShowing = true; - mQuerying = false; - mDismissed = false; + private void hide(boolean withAnimation) { + // cancel any pending queries + mHandler.cancelOperation(TOKEN_DATA); - if (TRACE_LAUNCH) { - android.os.Debug.stopMethodTracing(); - Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets " - + mActionRecycled + " times"); + if (withAnimation) { + mFloatingLayout.hideChild(new Runnable() { + /** {@inheritDoc} */ + public void run() { + finish(); + } + }); + } else { + mFloatingLayout.hideChild(null); + finish(); } } - /** {@inheritDoc} */ @Override - public void onGlobalLayout() { - layoutInScreen(); - } - - /** - * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best - * as possible, shifting up to display content as needed. - */ - private void layoutInScreen() { - if (!mShowing) return; - - final WindowManager.LayoutParams l = mWindow.getAttributes(); - final int originalY = l.y; - - final int blockHeight = mDecor.getHeight(); - - l.y = mRequestedY; - if (mRequestedY + blockHeight > mUseableScreenHeight) { - // Shift up from bottom when overflowing - l.y = mUseableScreenHeight - blockHeight; - } - - if (originalY != l.y) { - // Only update when value is changed - mWindow.setAttributes(l); - } - } - - /** - * Dismiss this dialog if showing. - */ - public synchronized void dismiss() { - // Notify any listeners that we've been dismissed - if (mDismissListener != null) { - mDismissListener.onDismiss(this); - } - - dismissInternal(); - } - - private void dismissInternal() { - // Remove any attached window decor for recycling - boolean hadDecor = mDecor != null; - if (hadDecor) { - mWindowManager.removeView(mDecor); - mWindowRecycled++; - mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this); - mDecor = null; - mWindow.closeAllPanels(); - } - mShowing = false; - mDismissed = true; - - // Cancel any pending queries - mHandler.cancelOperation(TOKEN_DATA); - mQuerying = false; - - // Completely hide header and reset track - mHeader.setVisibility(View.GONE); - resetTrack(); + public void onBackPressed() { + hide(true); } - /** - * Reset track to initial state, recycling any chiclets. - */ - private void resetTrack() { - // Clear background height-animation override - mBackground.clearBottomOverride(); - - // Release reference to last chiclet - mLastAction = null; - - // Clear track actions and scroll to hard left - mActions.clear(); - - // Recycle any chiclets in use - for (int i = mTrack.getChildCount() - 1; i >= 0; i--) { - final View child = mTrack.getChildAt(i); - // there can be non-CheckableImageView children, e.g. a "No Data" label - if (child instanceof CheckableImageView) { - releaseView((CheckableImageView)child); + /** {@inheritDoc} */ + public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) { + try { + if (isFinishing()) { + hide(false); + return; + } else if (cursor == null || cursor.getCount() == 0) { + Toast.makeText(this, R.string.invalidContactMessage, Toast.LENGTH_LONG).show(); + hide(false); + return; } - mTrack.removeViewAt(i); - } - mTrackScroll.fullScroll(View.FOCUS_LEFT); + bindData(cursor); - // Clear any primary requests - mSetPrimaryCheckBox.setChecked(false); - - setNewActionViewChecked(null); - mFooter.setVisibility(View.GONE); - } - - /** - * Consider showing this window, which will only call through to - * {@link #showInternal()} when all data items are present. - */ - private void considerShowing() { - if (!mShowing && !mDismissed) { if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) { // Missing valid social, swap medium for small header mHeader.setVisibility(View.GONE); mHeader = getHeaderView(QuickContact.MODE_SMALL); } - // All queries have returned, pull curtain - showInternal(); - } - } - - /** {@inheritDoc} */ - @Override - public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) { - // Bail early when query is stale - if (cookie != mLookupUri) return; - - if (cursor == null) { - // Problem while running query, so bail without showing - Log.w(TAG, "Missing cursor for token=" + token); - this.dismiss(); - return; - } + if (TRACE_LAUNCH) { + android.os.Debug.stopMethodTracing(); + } - handleData(cursor); + // data bound and ready, pull curtain to show + mFloatingLayout.showChild(); - if (!cursor.isClosed()) { - cursor.close(); + } finally { + if (cursor != null) { + cursor.close(); + } } - - considerShowing(); } /** Assign this string to the view, if found in {@link #mHeader}. */ private void setHeaderText(int id, int resId) { - setHeaderText(id, mContext.getResources().getText(resId)); + setHeaderText(id, getText(resId)); } /** Assign this string to the view, if found in {@link #mHeader}. */ @@ -637,26 +404,22 @@ public class QuickContactWindow implements Window.Callback, /** * Handle the result from the {@link #TOKEN_DATA} query. */ - private void handleData(Cursor cursor) { - final ResolveCache cache = ResolveCache.getInstance(mContext); - if (cursor == null) return; - if (cursor.getCount() == 0) { - Toast.makeText(mContext, R.string.invalidContactMessage, Toast.LENGTH_LONG).show(); - dismiss(); - return; - } + private void bindData(Cursor cursor) { + final ResolveCache cache = ResolveCache.getInstance(this); + final Context context = this; if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) { // Add the profile shortcut action - final Action action = new ProfileAction(mContext, mLookupUri); + final Action action = new ProfileAction(context, mLookupUri); mActions.put(Contacts.CONTENT_ITEM_TYPE, action); } mDefaultsMap.clear(); final DataStatus status = new DataStatus(); - final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); - final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo); + final AccountTypeManager accountTypes = AccountTypeManager.getInstance( + context.getApplicationContext()); + final ImageView photoView = (ImageView) mHeader.findViewById(R.id.photo); Bitmap photoBitmap = null; while (cursor.moveToNext()) { @@ -688,7 +451,7 @@ public class QuickContactWindow implements Window.Callback, // Build an action for this data entry, find a mapping to a UI // element, build its summary from the cursor, and collect it // along with all others of this MIME-type. - final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor); + final Action action = new DataAction(context, mimeType, kind, dataId, cursor); final boolean wasAdded = considerAdd(action, cache); if (wasAdded) { // Remember the default @@ -700,7 +463,7 @@ public class QuickContactWindow implements Window.Callback, // If phone number, also insert as text message action if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) { - final DataAction action = new DataAction(mContext, Constants.MIME_TYPE_SMS_ADDRESS, + final DataAction action = new DataAction(context, Constants.MIME_TYPE_SMS_ADDRESS, kind, dataId, cursor); considerAdd(action, cache); } @@ -713,7 +476,7 @@ public class QuickContactWindow implements Window.Callback, final DataKind imKind = accountTypes.getKindOrFallback(accountType, Im.CONTENT_ITEM_TYPE); if (imKind != null) { - final DataAction action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind, + final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, imKind, dataId, cursor); considerAdd(action, cache); isIm = true; @@ -726,7 +489,7 @@ public class QuickContactWindow implements Window.Callback, final DataKind imKind = accountTypes.getKindOrFallback(accountType, Im.CONTENT_ITEM_TYPE); if (imKind != null) { - final DataAction chatAction = new DataAction(mContext, + final DataAction chatAction = new DataAction(context, Constants.MIME_TYPE_VIDEO_CHAT, imKind, dataId, cursor); considerAdd(chatAction, cache); } @@ -760,7 +523,7 @@ public class QuickContactWindow implements Window.Callback, final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE); final int chatCapability = cursor.getInt(DataQuery.CONTACT_CHAT_CAPABILITY); final Drawable statusIcon = ContactPresenceIconUtil.getChatCapabilityIcon( - mContext, presence, chatCapability); + context, presence, chatCapability); setHeaderText(R.id.name, name); setHeaderImage(R.id.presence, statusIcon); @@ -776,7 +539,7 @@ public class QuickContactWindow implements Window.Callback, if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) { // Update status when valid was found setHeaderText(R.id.status, status.getStatus()); - setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext)); + setHeaderText(R.id.timestamp, status.getTimestampLabel(context)); } // Turn our list of actions into UI elements @@ -790,7 +553,7 @@ public class QuickContactWindow implements Window.Callback, for (String mimeType : PRECEDING_MIMETYPES) { if (containedTypes.contains(mimeType)) { hasData = true; - mTrack.addView(inflateAction(mimeType, cache)); + mTrack.addView(inflateAction(mimeType, cache, mTrack)); containedTypes.remove(mimeType); } } @@ -802,7 +565,7 @@ public class QuickContactWindow implements Window.Callback, for (String mimeType : FOLLOWING_MIMETYPES) { if (containedTypes.contains(mimeType)) { hasData = true; - mTrack.addView(inflateAction(mimeType, cache)); + mTrack.addView(inflateAction(mimeType, cache, mTrack)); containedTypes.remove(mimeType); } } @@ -811,9 +574,11 @@ public class QuickContactWindow implements Window.Callback, if (containedTypes.contains(ClearDefaultsAction.PSEUDO_MIME_TYPE)) { final ClearDefaultsAction action = (ClearDefaultsAction) mActions.get( ClearDefaultsAction.PSEUDO_MIME_TYPE).get(0); - final CheckableImageView view = obtainView(); + final CheckableImageView view = (CheckableImageView) getLayoutInflater().inflate( + R.layout.quickcontact_item, mTrack, false); + view.setChecked(false); - final String description = mContext.getResources().getString( + final String description = context.getResources().getString( R.string.quickcontact_clear_defaults_description); view.setContentDescription(description); view.setImageResource(R.drawable.ic_menu_settings_holo_light); @@ -829,12 +594,13 @@ public class QuickContactWindow implements Window.Callback, if (remainingTypes.length > 0) hasData = true; Arrays.sort(remainingTypes); for (String mimeType : remainingTypes) { - mTrack.addView(inflateAction(mimeType, cache), index++); + mTrack.addView(inflateAction(mimeType, cache, mTrack), index++); } if (!hasData) { // When there is no data to display, add a TextView to show the user there's no data - View view = mInflater.inflate(R.layout.quickcontact_item_nodata, mTrack, false); + View view = getLayoutInflater().inflate( + R.layout.quickcontact_item_nodata, mTrack, false); mTrack.addView(view, index++); } } @@ -843,7 +609,9 @@ public class QuickContactWindow implements Window.Callback, * Clears the defaults currently set on the Contact */ private void clearDefaults() { + final Context context = this; final Set<String> mimeTypesKeySet = mDefaultsMap.keySet(); + // Copy to array so that we can modify the HashMap below final String[] mimeTypes = new String[mimeTypesKeySet.size()]; mimeTypesKeySet.toArray(mimeTypes); @@ -851,9 +619,9 @@ public class QuickContactWindow implements Window.Callback, // Send clear default Intents, one by one for (String mimeType : mimeTypes) { final Action action = mDefaultsMap.get(mimeType); - final Intent intent = - ContactSaveService.createClearPrimaryIntent(mContext, action.getDataId()); - mContext.startService(intent); + final Intent intent = ContactSaveService.createClearPrimaryIntent( + context, action.getDataId()); + context.startService(intent); mDefaultsMap.remove(mimeType); } @@ -864,7 +632,6 @@ public class QuickContactWindow implements Window.Callback, for (int i = mTrack.getChildCount() - 1; i >= 0; i--) { final CheckableImageView button = (CheckableImageView) mTrack.getChildAt(i); if (button.getTag() instanceof ClearDefaultsAction) { - releaseView(button); mTrack.removeViewAt(i); break; } @@ -888,35 +655,12 @@ public class QuickContactWindow implements Window.Callback, } /** - * Obtain a new {@link CheckableImageView} for a new chiclet, either by - * recycling one from {@link #mActionPool}, or by inflating a new one. When - * finished, use {@link #releaseView(CheckableImageView)} to return back into the pool for - * later recycling. - */ - private synchronized CheckableImageView obtainView() { - CheckableImageView view = mActionPool.poll(); - if (view == null || QuickContactActivity.FORCE_CREATE) { - view = (CheckableImageView) mInflater.inflate(R.layout.quickcontact_item, mTrack, - false); - } - return view; - } - - /** - * Return the given {@link CheckableImageView} into our internal pool for - * possible recycling during another pass. - */ - private synchronized void releaseView(CheckableImageView view) { - mActionPool.offer(view); - mActionRecycled++; - } - - /** * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values. * Will use the icon provided by the {@link DataKind}. */ - private View inflateAction(String mimeType, ResolveCache resolveCache) { - final CheckableImageView view = obtainView(); + private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) { + final CheckableImageView view = (CheckableImageView) getLayoutInflater().inflate( + R.layout.quickcontact_item, root, false); // Add direct intent if single child, otherwise flag for multiple List<Action> children = mActions.get(mimeType); @@ -996,6 +740,13 @@ public class QuickContactWindow implements Window.Callback, expandAnimator.setDuration(ANIMATION_EXPAND_TIME); expandAnimator.start(); + expandAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBackground.clearBottomOverride(); + } + }); + final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(mFooter, "alpha", 0.0f, 1.0f); fadeInAnimator.setDuration(ANIMATION_FADE_IN_TIME); @@ -1008,9 +759,12 @@ public class QuickContactWindow implements Window.Callback, /** {@inheritDoc} */ @Override public void onClick(View view) { + final Context context = this; + final boolean isActionView = (view instanceof CheckableImageView); final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null; final Object tag = view.getTag(); + if (tag instanceof ClearDefaultsAction) { // Do nothing if already open if (actionView == mLastAction) return; @@ -1051,29 +805,28 @@ public class QuickContactWindow implements Window.Callback, @Override public View getView(int position, View convertView, ViewGroup parent) { - final View result = convertView != null ? convertView : - mInflater.inflate(R.layout.quickcontact_default_item, - parent, false); + if (convertView == null) { + convertView = getLayoutInflater().inflate( + R.layout.quickcontact_default_item, parent, false); + } + // Set action title based on summary value final Action defaultAction = actions[position]; - TextView text1 = (TextView)result.findViewById(android.R.id.text1); - TextView text2 = (TextView)result.findViewById(android.R.id.text2); + final TextView text1 = (TextView) convertView.findViewById( + android.R.id.text1); + final TextView text2 = (TextView) convertView.findViewById( + android.R.id.text2); text1.setText(defaultAction.getHeader()); text2.setText(defaultAction.getBody()); - result.setTag(defaultAction); - return result; + convertView.setTag(defaultAction); + return convertView; } }); animateExpand(true); - // Make sure we resize to make room for ListView - if (mDecor != null) { - mDecor.forceLayout(); - mDecor.invalidate(); - } } }; if (mFooter.getVisibility() == View.VISIBLE) { @@ -1122,24 +875,24 @@ public class QuickContactWindow implements Window.Callback, public void run() { // Incoming tag is concrete intent, so try launching try { - mContext.startActivity(action.getIntent()); + context.startActivity(action.getIntent()); } catch (ActivityNotFoundException e) { - Toast.makeText(mContext, R.string.quickcontact_missing_app, + Toast.makeText(context, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT).show(); } // Hide the resolution list, if present setNewActionViewChecked(null); - dismiss(); - mFooter.setVisibility(View.GONE); // Set default? final long dataId = action.getDataId(); if (makePrimary && dataId != -1) { Intent serviceIntent = ContactSaveService.createSetSuperPrimaryIntent( - mContext, dataId); - mContext.startService(serviceIntent); + context, dataId); + context.startService(serviceIntent); } + + hide(false); } }; if (isActionView && mFooter.getVisibility() == View.VISIBLE) { @@ -1184,31 +937,31 @@ public class QuickContactWindow implements Window.Callback, @Override public View getView(int position, View convertView, ViewGroup parent) { - final View result = convertView != null ? convertView : - mInflater.inflate(R.layout.quickcontact_resolve_item, - parent, false); + if (convertView == null) { + convertView = getLayoutInflater().inflate( + R.layout.quickcontact_resolve_item, parent, false); + } + // Set action title based on summary value final Action listAction = actionList.get(position); - TextView text1 = (TextView)result.findViewById(android.R.id.text1); - TextView text2 = (TextView)result.findViewById(android.R.id.text2); + final TextView text1 = (TextView) convertView.findViewById( + android.R.id.text1); + final TextView text2 = (TextView) convertView.findViewById( + android.R.id.text2); text1.setText(listAction.getHeader()); text2.setText(listAction.getBody()); - result.setTag(listAction); - return result; + convertView.setTag(listAction); + return convertView; } }); animateExpand(false); - // Make sure we resize to make room for ListView - if (mDecor != null) { - mDecor.forceLayout(); - mDecor.invalidate(); - } } }; + if (mFooter.getVisibility() == View.VISIBLE) { // If the expansion list is currently opened, animate its collapse and then // execute the target app @@ -1219,191 +972,6 @@ public class QuickContactWindow implements Window.Callback, } } - @Override - public void onBackPressed() { - dismiss(); - } - - /** {@inheritDoc} */ - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (mWindow.superDispatchKeyEvent(event)) { - return true; - } - return event.dispatch(this, mDecor != null - ? mDecor.getKeyDispatcherState() : null, this); - } - - /** {@inheritDoc} */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - event.startTracking(); - return true; - } - - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() - && !event.isCanceled()) { - onBackPressed(); - return true; - } - - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { - return false; - } - - /** {@inheritDoc} */ - public boolean dispatchKeyShortcutEvent(KeyEvent event) { - return mWindow.superDispatchKeyShortcutEvent(event); - } - - /** {@inheritDoc} */ - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - // TODO: make this window accessible - return false; - } - - /** - * Detect if the given {@link MotionEvent} is outside the boundaries of this - * window, which usually means we should dismiss. - */ - protected void detectEventOutside(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) { - // Only try detecting outside events on down-press - mDecor.getHitRect(mRect); - final int x = (int)event.getX(); - final int y = (int)event.getY(); - if (!mRect.contains(x, y)) { - event.setAction(MotionEvent.ACTION_OUTSIDE); - } - } - } - - /** {@inheritDoc} */ - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - detectEventOutside(event); - if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { - dismiss(); - return true; - } - return mWindow.superDispatchTouchEvent(event); - } - - /** {@inheritDoc} */ - @Override - public boolean dispatchTrackballEvent(MotionEvent event) { - return mWindow.superDispatchTrackballEvent(event); - } - - /** {@inheritDoc} */ - @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - return mWindow.superDispatchGenericMotionEvent(event); - } - - /** {@inheritDoc} */ - @Override - public void onContentChanged() { - } - - /** {@inheritDoc} */ - @Override - public boolean onCreatePanelMenu(int featureId, Menu menu) { - return false; - } - - /** {@inheritDoc} */ - @Override - public View onCreatePanelView(int featureId) { - return null; - } - - /** {@inheritDoc} */ - @Override - public boolean onMenuItemSelected(int featureId, MenuItem item) { - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean onMenuOpened(int featureId, Menu menu) { - return false; - } - - /** {@inheritDoc} */ - @Override - public void onPanelClosed(int featureId, Menu menu) { - } - - /** {@inheritDoc} */ - @Override - public boolean onPreparePanel(int featureId, View view, Menu menu) { - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean onSearchRequested() { - return false; - } - - /** {@inheritDoc} */ - @Override - public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) { - if (mDecor != null) { - mWindowManager.updateViewLayout(mDecor, attrs); - } - } - - /** {@inheritDoc} */ - @Override - public void onWindowFocusChanged(boolean hasFocus) { - } - - /** {@inheritDoc} */ - @Override - public void onAttachedToWindow() { - // No actions - } - - /** {@inheritDoc} */ - @Override - public void onDetachedFromWindow() { - // No actions - } - - /** {@inheritDoc} */ - @Override - public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { - return null; - } - - @Override - public void onActionModeStarted(ActionMode mode) { - } - - @Override - public void onActionModeFinished(ActionMode mode) { - } private interface DataQuery { final String[] PROJECTION = new String[] { diff --git a/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java b/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java index 83fae2928..c8cfc8dcb 100644 --- a/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java +++ b/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java @@ -29,8 +29,6 @@ import java.lang.ref.WeakReference; * <p> * This pattern can be used to perform background queries without leaking * {@link Context} objects. - * - * @hide pending API council review */ public class NotifyingAsyncQueryHandler extends AsyncQueryHandler { private WeakReference<AsyncQueryListener> mListener; |