Merge "Disable "Create new contact" when setting photo from gallery." into jb-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 33da473..996bc78 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -121,7 +121,7 @@
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:icon="@mipmap/ic_launcher_phone"
- android:screenOrientation="nosensor"
+ android:screenOrientation="portrait"
android:enabled="@*android:bool/config_voice_capable"
android:taskAffinity="android.task.contacts.phone"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
diff --git a/res/layout/people_navigation_item.xml b/res/layout/people_navigation_item.xml
new file mode 100644
index 0000000..b7d86d2
--- /dev/null
+++ b/res/layout/people_navigation_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- This is the text view layout that is shown in the spinner for the navigation drop down menu on
+ tablet devices. The text appearance is governed via two styles:
+ PeopleNavigationDropDownHeaderTextAppearance - text appearance of the item in the header part
+ of navigation drop down list of the action bar.
+ PeopleNavigationDropDownTextAppearance - text appearance of the item in the drop down part of
+ the navigation drop down list of the action bar. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:paddingLeft="18dip"
+ android:paddingRight="18dip"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+/>
diff --git a/res/values-sw580dp-w940dp/styles.xml b/res/values-sw580dp-w940dp/styles.xml
new file mode 100644
index 0000000..07ef912
--- /dev/null
+++ b/res/values-sw580dp-w940dp/styles.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+<resources>
+ <style name="PeopleTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
+ <item name="android:actionBarStyle">@style/ContactsActionBarStyle</item>
+ <item name="android:actionBarWidgetTheme">@style/ContactsActionBarTheme</item>
+ <item name="android:actionBarItemBackground">@drawable/action_bar_item_background</item>
+ <item name="android:actionBarTabStyle">@style/ContactsActionBarTabView</item>
+ <item name="android:actionDropDownStyle">@style/ContactsActionBarDropDownStyle</item>
+ <item name="android:textColorPrimary">@color/primary_text_color</item>
+ <item name="android:textColorSecondary">@color/secondary_text_color</item>
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ <item name="list_item_height">?android:attr/listPreferredItemHeight</item>
+ <item name="activated_background">@drawable/list_item_activated_background</item>
+ <item name="section_header_background">@drawable/list_title_holo</item>
+ <item name="list_item_divider">?android:attr/listDivider</item>
+ <item name="list_item_padding_top">0dip</item>
+ <item name="list_item_padding_right">24dip</item>
+ <item name="list_item_padding_bottom">0dip</item>
+ <item name="list_item_padding_left">0dip</item>
+ <item name="list_item_gap_between_image_and_text">8dip</item>
+ <item name="list_item_gap_between_label_and_data">5dip</item>
+ <item name="list_item_vertical_divider_margin">5dip</item>
+ <item name="list_item_presence_icon_margin">4dip</item>
+ <item name="list_item_photo_size">64dip</item>
+ <item name="list_item_profile_photo_size">80dip</item>
+ <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
+ <item name="list_item_header_text_indent">8dip</item>
+ <item name="list_item_header_text_size">14sp</item>
+ <item name="list_item_header_text_color">@color/people_app_theme_color</item>
+ <item name="list_item_header_height">32dip</item>
+ <item name="list_item_header_underline_height">1dip</item>
+ <item name="list_item_header_underline_color">@color/people_app_theme_color</item>
+ <item name="list_item_data_width_weight">5</item>
+ <item name="list_item_label_width_weight">3</item>
+ <item name="list_item_contacts_count_text_color">@color/contact_count_text_color</item>
+ <item name="list_item_contacts_count_text_size">12sp</item>
+ <item name="contact_browser_list_padding_left">0dip</item>
+ <item name="contact_browser_list_padding_right">0dip</item>
+ <item name="contact_browser_background">@android:color/transparent</item>
+ <item name="list_item_text_indent">@dimen/contact_browser_list_item_text_indent</item>
+ <!-- Favorites -->
+ <item name="favorites_padding_bottom">0dip</item>
+ </style>
+
+</resources>
diff --git a/res/values-sw580dp/styles.xml b/res/values-sw580dp/styles.xml
index c387435..7a247ae 100644
--- a/res/values-sw580dp/styles.xml
+++ b/res/values-sw580dp/styles.xml
@@ -19,6 +19,7 @@
<item name="android:actionBarWidgetTheme">@style/ContactsActionBarTheme</item>
<item name="android:actionBarItemBackground">@drawable/action_bar_item_background</item>
<item name="android:actionBarTabStyle">@style/ContactsActionBarTabView</item>
+ <item name="android:actionDropDownStyle">@style/ContactsActionBarDropDownStyle</item>
<item name="android:textColorPrimary">@color/primary_text_color</item>
<item name="android:textColorSecondary">@color/secondary_text_color</item>
<item name="android:listViewStyle">@style/ListViewStyle</item>
@@ -27,7 +28,7 @@
<item name="section_header_background">@drawable/list_title_holo</item>
<item name="list_item_divider">?android:attr/listDivider</item>
<item name="list_item_padding_top">0dip</item>
- <item name="list_item_padding_right">24dip</item>
+ <item name="list_item_padding_right">12dip</item>
<item name="list_item_padding_bottom">0dip</item>
<item name="list_item_padding_left">0dip</item>
<item name="list_item_gap_between_image_and_text">8dip</item>
diff --git a/res/values-sw680dp/styles.xml b/res/values-sw680dp/styles.xml
index 31de81b..3db3867 100644
--- a/res/values-sw680dp/styles.xml
+++ b/res/values-sw680dp/styles.xml
@@ -19,6 +19,7 @@
<item name="android:actionBarWidgetTheme">@style/ContactsActionBarTheme</item>
<item name="android:actionBarItemBackground">@drawable/action_bar_item_background</item>
<item name="android:actionBarTabStyle">@style/ContactsActionBarTabView</item>
+ <item name="android:actionDropDownStyle">@style/ContactsActionBarDropDownStyle</item>
<item name="android:textColorPrimary">@color/primary_text_color</item>
<item name="android:textColorSecondary">@color/secondary_text_color</item>
<item name="android:listViewStyle">@style/ListViewStyle</item>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1c375e8..93c130f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -363,7 +363,7 @@
This is especially valuable for views without textual representation like ImageView.
[CHAR LIMIT=NONE] -->
- <string name="contactsAllLabel">All</string>
+ <string name="contactsAllLabel">All contacts</string>
<!-- The content description text for the groups tab.
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 3be1222..1702585 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -130,6 +130,7 @@
<item name="android:actionBarItemBackground">@drawable/action_bar_item_background</item>
<item name="android:actionBarWidgetTheme">@style/ContactsActionBarTheme</item>
<item name="android:actionBarTabStyle">@style/ContactsActionBarTabView</item>
+ <item name="android:actionDropDownStyle">@style/ContactsActionBarDropDownStyle</item>
<item name="android:textColorPrimary">@color/primary_text_color</item>
<item name="android:textColorSecondary">@color/secondary_text_color</item>
<item name="android:listViewStyle">@style/ListViewStyle</item>
@@ -174,6 +175,9 @@
<item name="android:displayOptions"></item>
</style>
+ <style name="ContactsActionBarDropDownStyle" parent="@android:style/Widget.Holo.Light.Spinner">
+ </style>
+
<style name="ContactsActionBarTheme" parent="@android:style/Theme.Holo">
<item name="android:textColorHint">#CCCCCC</item>
<item name="android:textColor">@android:color/black</item>
@@ -343,4 +347,18 @@
<item name="android:singleLine">true</item>
<item name="android:textAllCaps">true</item>
</style>
+
+ <style name="PeopleNavigationDropDownTextAppearance">
+ <item name="android:textColor">#333333</item>
+ <item name="android:textSize">18sp</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textAllCaps">false</item>
+ </style>
+
+ <style name="PeopleNavigationDropDownHeaderTextAppearance">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
</resources>
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 5d188fb..ab2ac41 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -226,10 +226,11 @@
mIsUserProfile = isUserProfile;
}
- private Result(Result from) {
+ private Result(Uri requestedUri, Result from) {
+ mRequestedUri = requestedUri;
+
mStatus = from.mStatus;
mException = from.mException;
- mRequestedUri = from.mRequestedUri;
mLookupUri = from.mLookupUri;
mUri = from.mUri;
mDirectoryId = from.mDirectoryId;
@@ -520,6 +521,12 @@
public boolean isUserProfile() {
return mIsUserProfile;
}
+
+ @Override
+ public String toString() {
+ return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
+ ",uri=" + mUri + ",status=" + mStatus + "}";
+ }
}
/**
@@ -730,13 +737,13 @@
UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
// We are using a cached result from earlier. Below, we should make sure
// we are not doing any more network or disc accesses
- result = cachedResult;
+ result = new Result(mRequestedUri, cachedResult);
resultIsCached = true;
} else {
result = loadContactEntity(resolver, uriCurrentFormat);
resultIsCached = false;
}
- if (!result.isNotFound()) {
+ if (result.isLoaded()) {
if (result.isDirectoryEntry()) {
if (!resultIsCached) {
loadDirectoryMetaData(result);
@@ -1345,10 +1352,10 @@
* contact. If the next load is for a different contact, the cached result will be dropped
*/
public void cacheResult() {
- if (mContact == null) {
+ if (mContact == null || !mContact.isLoaded()) {
sCachedResult = null;
} else {
- sCachedResult = new Result(mContact);
+ sCachedResult = mContact;
}
}
}
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index fd06d60..4fc9866 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -26,15 +26,19 @@
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.SharedPreferences;
+import android.graphics.Color;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
+import android.widget.ArrayAdapter;
import android.widget.SearchView;
import android.widget.SearchView.OnCloseListener;
import android.widget.SearchView.OnQueryTextListener;
+import android.widget.TextView;
/**
* Adapter for the action bar at the top of the Contacts activity.
@@ -74,7 +78,9 @@
private Listener mListener;
private final ActionBar mActionBar;
- private final MyTabListener mTabListener = new MyTabListener();
+ private final int mActionBarNavigationMode;
+ private final MyTabListener mTabListener;
+ private final MyNavigationListener mNavigationListener;
private boolean mShowHomeIcon;
private boolean mShowTabsAsText;
@@ -90,6 +96,34 @@
private int mCurrentTab = TabState.DEFAULT;
+ /**
+ * Extension of ArrayAdapter to be used for the action bar navigation drop list. It is not
+ * possible to change the text appearance of a text item that is in the spinner header or
+ * in the drop down list using a selector xml file. The only way to differentiate the two
+ * is if the view is gotten via {@link #getView(int, View, ViewGroup)} or
+ * {@link #getDropDownView(int, View, ViewGroup)}.
+ */
+ private class CustomArrayAdapter extends ArrayAdapter<String> {
+
+ public CustomArrayAdapter(Context context, int textResId) {
+ super(context, textResId);
+ }
+
+ public View getView (int position, View convertView, ViewGroup parent) {
+ TextView textView = (TextView) super.getView(position, convertView, parent);
+ textView.setTextAppearance(mContext,
+ R.style.PeopleNavigationDropDownHeaderTextAppearance);
+ return textView;
+ }
+
+ public View getDropDownView (int position, View convertView, ViewGroup parent) {
+ TextView textView = (TextView) super.getDropDownView(position, convertView, parent);
+ textView.setTextAppearance(mContext,
+ R.style.PeopleNavigationDropDownTextAppearance);
+ return textView;
+ }
+ }
+
public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
boolean isUsingTwoPanes) {
mContext = context;
@@ -101,6 +135,15 @@
// On wide screens, show the tabs as text (instead of icons)
mShowTabsAsText = isUsingTwoPanes;
+ if (isUsingTwoPanes) {
+ mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_LIST;
+ mTabListener = null;
+ mNavigationListener = new MyNavigationListener();
+ } else {
+ mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_TABS;
+ mTabListener = new MyTabListener();
+ mNavigationListener = null;
+ }
// Set up search view.
View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate(
@@ -124,12 +167,67 @@
mSearchView.setQuery(mQueryString, false);
mActionBar.setCustomView(customSearchView, layoutParams);
- // Set up tabs
+ // Set up tabs or navigation list
+ switch(mActionBarNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_TABS:
+ setupTabs();
+ break;
+ case ActionBar.NAVIGATION_MODE_LIST:
+ setupNavigationList();
+ break;
+ }
+ }
+
+ private void setupTabs() {
addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel);
addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel);
addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel);
}
+ private void setupNavigationList() {
+ ArrayAdapter<String> navAdapter = new CustomArrayAdapter(mContext,
+ R.layout.people_navigation_item);
+ navAdapter.add(mContext.getString(R.string.contactsAllLabel));
+ navAdapter.add(mContext.getString(R.string.contactsFavoritesLabel));
+ navAdapter.add(mContext.getString(R.string.contactsGroupsLabel));
+ mActionBar.setListNavigationCallbacks(navAdapter, mNavigationListener);
+ }
+
+ /**
+ * Because the navigation list items are in a different order than tab items, this returns
+ * the appropriate tab from the navigation item position.
+ */
+ private int getTabPositionFromNavigationItemPosition(int navItemPos) {
+ switch(navItemPos) {
+ case 0:
+ return TabState.ALL;
+ case 1:
+ return TabState.FAVORITES;
+ case 2:
+ return TabState.GROUPS;
+ }
+ throw new IllegalArgumentException(
+ "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1)
+ + " inclusive.");
+ }
+
+ /**
+ * This is the inverse of {@link getTabPositionFromNavigationItemPosition}.
+ */
+ private int getNavigationItemPositionFromTabPosition(int tabPos) {
+ switch(tabPos) {
+ case TabState.ALL:
+ return 0;
+ case TabState.FAVORITES:
+ return 1;
+ case TabState.GROUPS:
+ return 2;
+ }
+ throw new IllegalArgumentException(
+ "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1)
+ + " inclusive.");
+ }
+
public void initialize(Bundle savedState, ContactsRequest request) {
if (savedState == null) {
mSearchMode = request.isSearchMode();
@@ -189,6 +287,17 @@
}
}
+ private class MyNavigationListener implements ActionBar.OnNavigationListener {
+ public boolean mIgnoreNavigationItemSelected;
+
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ if (!mIgnoreNavigationItemSelected) {
+ setCurrentTab(getTabPositionFromNavigationItemPosition(itemPosition));
+ }
+ return true;
+ }
+ }
+
/**
* Change the current tab, and notify the listener.
*/
@@ -205,9 +314,20 @@
}
mCurrentTab = tab;
- if ((mActionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_TABS)
- && (mCurrentTab != mActionBar.getSelectedNavigationIndex())) {
- mActionBar.setSelectedNavigationItem(mCurrentTab);
+ final int actionBarSelectedNavIndex = mActionBar.getSelectedNavigationIndex();
+ switch(mActionBar.getNavigationMode()) {
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mCurrentTab != actionBarSelectedNavIndex) {
+ mActionBar.setSelectedNavigationItem(mCurrentTab);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mCurrentTab != getTabPositionFromNavigationItemPosition(
+ actionBarSelectedNavIndex)) {
+ mActionBar.setSelectedNavigationItem(
+ getNavigationItemPositionFromTabPosition(mCurrentTab));
+ }
+ break;
}
if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
@@ -306,7 +426,9 @@
mListener.onAction(Action.START_SEARCH_MODE);
}
} else {
- if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_TABS) {
+ final int currentNavigationMode = mActionBar.getNavigationMode();
+ if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_TABS
+ && currentNavigationMode != ActionBar.NAVIGATION_MODE_TABS) {
// setNavigationMode will trigger onTabSelected() with the tab which was previously
// selected.
// The issue is that when we're first switching to the tab navigation mode after
@@ -319,6 +441,13 @@
mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
mActionBar.setSelectedNavigationItem(mCurrentTab);
mTabListener.mIgnoreTabSelected = false;
+ } else if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_LIST
+ && currentNavigationMode != ActionBar.NAVIGATION_MODE_LIST) {
+ mNavigationListener.mIgnoreNavigationItemSelected = true;
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ mActionBar.setSelectedNavigationItem(
+ getNavigationItemPositionFromTabPosition(mCurrentTab));
+ mNavigationListener.mIgnoreNavigationItemSelected = false;
}
mActionBar.setTitle(null);
// Since we have the {@link SearchView} in a custom action bar, we must manually handle
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index d8fa158..25c60d6 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -191,6 +191,7 @@
@Override
public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
if (!mLookupUri.equals(data.getRequestedUri())) {
+ Log.e(TAG, "Different URI: requested=" + mLookupUri + " actual=" + data);
return;
}
diff --git a/src/com/android/contacts/dialog/ClearFrequentsDialog.java b/src/com/android/contacts/dialog/ClearFrequentsDialog.java
index ecb1a27..4aeaf1c 100644
--- a/src/com/android/contacts/dialog/ClearFrequentsDialog.java
+++ b/src/com/android/contacts/dialog/ClearFrequentsDialog.java
@@ -45,6 +45,9 @@
final OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
+ final IndeterminateProgressDialog progressDialog = IndeterminateProgressDialog.show(
+ getFragmentManager(), getString(R.string.clearFrequentsProgress_title),
+ null, 500);
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -52,8 +55,13 @@
null, null);
return null;
}
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ }
};
- task.execute();
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
};
return new AlertDialog.Builder(getActivity())
diff --git a/src/com/android/contacts/dialog/IndeterminateProgressDialog.java b/src/com/android/contacts/dialog/IndeterminateProgressDialog.java
new file mode 100644
index 0000000..21cd4bb
--- /dev/null
+++ b/src/com/android/contacts/dialog/IndeterminateProgressDialog.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2012 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.dialog;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+
+/**
+ * Indeterminate progress dialog wrapped up in a DialogFragment to work even when the device
+ * orientation is changed. Currently, only supports adding a title and/or message to the progress
+ * dialog. There is an additional parameter of the minimum amount of time to display the progress
+ * dialog even after a call to dismiss the dialog {@link #dismiss()} or
+ * {@link #dismissAllowingStateLoss()}.
+ * <p>
+ * To create and show the progress dialog, use
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)} and retain the reference to the
+ * IndeterminateProgressDialog instance.
+ * <p>
+ * To dismiss the dialog, use {@link #dismiss()} or {@link #dismissAllowingStateLoss()} on the
+ * instance. The instance returned by
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)} is guaranteed to be valid
+ * after a device orientation change because the {@link #setRetainInstance(boolean)} is called
+ * internally with true.
+ */
+public class IndeterminateProgressDialog extends DialogFragment {
+ private static final String TAG = IndeterminateProgressDialog.class.getSimpleName();
+
+ private CharSequence mTitle;
+ private CharSequence mMessage;
+ private long mMinDisplayTime;
+ private long mShowTime = 0;
+ private boolean mActivityReady = false;
+ private Dialog mOldDialog;
+ private final Handler mHandler = new Handler();
+ private boolean mCalledSuperDismiss = false;
+ private boolean mAllowStateLoss;
+ private final Runnable mDismisser = new Runnable() {
+ @Override
+ public void run() {
+ superDismiss();
+ }
+ };
+
+ /**
+ * Creates and shows an indeterminate progress dialog. Once the progress dialog is shown, it
+ * will be shown for at least the minDisplayTime (in milliseconds), so that the progress dialog
+ * does not flash in and out to quickly.
+ */
+ public static IndeterminateProgressDialog show(FragmentManager fragmentManager,
+ CharSequence title, CharSequence message, long minDisplayTime) {
+ IndeterminateProgressDialog dialogFragment = new IndeterminateProgressDialog();
+ dialogFragment.mTitle = title;
+ dialogFragment.mMessage = message;
+ dialogFragment.mMinDisplayTime = minDisplayTime;
+ dialogFragment.show(fragmentManager, TAG);
+ dialogFragment.mShowTime = System.currentTimeMillis();
+ dialogFragment.setCancelable(false);
+
+ return dialogFragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Create the progress dialog and set its properties
+ final ProgressDialog dialog = new ProgressDialog(getActivity());
+ dialog.setIndeterminate(true);
+ dialog.setIndeterminateDrawable(null);
+ dialog.setTitle(mTitle);
+ dialog.setMessage(mMessage);
+
+ return dialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mActivityReady = true;
+
+ // Check if superDismiss() had been called before. This can happen if in a long
+ // running operation, the user hits the home button and closes this fragment's activity.
+ // Upon returning, we want to dismiss this progress dialog fragment.
+ if (mCalledSuperDismiss) {
+ superDismiss();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mActivityReady = false;
+ }
+
+ /**
+ * There is a race condition that is not handled properly by the DialogFragment class.
+ * If we don't check that this onDismiss callback isn't for the old progress dialog from before
+ * the device orientation change, then this will cause the newly created dialog after the
+ * orientation change to be dismissed immediately.
+ */
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mOldDialog != null && mOldDialog == dialog) {
+ // This is the callback from the old progress dialog that was already dismissed before
+ // the device orientation change, so just ignore it.
+ return;
+ }
+ super.onDismiss(dialog);
+ }
+
+ /**
+ * Save the old dialog that is about to get destroyed in case this is due to a change
+ * in device orientation. This will allow us to intercept the callback to
+ * {@link #onDismiss(DialogInterface)} in case the callback happens after a new progress dialog
+ * instance was created.
+ */
+ @Override
+ public void onDestroyView() {
+ mOldDialog = getDialog();
+ super.onDestroyView();
+ }
+
+ /**
+ * This tells the progress dialog to dismiss itself after guaranteeing to be shown for the
+ * specified time in {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+ */
+ @Override
+ public void dismiss() {
+ mAllowStateLoss = false;
+ dismissWhenReady();
+ }
+
+ /**
+ * This tells the progress dialog to dismiss itself (with state loss) after guaranteeing to be
+ * shown for the specified time in
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+ */
+ @Override
+ public void dismissAllowingStateLoss() {
+ mAllowStateLoss = true;
+ dismissWhenReady();
+ }
+
+ /**
+ * Tells the progress dialog to dismiss itself after guaranteeing that the dialog had been
+ * showing for at least the minimum display time as set in
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+ */
+ private void dismissWhenReady() {
+ // Compute how long the dialog has been showing
+ final long shownTime = System.currentTimeMillis() - mShowTime;
+ if (shownTime >= mMinDisplayTime) {
+ // dismiss immediately
+ mHandler.post(mDismisser);
+ } else {
+ // Need to wait some more, so compute the amount of time to sleep.
+ final long sleepTime = mMinDisplayTime - shownTime;
+ mHandler.postDelayed(mDismisser, sleepTime);
+ }
+ }
+
+ /**
+ * Actually dismiss the dialog fragment.
+ */
+ private void superDismiss() {
+ mCalledSuperDismiss = true;
+ if (mActivityReady) {
+ // The fragment is either in onStart or past it, but has not gotten to onStop yet.
+ // It is safe to dismiss this dialog fragment.
+ if (mAllowStateLoss) {
+ super.dismissAllowingStateLoss();
+ } else {
+ super.dismiss();
+ }
+ }
+ // If mActivityReady is false, then this dialog fragment has already passed the onStop
+ // state. This can happen if the user hit the 'home' button before this dialog fragment was
+ // dismissed or if there is a configuration change.
+ // In the event that this dialog fragment is re-attached and reaches onStart (e.g.,
+ // because the user returns to this fragment's activity or the device configuration change
+ // has re-attached this dialog fragment), because the mCalledSuperDismiss flag was set to
+ // true, this dialog fragment will be dismissed within onStart. So, there's nothing else
+ // that needs to be done.
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/FloatingChildLayout.java b/src/com/android/contacts/quickcontact/FloatingChildLayout.java
index 6302c5d..dca738c 100644
--- a/src/com/android/contacts/quickcontact/FloatingChildLayout.java
+++ b/src/com/android/contacts/quickcontact/FloatingChildLayout.java
@@ -23,15 +23,13 @@
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
-import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewPropertyAnimator;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.PopupWindow;
@@ -53,11 +51,30 @@
private static final String TAG = "FloatingChildLayout";
private int mFixedTopPosition;
private View mChild;
- private boolean mIsShowingChild;
private Rect mTargetScreen = new Rect();
private final int mAnimationDuration;
private final TransitionDrawable mBackground;
+ /** The phase of the background dim. This is one of the values of {@link BackgroundPhase} */
+ private int mBackgroundPhase = BackgroundPhase.BEFORE;
+
+ private interface BackgroundPhase {
+ public static final int BEFORE = 0;
+ public static final int APPEARING_OR_VISIBLE = 1;
+ public static final int DISAPPEARING_OR_GONE = 3;
+ }
+
+ /** The phase of the contents window. This is one of the values of {@link ForegroundPhase} */
+ private int mForegroundPhase = ForegroundPhase.BEFORE;
+
+ private interface ForegroundPhase {
+ public static final int BEFORE = 0;
+ public static final int APPEARING = 1;
+ public static final int IDLE = 2;
+ public static final int DISAPPEARING = 3;
+ public static final int AFTER = 4;
+ }
+
// Black, 50% alpha as per the system default.
private static final int DIM_BACKGROUND_COLOR = 0x7F000000;
@@ -83,8 +100,6 @@
mChild.setScaleX(0.5f);
mChild.setScaleY(0.5f);
mChild.setAlpha(0.0f);
-
- mIsShowingChild = false;
}
public View getChild() {
@@ -163,43 +178,51 @@
child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
}
- /** Begin animating {@link #getChild()} visible. */
- public void showChild(final Runnable onAnimationEndRunnable) {
- if (mIsShowingChild) return;
- mIsShowingChild = true;
-
- // TODO: understand this.
- // For some reason this needs wait a tick in order to avoid jank.
- // Maybe because we set up a hardware layer in animateScale()?
- // Probably not, since it should also be required in hideChild().
- new Handler().post(new Runnable() {
- @Override public void run() {
- animateBackground(false);
- }
- });
-
- animateScale(false, onAnimationEndRunnable);
- }
-
- /** Begin animating {@link #getChild()} invisible. */
- public void hideChild(final Runnable onAnimationEndRunnable) {
- if (!mIsShowingChild) return;
- mIsShowingChild = false;
-
- animateBackground(true);
- animateScale(true, onAnimationEndRunnable);
- }
-
- private void animateBackground(boolean isExitAnimation) {
- if (isExitAnimation) {
- mBackground.reverseTransition(mAnimationDuration);
- } else {
+ public void fadeInBackground() {
+ if (mBackgroundPhase == BackgroundPhase.BEFORE) {
+ mBackgroundPhase = BackgroundPhase.APPEARING_OR_VISIBLE;
mBackground.startTransition(mAnimationDuration);
}
}
+ public void fadeOutBackground() {
+ if (mBackgroundPhase == BackgroundPhase.APPEARING_OR_VISIBLE) {
+ mBackgroundPhase = BackgroundPhase.DISAPPEARING_OR_GONE;
+ mBackground.reverseTransition(mAnimationDuration);
+ }
+ }
+
+ public boolean isContentFullyVisible() {
+ return mForegroundPhase == ForegroundPhase.IDLE;
+ }
+
+ /** Begin animating {@link #getChild()} visible. */
+ public void showContent(final Runnable onAnimationEndRunnable) {
+ if (mForegroundPhase == ForegroundPhase.BEFORE) {
+ mForegroundPhase = ForegroundPhase.APPEARING;
+ animateScale(false, onAnimationEndRunnable);
+ }
+ }
+
+ /**
+ * Begin animating {@link #getChild()} invisible. Returns false if animation is not valid in
+ * this state
+ */
+ public boolean hideContent(final Runnable onAnimationEndRunnable) {
+ if (mForegroundPhase == ForegroundPhase.APPEARING ||
+ mForegroundPhase == ForegroundPhase.IDLE) {
+ mForegroundPhase = ForegroundPhase.DISAPPEARING;
+ animateScale(true, onAnimationEndRunnable);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
/** Creates the open/close animation */
- private void animateScale(boolean isExitAnimation, final Runnable onAnimationEndRunnable) {
+ private void animateScale(
+ final boolean isExitAnimation,
+ final Runnable onAnimationEndRunnable) {
mChild.setPivotX(mTargetScreen.centerX() - mChild.getLeft());
mChild.setPivotY(mTargetScreen.centerY() - mChild.getTop());
@@ -208,7 +231,7 @@
: android.R.interpolator.decelerate_quint;
final float scaleTarget = isExitAnimation ? 0.5f : 1.0f;
- ViewPropertyAnimator animator = mChild.animate().withLayer()
+ mChild.animate().withLayer()
.setDuration(mAnimationDuration)
.setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator))
.scaleX(scaleTarget)
@@ -217,7 +240,17 @@
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- if (onAnimationEndRunnable != null) onAnimationEndRunnable.run();
+ if (isExitAnimation) {
+ if (mForegroundPhase == ForegroundPhase.DISAPPEARING) {
+ mForegroundPhase = ForegroundPhase.AFTER;
+ if (onAnimationEndRunnable != null) onAnimationEndRunnable.run();
+ }
+ } else {
+ if (mForegroundPhase == ForegroundPhase.APPEARING) {
+ mForegroundPhase = ForegroundPhase.IDLE;
+ if (onAnimationEndRunnable != null) onAnimationEndRunnable.run();
+ }
+ }
}
});
}
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index b656969..5fe34e0 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -98,9 +98,6 @@
private String[] mExcludeMimes;
private List<String> mSortedActionMimeTypes = Lists.newArrayList();
- private boolean mHasFinishedAnimatingIn = false;
- private boolean mHasStartedAnimatingOut = false;
-
private FloatingChildLayout mFloatingLayout;
private View mPhotoContainer;
@@ -154,6 +151,8 @@
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
+ if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG);
+
// Show QuickContact in front of soft input
getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
@@ -172,7 +171,8 @@
mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
- return handleOutsideTouch();
+ handleOutsideTouch();
+ return true;
}
});
@@ -183,7 +183,7 @@
mContactLoader.cacheResult();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
- hide(false);
+ close(false);
}
};
mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
@@ -191,15 +191,6 @@
mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
mListPager.setOnPageChangeListener(new PageChangeListener());
- show();
- }
-
- private void show() {
-
- if (TRACE_LAUNCH) {
- android.os.Debug.startMethodTracing(TRACE_TAG);
- }
-
final Intent intent = getIntent();
Uri lookupUri = intent.getData();
@@ -226,23 +217,23 @@
mContactLoader = (ContactLoader) getLoaderManager().initLoader(
LOADER_ID, null, mLoaderCallbacks);
+
+ mFloatingLayout.fadeInBackground();
}
- private boolean handleOutsideTouch() {
- if (!mHasFinishedAnimatingIn) return false;
- if (mHasStartedAnimatingOut) return false;
-
- mHasStartedAnimatingOut = true;
- hide(true);
- return true;
+ private void handleOutsideTouch() {
+ if (mFloatingLayout.isContentFullyVisible()) {
+ close(true);
+ }
}
- private void hide(boolean withAnimation) {
+ private void close(boolean withAnimation) {
// cancel any pending queries
getLoaderManager().destroyLoader(LOADER_ID);
if (withAnimation) {
- mFloatingLayout.hideChild(new Runnable() {
+ mFloatingLayout.fadeOutBackground();
+ final boolean animated = mFloatingLayout.hideContent(new Runnable() {
@Override
public void run() {
// Wait until the final animation frame has been drawn, otherwise
@@ -266,15 +257,19 @@
});
}
});
+ if (!animated) {
+ // If we were in the wrong state, simply quit (this can happen for example
+ // if the user pushes BACK before anything has loaded)
+ finish();
+ }
} else {
- mFloatingLayout.hideChild(null);
finish();
}
}
@Override
public void onBackPressed() {
- hide(true);
+ close(true);
}
/** Assign this string to the view if it is not empty. */
@@ -480,7 +475,7 @@
@Override
public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
if (isFinishing()) {
- hide(false);
+ close(false);
return;
}
if (data.isError()) {
@@ -492,25 +487,22 @@
Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
Toast.LENGTH_LONG).show();
- hide(false);
+ close(false);
return;
}
bindData(data);
- if (TRACE_LAUNCH) {
- android.os.Debug.stopMethodTracing();
- }
+ if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
// Data bound and ready, pull curtain to show. Put this on the Handler to ensure
// that the layout passes are completed
SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
@Override
public void run() {
- mFloatingLayout.showChild(new Runnable() {
+ mFloatingLayout.showContent(new Runnable() {
@Override
public void run() {
- mHasFinishedAnimatingIn = true;
mContactLoader.upgradeToFullContact();
}
});
@@ -599,7 +591,7 @@
Toast.LENGTH_SHORT).show();
}
- hide(false);
+ close(false);
}
};
// Defer the action to make the window properly repaint
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java b/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
index 98812d9..39307bf 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
@@ -23,12 +23,19 @@
import android.provider.ContactsContract.Contacts;
public class SocialWidgetConfigureActivity extends Activity {
+ private static final String KEY_LAUNCHED = "already_launched_picker_activity";
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// If the user presses back, we want to cancel
setResult(RESULT_CANCELED);
+ // Don't launch contact-picker if we already launched it (for example, if
+ // we launched it in a previous onCreate() and the device orientation changes
+ // before the picker returns its result, then this activity will be recreated).
+ if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_LAUNCHED)) return;
+
// Forward the Intent to the picker
final Intent pickerIntent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
pickerIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -36,6 +43,14 @@
}
@Override
+ protected void onSaveInstanceState(Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+
+ // We know for sure that we've launched the contact-picker... see onCreate()
+ savedInstanceState.putBoolean(KEY_LAUNCHED, true);
+ }
+
+ @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// We came back from the Picker. If the user actually selected a contact,
// return it now