Fix FastTrack recycling consistency issues, shadows.
When FastTrack is displayed and a configuration change
occurs, we tear down and recreate the host Activity, but in
the process moveTaskToBack(), which means the next trigger
will show a stale track and rectangle.
This change dismisses the track in onPause() to reduce risk
of bringing back a stale track. It also introduces a new
mDismissed flag to catch any race conditions where the track
is dismissed before showInternal() is called.
When requested a show() and already visible, instead of
ignoring, I'm calling dismissInternal() to replace the track
with the updated request. I'm also using query tokens to
ignore query results from stale show() requests.
This change also fixes padding in the large FT format when
no photo is available. It also fixes shadow behind track
to remove sharp vertical edges visible during animations.
Also clears "make default" checkbox between recycles and
fixes an issue where icons for preferred apps remained after
the user selects "always use" from intent disambig list.
Fixes http://b/2163611 and http://b/2164119
diff --git a/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png b/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png
index 6fafcbe..0dcf076 100644
--- a/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png
+++ b/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png b/res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png
index efbb1da..2d20076 100644
--- a/res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png
+++ b/res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png
Binary files differ
diff --git a/res/layout-finger/quickcontact.xml b/res/layout-finger/quickcontact.xml
index a2a94d4..13b5c20 100644
--- a/res/layout-finger/quickcontact.xml
+++ b/res/layout-finger/quickcontact.xml
@@ -18,6 +18,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/quickcontact_shadow_horiz"
+ android:paddingRight="@dimen/quickcontact_shadow_horiz"
android:background="@drawable/quickcontact_drop_shadow">
<FrameLayout
@@ -134,4 +136,13 @@
android:layout_below="@id/footer"
android:src="@drawable/quickcontact_arrow_down" />
+ <ImageView
+ android:id="@+id/arrow_down_stub"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-1dip"
+ android:layout_below="@id/footer_disambig"
+ android:visibility="invisible"
+ android:src="@drawable/quickcontact_arrow_down" />
+
</RelativeLayout>
diff --git a/res/layout-finger/quickcontact_header_large.xml b/res/layout-finger/quickcontact_header_large.xml
index df01f8a..8a11aa2 100644
--- a/res/layout-finger/quickcontact_header_large.xml
+++ b/res/layout-finger/quickcontact_header_large.xml
@@ -29,13 +29,13 @@
android:layout_width="50dip"
android:layout_height="56dip"
android:layout_marginLeft="15dip"
- android:layout_marginRight="15dip"
style="@*android:style/Widget.QuickContactBadge.WindowLarge" />
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:layout_marginLeft="15dip"
android:paddingRight="8dip"
android:orientation="vertical">
diff --git a/res/layout-finger/quickcontact_item.xml b/res/layout-finger/quickcontact_item.xml
index 819915d..8580ac5 100644
--- a/res/layout-finger/quickcontact_item.xml
+++ b/res/layout-finger/quickcontact_item.xml
@@ -17,11 +17,11 @@
<com.android.contacts.ui.widget.CheckableImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="59dip"
- android:layout_height="52dip"
+ android:layout_height="51dip"
android:paddingLeft="12dip"
android:paddingRight="12dip"
android:paddingTop="8dip"
- android:paddingBottom="9dip"
+ android:paddingBottom="8dip"
android:scaleType="centerInside"
android:focusable="true"
android:clickable="true"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 845c3dc..a82f5a9 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -15,9 +15,12 @@
-->
<resources>
- <dimen name="quickcontact_shadow">37dip</dimen>
+ <dimen name="quickcontact_shadow_horiz">30dip</dimen>
+ <dimen name="quickcontact_shadow_vert">37dip</dimen>
+ <dimen name="quickcontact_shadow_touch">20dip</dimen>
+
<dimen name="edit_photo_size">76dip</dimen>
-
+
<!-- The height of the ScrollingTabWidget -->
<dimen name="tab_height">40dip</dimen>
<dimen name="account_name_height">25dip</dimen>
diff --git a/src/com/android/contacts/ui/QuickContactActivity.java b/src/com/android/contacts/ui/QuickContactActivity.java
index 6445664..d17e3be 100644
--- a/src/com/android/contacts/ui/QuickContactActivity.java
+++ b/src/com/android/contacts/ui/QuickContactActivity.java
@@ -41,13 +41,14 @@
@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) {
@@ -82,14 +83,15 @@
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");
-
- mQuickContact.dismiss();
}
/** {@inheritDoc} */
@@ -97,8 +99,6 @@
if (LOGV) Log.d(TAG, "onDismiss");
if (isTaskRoot() && !FORCE_CREATE) {
- if (LOGV) Log.d(TAG, "Moving task to back");
-
// 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
diff --git a/src/com/android/contacts/ui/QuickContactWindow.java b/src/com/android/contacts/ui/QuickContactWindow.java
index 45cb73f..ecb5f9d 100644
--- a/src/com/android/contacts/ui/QuickContactWindow.java
+++ b/src/com/android/contacts/ui/QuickContactWindow.java
@@ -26,15 +26,16 @@
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.android.collect.Sets;
+import android.app.Activity;
import android.content.ActivityNotFoundException;
-import android.content.ContentValues;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.content.EntityIterator;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -45,7 +46,6 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Handler;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.QuickContact;
@@ -55,14 +55,9 @@
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.SocialContract.Activities;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
-import android.util.Pool;
-import android.util.Poolable;
-import android.util.PoolableManager;
-import android.util.Pools;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -70,13 +65,12 @@
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
-import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.Window;
import android.view.WindowManager;
-import android.view.Window.Callback;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
@@ -93,7 +87,6 @@
import android.widget.Toast;
import java.lang.ref.SoftReference;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -106,7 +99,8 @@
*/
public class QuickContactWindow implements Window.Callback,
NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
- AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
+ AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
+ OnGlobalLayoutListener {
private static final String TAG = "QuickContactWindow";
/**
@@ -124,6 +118,7 @@
private View mDecor;
private final Rect mRect = new Rect();
+ private boolean mDismissed = false;
private boolean mQuerying = false;
private boolean mShowing = false;
@@ -134,7 +129,13 @@
private Uri mLookupUri;
private Rect mAnchor;
- private int mShadowHeight;
+ private int mShadowHoriz;
+ private int mShadowVert;
+ private int mShadowTouch;
+
+ private int mScreenWidth;
+ private int mScreenHeight;
+ private int mRequestedY;
private boolean mHasValidSocial = false;
private boolean mHasData = false;
@@ -226,7 +227,12 @@
mResolveCache = new ResolveCache(mContext);
final Resources res = mContext.getResources();
- mShadowHeight = res.getDimensionPixelSize(R.dimen.quickcontact_shadow);
+ mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz);
+ mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert);
+ mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch);
+
+ mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
+ mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
@@ -290,10 +296,10 @@
* Start showing a dialog for the given {@link Contacts#_ID} pointing
* towards the given location.
*/
- public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
- if (mShowing || mQuerying) {
- Log.w(TAG, "already in process of showing");
- return;
+ public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
+ if (mQuerying || mShowing) {
+ Log.w(TAG, "dismissing before showing");
+ dismissInternal();
}
if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
@@ -316,7 +322,10 @@
setHeaderImage(R.id.presence, null);
setHeaderImage(R.id.source, null);
+ resetTrack();
+
mHasValidSocial = false;
+ mDismissed = false;
mQuerying = true;
// Start background query for data, but only select photo rows when they
@@ -327,12 +336,12 @@
// Only request photo data when required by mode
if (mMode == QuickContact.MODE_LARGE) {
// Select photos, but only super-primary
- mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
+ ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
} else {
// Exclude all photos from cursor
- mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
}
}
@@ -380,41 +389,43 @@
*/
private void showInternal() {
mDecor = mWindow.getDecorView();
+ mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
WindowManager.LayoutParams l = mWindow.getAttributes();
- l.width = WindowManager.LayoutParams.FILL_PARENT;
+ l.width = mScreenWidth + mShadowHoriz + mShadowHoriz;
l.height = WindowManager.LayoutParams.WRAP_CONTENT;
// Force layout measuring pass so we have baseline numbers
mDecor.measure(l.width, l.height);
-
final int blockHeight = mDecor.getMeasuredHeight();
l.gravity = Gravity.TOP | Gravity.LEFT;
- l.x = 0;
+ l.x = -mShadowHoriz;
if (mAnchor.top > blockHeight) {
// Show downwards callout when enough room, aligning bottom block
// edge with top of anchor area, and adjusting to inset arrow.
showArrow(R.id.arrow_down, mAnchor.centerX());
- l.y = mAnchor.top - blockHeight + mShadowHeight;
+ l.y = mAnchor.top - blockHeight + mShadowVert;
l.windowAnimations = R.style.QuickContactAboveAnimation;
} else {
// Otherwise show upwards callout, aligning block top with bottom of
// anchor area, and adjusting to inset arrow.
showArrow(R.id.arrow_up, mAnchor.centerX());
- l.y = mAnchor.bottom - mShadowHeight;
+ l.y = mAnchor.bottom - mShadowVert;
l.windowAnimations = R.style.QuickContactBelowAnimation;
}
l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+ mRequestedY = l.y;
mWindowManager.addView(mDecor, l);
mShowing = true;
mQuerying = false;
+ mDismissed = false;
mTrack.startAnimation(mTrackAnim);
@@ -425,35 +436,75 @@
}
}
+ /** {@inheritDoc} */
+ 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 > mScreenHeight) {
+ // Shift up from bottom when overflowing
+ l.y = mScreenHeight - blockHeight;
+ }
+
+ if (originalY != l.y) {
+ // Only update when value is changed
+ mWindow.setAttributes(l);
+ }
+ }
+
/**
* Dismiss this dialog if showing.
*/
- public void dismiss() {
+ public synchronized void dismiss() {
// Notify any listeners that we've been dismissed
if (mDismissListener != null) {
mDismissListener.onDismiss(this);
}
- if (!isShowing()) {
- if (LOGD) Log.d(TAG, "not visible, ignore");
- return;
- }
+ 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();
}
-
- // Release reference to last chiclet.
- mLastAction = null;
-
- // Completely hide header from current mode
- mHeader.setVisibility(View.GONE);
+ 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();
+ }
+
+ /**
+ * Reset track to initial state, recycling any chiclets.
+ */
+ private void resetTrack() {
+ // Release reference to last chiclet
+ mLastAction = null;
// Clear track actions and scroll to hard left
mResolveCache.clear();
@@ -468,32 +519,19 @@
mTrackScroll.fullScroll(View.FOCUS_LEFT);
mWasDownArrow = false;
+ // Clear any primary requests
+ mMakePrimary = false;
+ mSetPrimaryCheckBox.setChecked(false);
+
setResolveVisible(false, null);
-
- mQuerying = false;
-
- if (!hadDecor || !mShowing) {
- if (LOGD) Log.d(TAG, "not showing, ignore");
- return;
- }
-
- mShowing = false;
- mWindowRecycled++;
- }
-
- /**
- * Returns true if this dialog is showing or querying.
- */
- public boolean isShowing() {
- return mShowing || mQuerying;
}
/**
* Consider showing this window, which will only call through to
* {@link #showInternal()} when all data items are present.
*/
- private synchronized void considerShowing() {
- if (mHasData && !mShowing) {
+ private void considerShowing() {
+ if (mHasData && !mShowing && !mDismissed) {
if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
// Missing valid social, swap medium for small header
mHeader.setVisibility(View.GONE);
@@ -506,7 +544,10 @@
}
/** {@inheritDoc} */
- public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ 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);
@@ -741,7 +782,7 @@
}
// Always launch as new task, since we're like a launcher
- mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
/** {@inheritDoc} */
@@ -826,7 +867,7 @@
/** {@inheritDoc} */
public Intent getIntent() {
final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return intent;
}
@@ -887,7 +928,7 @@
if (size == 1) {
bestResolve = matches.get(0);
} else if (size > 1) {
- bestResolve = getBestResolve(matches);
+ bestResolve = getBestResolve(intent, matches);
}
if (bestResolve != null) {
@@ -911,7 +952,18 @@
* displaying in the track, and does not shortcut the system
* {@link Intent} disambiguation dialog.
*/
- protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
+ protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
+ // Try finding preferred activity, otherwise detect disambig
+ final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ final boolean foundDisambig = (foundResolve.match &
+ IntentFilter.MATCH_CATEGORY_MASK) == 0;
+
+ if (!foundDisambig) {
+ // Found concrete match, so return directly
+ return foundResolve;
+ }
+
// Accept any package from prefer list, otherwise first system app
ResolveInfo firstSystem = null;
for (ResolveInfo info : matches) {
@@ -920,6 +972,8 @@
final boolean isPrefer = QuickContactWindow.sPreferResolve
.contains(info.activityInfo.applicationInfo.packageName);
+
+
if (isPrefer) return info;
if (isSystem && firstSystem != null) firstSystem = info;
}
@@ -1245,6 +1299,7 @@
if (tag instanceof Action) {
// Incoming tag is concrete intent, so try launching
final Action action = (Action)tag;
+ final boolean makePrimary = mMakePrimary;
try {
mContext.startActivity(action.getIntent());
@@ -1257,7 +1312,7 @@
setResolveVisible(false, actionView);
this.dismiss();
- if (mMakePrimary) {
+ if (makePrimary) {
ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 1);
final Uri dataUri = action.getDataUri();
@@ -1308,7 +1363,8 @@
});
// Make sure we resize to make room for ListView
- onWindowAttributesChanged(mWindow.getAttributes());
+ mDecor.forceLayout();
+ mDecor.invalidate();
}
}
@@ -1322,6 +1378,8 @@
// it will close the entire dialog.
if (mFooterDisambig.getVisibility() == View.VISIBLE) {
setResolveVisible(false, null);
+ mDecor.forceLayout();
+ mDecor.invalidate();
} else {
dismiss();
}
@@ -1381,8 +1439,8 @@
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Only try detecting outside events on down-press
mDecor.getHitRect(mRect);
- mRect.top = mRect.top + mDecor.getPaddingTop();
- mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
+ mRect.top = mRect.top + mShadowTouch;
+ mRect.bottom = mRect.bottom - mShadowTouch;
final int x = (int)event.getX();
final int y = (int)event.getY();
if (!mRect.contains(x, y)) {