OSDN Git Service

Adding support for display and sort order preferences.
authorDmitri Plotnikov <dplotnikov@google.com>
Sat, 30 Jan 2010 01:44:20 +0000 (17:44 -0800)
committerDmitri Plotnikov <dplotnikov@google.com>
Sat, 30 Jan 2010 01:44:20 +0000 (17:44 -0800)
Bug: 2267198
Change-Id: I8153287896b03d798de163ea231b6ae2360cd6dc

AndroidManifest.xml
res/layout-finger/contact_options.xml
res/layout-finger/display_options_phones_only.xml [moved from res/layout-finger/display_header.xml with 98% similarity]
res/layout-finger/preference_with_more_button.xml [moved from res/layout-finger/edit_contact_entry_ringtone.xml with 98% similarity]
res/values/strings.xml
src/com/android/contacts/ContactsListActivity.java
src/com/android/contacts/TextHighlightingAnimation.java [new file with mode: 0644]
src/com/android/contacts/ui/ContactsPreferences.java [new file with mode: 0644]
src/com/android/contacts/ui/DisplayGroupsActivity.java

index a4ec373..0c59b5d 100644 (file)
@@ -28,6 +28,7 @@
     <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.USE_CREDENTIALS" />
     <uses-permission android:name="android.permission.VIBRATE" />
 
index 6b29e65..5bd8836 100644 (file)
@@ -20,7 +20,7 @@
     android:orientation="vertical"
 >
 
-    <include layout="@layout/edit_contact_entry_ringtone" android:id="@+id/ringtone" />
+    <include layout="@layout/preference_with_more_button" android:id="@+id/ringtone" />
     <View
         android:layout_width="match_parent"
         android:layout_height="1dip"
similarity index 98%
rename from res/layout-finger/display_header.xml
rename to res/layout-finger/display_options_phones_only.xml
index 210eb1b..35965da 100644 (file)
@@ -25,7 +25,7 @@
     <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginLeft="20dip"
+        android:layout_marginLeft="14dip"
         android:layout_marginRight="6dip"
         android:layout_marginTop="6dip"
         android:layout_marginBottom="6dip"
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/entry_ringtone"
+    android:id="@+id/preference"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:paddingRight="?android:attr/scrollbarSize"
index 120d996..9755c0e 100644 (file)
 
     <!-- Text describing that a contact has no information available other than name and photo -->
     <string name="no_contact_details">No additional information for this contact</string>
+    
+    <!-- Label of the "sort list by" display option -->
+    <string name="display_options_sort_list_by">Sort list by</string>
+    
+    <!-- An allowable value for the "sort list by" contact display option  -->
+    <string name="display_options_sort_by_given_name">Given name</string>
+    
+    <!-- An allowable value for the "sort list by" contact display option  -->
+    <string name="display_options_sort_by_family_name">Family name</string>
+           
+    <!-- Label of the "view names as" display option -->
+    <string name="display_options_view_names_as">View contact names as</string>
+    
+    <!-- An allowable value for the "view names as" contact display option  -->
+    <string name="display_options_view_given_name_first">Given name first</string>
+    
+    <!-- An allowable value for the "view names as" contact display option  -->
+    <string name="display_options_view_family_name_first">Family name first</string>
 </resources>
index cba3015..756ab97 100644 (file)
 
 package com.android.contacts;
 
+import com.android.contacts.TextHighlightingAnimation.TextWithHighlighting;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
+import com.android.contacts.ui.ContactsPreferences;
 import com.android.contacts.ui.DisplayGroupsActivity;
 import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
 import com.android.contacts.util.AccountSelectionUtil;
@@ -77,7 +79,6 @@ import android.provider.ContactsContract.Intents.Insert;
 import android.provider.ContactsContract.Intents.UI;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.ContextThemeWrapper;
@@ -103,7 +104,6 @@ import android.widget.ResourceCursorAdapter;
 import android.widget.SectionIndexer;
 import android.widget.TextView;
 import android.widget.AbsListView.OnScrollListener;
-import android.*;
 
 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
@@ -152,6 +152,8 @@ public class ContactsListActivity extends ListActivity implements
     private static final int SUBACTIVITY_VIEW_CONTACT = 2;
     private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
 
+    private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
+
     /**
      * The action for the join contact activity.
      * <p>
@@ -251,42 +253,48 @@ public class ContactsListActivity extends ListActivity implements
     static final int MAX_SUGGESTIONS = 4;
 
     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
-        Contacts._ID, // 0
-        Contacts.DISPLAY_NAME, // 1
-        Contacts.STARRED, //2
-        Contacts.TIMES_CONTACTED, //3
-        Contacts.CONTACT_PRESENCE, //4
-        Contacts.PHOTO_ID, //5
-        Contacts.LOOKUP_KEY, //6
-        Contacts.HAS_PHONE_NUMBER, //7
-        Contacts.SORT_KEY_PRIMARY, //8
+        Contacts._ID,                       // 0
+        Contacts.DISPLAY_NAME_PRIMARY,      // 1
+        Contacts.DISPLAY_NAME_ALTERNATIVE,  // 2
+        Contacts.SORT_KEY_PRIMARY,          // 3
+        Contacts.STARRED,                   // 4
+        Contacts.TIMES_CONTACTED,           // 5
+        Contacts.CONTACT_PRESENCE,          // 6
+        Contacts.PHOTO_ID,                  // 7
+        Contacts.LOOKUP_KEY,                // 8
+        Contacts.HAS_PHONE_NUMBER,          // 9
     };
     static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
-        Contacts._ID, // 0
-        Contacts.DISPLAY_NAME, // 1
-        Contacts.STARRED, //2
-        Contacts.TIMES_CONTACTED, //3
-        Contacts.CONTACT_PRESENCE, //4
-        Contacts.PHOTO_ID, //5
-        Contacts.LOOKUP_KEY, //6
+        Contacts._ID,                       // 0
+        Contacts.DISPLAY_NAME_PRIMARY,      // 1
+        Contacts.DISPLAY_NAME_ALTERNATIVE,  // 2
+        Contacts.SORT_KEY_PRIMARY,          // 3
+        Contacts.STARRED,                   // 4
+        Contacts.TIMES_CONTACTED,           // 5
+        Contacts.CONTACT_PRESENCE,          // 6
+        Contacts.PHOTO_ID,                  // 7
+        Contacts.LOOKUP_KEY,                // 8
         // email lookup doesn't included HAS_PHONE_NUMBER OR LOOKUP_KEY in projection
     };
     static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
-        People._ID, // 0
-        People.DISPLAY_NAME, // 1
-        People.STARRED, //2
-        PeopleColumns.TIMES_CONTACTED, //3
-        People.PRESENCE_STATUS, //4
+        People._ID,                         // 0
+        People.DISPLAY_NAME,                // 1
+        People.DISPLAY_NAME,                // 2
+        People.DISPLAY_NAME,                // 3
+        People.STARRED,                     // 4
+        PeopleColumns.TIMES_CONTACTED,      // 5
+        People.PRESENCE_STATUS,             // 6
     };
     static final int SUMMARY_ID_COLUMN_INDEX = 0;
-    static final int SUMMARY_NAME_COLUMN_INDEX = 1;
-    static final int SUMMARY_STARRED_COLUMN_INDEX = 2;
-    static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 3;
-    static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 4;
-    static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5;
-    static final int SUMMARY_LOOKUP_KEY = 6;
-    static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 7;
-    static final int SUMMARY_SORT_KEY_PRIMARY = 8;
+    static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
+    static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
+    static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
+    static final int SUMMARY_STARRED_COLUMN_INDEX = 4;
+    static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5;
+    static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
+    static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7;
+    static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8;
+    static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 9;
 
     static final String[] PHONES_PROJECTION = new String[] {
         Phone._ID, //0
@@ -416,8 +424,51 @@ public class ContactsListActivity extends ListActivity implements
         }
     }
 
+    /**
+     * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
+     * list item.
+     */
+    private static class NameHighlightingAnimation extends TextHighlightingAnimation {
+        private final ListView mListView;
+
+        private NameHighlightingAnimation(ListView listView, int duration) {
+            super(duration);
+            this.mListView = listView;
+        }
+
+        /**
+         * Redraws all visible items of the list corresponding to contacts
+         */
+        @Override
+        protected void invalidate() {
+            int childCount = mListView.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View listItem = mListView.getChildAt(i);
+                Object tag = listItem.getTag();
+                if (tag instanceof ContactListItemCache) {
+                    ((ContactListItemCache)tag).nameView.invalidate();
+                }
+            }
+        }
+
+        @Override
+        protected void onAnimationStarted() {
+            mListView.setScrollingCacheEnabled(false);
+        }
+
+        @Override
+        protected void onAnimationEnded() {
+            mListView.setScrollingCacheEnabled(true);
+        }
+    }
+
     // The size of a home screen shortcut icon.
     private int mIconSize;
+    private ContactsPreferences mContactsPrefs;
+    private int mDisplayOrder;
+    private int mSortOrder;
+    private boolean mHighlightWhenScrolling;
+    private TextHighlightingAnimation mHighlightingAnimation;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -427,6 +478,7 @@ public class ContactsListActivity extends ListActivity implements
         final Intent intent = getIntent();
 
         mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+        mContactsPrefs = new ContactsPreferences(this);
 
         // Allow the title to be set to a custom String using an extra on the intent
         String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
@@ -611,6 +663,8 @@ public class ContactsListActivity extends ListActivity implements
 
         // Setup the UI
         final ListView list = getListView();
+        mHighlightingAnimation =
+                new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
 
         // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
         // them when an A-Z headers is visible.
@@ -684,8 +738,14 @@ public class ContactsListActivity extends ListActivity implements
         return contactName;
     }
 
-    private int[] mLocation = new int[2];
-    private Rect mRect = new Rect();
+
+    private int getSummaryDisplayNameColumnIndex() {
+        if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+        } else {
+            return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+        }
+    }
 
     /** {@inheritDoc} */
     public void onClick(View v) {
@@ -1090,7 +1150,7 @@ public class ContactsListActivity extends ListActivity implements
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
         // Setup the menu header
-        menu.setHeaderTitle(cursor.getString(SUMMARY_NAME_COLUMN_INDEX));
+        menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex()));
 
         // View contact details
         menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
@@ -1278,7 +1338,7 @@ public class ContactsListActivity extends ListActivity implements
                     || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
                 if (mShortcutAction != null) {
                     Cursor c = (Cursor) mAdapter.getItem(position);
-                    returnPickerResult(c, c.getString(SUMMARY_NAME_COLUMN_INDEX), uri, id);
+                    returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri, id);
                 } else {
                     returnPickerResult(null, null, uri, id);
                 }
@@ -1584,7 +1644,7 @@ public class ContactsListActivity extends ListActivity implements
             default: {
                 // Build and return soft, lookup reference
                 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
+                final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
                 return Contacts.getLookupUri(contactId, lookupKey);
             }
         }
@@ -1737,15 +1797,12 @@ public class ContactsListActivity extends ListActivity implements
         return builder.build();
     }
 
-    private static String getSortOrder(String[] projectionType) {
-        /* if (Locale.getDefault().equals(Locale.JAPAN) &&
-                projectionType == AGGREGATES_PRIMARY_PHONE_PROJECTION) {
-            return SORT_STRING + " ASC";
+    private String getSortOrder(String[] projectionType) {
+        if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            return Contacts.SORT_KEY_PRIMARY;
         } else {
-            return NAME_COLUMN + " COLLATE LOCALIZED ASC";
-        } */
-
-        return Contacts.SORT_KEY_PRIMARY;
+            return Contacts.SORT_KEY_ALTERNATIVE;
+        }
     }
 
     void startQuery() {
@@ -1755,6 +1812,20 @@ public class ContactsListActivity extends ListActivity implements
         mQueryHandler.cancelOperation(QUERY_TOKEN);
         mQueryHandler.setLoadingJoinSuggestions(false);
 
+        mSortOrder = mContactsPrefs.getSortOrder();
+        mDisplayOrder = mContactsPrefs.getDisplayOrder();
+
+        // When sort order and display order contradict each other, we want to
+        // highlight the part of the name used for sorting.
+        mHighlightWhenScrolling = false;
+        if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
+                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
+            mHighlightWhenScrolling = true;
+        } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
+                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            mHighlightWhenScrolling = true;
+        }
+
         String[] projection = getProjectionForQuery();
         String callingPackage = getCallingPackage();
         Uri uri = getUriToQuery();
@@ -2074,7 +2145,7 @@ public class ContactsListActivity extends ListActivity implements
                                 CONTACTS_SUMMARY_PROJECTION,
                                 Contacts._ID + " != " + activity.mQueryAggregateId
                                         + " AND " + CLAUSE_ONLY_VISIBLE, null,
-                                getSortOrder(CONTACTS_SUMMARY_PROJECTION));
+                                activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
                         return;
                     }
 
@@ -2109,12 +2180,13 @@ public class ContactsListActivity extends ListActivity implements
         public ImageView callButton;
         public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
         public TextView labelView;
-        public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
         public TextView dataView;
         public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
         public ImageView presenceView;
         public QuickContactBadge photoView;
         public ImageView nonQuickContactPhotoView;
+        public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
+        public TextWithHighlighting textWithHighlighting;
     }
 
     final static class PhotoInfo {
@@ -2146,6 +2218,7 @@ public class ContactsListActivity extends ListActivity implements
         private int mSuggestionsCursorCount;
         private ImageFetchHandler mHandler;
         private ImageDbFetcher mImageFetcher;
+
         private static final int FETCH_IMAGE_MSG = 1;
 
         public ContactItemListAdapter(Context context) {
@@ -2304,9 +2377,10 @@ public class ContactsListActivity extends ListActivity implements
 
         private SectionIndexer getNewIndexer(Cursor cursor) {
             if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
-                return new JapaneseContactListIndexer(cursor, SUMMARY_SORT_KEY_PRIMARY);
+                return new JapaneseContactListIndexer(cursor,
+                        SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX);
             } else {
-                return new AlphabetIndexer(cursor, SUMMARY_NAME_COLUMN_INDEX, mAlphabet);
+                return new AlphabetIndexer(cursor, getSummaryDisplayNameColumnIndex(), mAlphabet);
             }
         }
 
@@ -2493,8 +2567,8 @@ public class ContactsListActivity extends ListActivity implements
                 cache.photoView.setExcludeMimes(new String[] {Contacts.CONTENT_ITEM_TYPE});
             }
             cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
+            cache.textWithHighlighting = mHighlightingAnimation.createTextWithHighlighting();
             view.setTag(cache);
-
             return view;
         }
 
@@ -2510,6 +2584,7 @@ public class ContactsListActivity extends ListActivity implements
             int defaultType;
             int nameColumnIndex;
             boolean displayAdditionalData = mDisplayAdditionalData;
+            boolean highlightingEnabled = false;
             switch(mMode) {
                 case MODE_PICK_PHONE:
                 case MODE_LEGACY_PICK_PHONE: {
@@ -2530,12 +2605,13 @@ public class ContactsListActivity extends ListActivity implements
                     break;
                 }
                 default: {
-                    nameColumnIndex = SUMMARY_NAME_COLUMN_INDEX;
+                    nameColumnIndex = getSummaryDisplayNameColumnIndex();
                     dataColumnIndex = -1;
                     typeColumnIndex = -1;
                     labelColumnIndex = -1;
                     defaultType = Phone.TYPE_HOME;
                     displayAdditionalData = false;
+                    highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT;
                 }
             }
 
@@ -2543,7 +2619,12 @@ public class ContactsListActivity extends ListActivity implements
             cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
             int size = cache.nameBuffer.sizeCopied;
             if (size != 0) {
-                cache.nameView.setText(cache.nameBuffer.data, 0, size);
+                if (highlightingEnabled) {
+                    buildDisplayNameWithHighlighting(cache.nameView, cursor, cache.nameBuffer,
+                            cache.highlightedTextBuffer, cache.textWithHighlighting);
+                } else {
+                    cache.nameView.setText(cache.nameBuffer.data, 0, size);
+                }
             } else {
                 cache.nameView.setText(mUnknownNameText);
             }
@@ -2574,7 +2655,7 @@ public class ContactsListActivity extends ListActivity implements
                     viewToUse = cache.photoView;
                     // Build soft lookup reference
                     final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                    final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
+                    final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
                     cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
                     cache.photoView.setVisibility(View.VISIBLE);
                     cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
@@ -2674,6 +2755,25 @@ public class ContactsListActivity extends ListActivity implements
             }
         }
 
+        /**
+         * Computes the span of the display name that has highlighted parts and configures
+         * the display name text view accordingly.
+         */
+        private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor,
+                CharArrayBuffer buffer1, CharArrayBuffer buffer2,
+                TextWithHighlighting textWithHighlighting) {
+            int oppositeDisplayOrderColumnIndex;
+            if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+                oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+            } else {
+                oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+            }
+            cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2);
+
+            textWithHighlighting.setText(buffer1, buffer2);
+            textView.setText(textWithHighlighting);
+        }
+
         private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
             if (!displaySectionHeaders) {
@@ -2937,6 +3037,14 @@ public class ContactsListActivity extends ListActivity implements
         }
 
         public void onScrollStateChanged(AbsListView view, int scrollState) {
+            if (mHighlightWhenScrolling) {
+                if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+                    mHighlightingAnimation.startHighlighting();
+                } else {
+                    mHighlightingAnimation.stopHighlighting();
+                }
+            }
+
             mScrollState = scrollState;
             if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
                 // If we are in a fling, stop loading images.
diff --git a/src/com/android/contacts/TextHighlightingAnimation.java b/src/com/android/contacts/TextHighlightingAnimation.java
new file mode 100644 (file)
index 0000000..3bf6499
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * 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;
+
+import com.android.internal.R;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Color;
+import android.os.Handler;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * An animation that alternately dims and brightens the non-highlighted portion of text.
+ */
+public abstract class TextHighlightingAnimation implements Runnable {
+
+    private static final int MAX_ALPHA = 255;
+    private static final int MIN_ALPHA = 50;
+
+    private AccelerateInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+    private DecelerateInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+    private final static DimmingSpan[] sEmptySpans = new DimmingSpan[0];
+
+    private DimmingSpan mDimmingSpan;
+    private Handler mHandler;
+    private boolean mAnimating;
+    private boolean mDimming;
+    private long mTargetTime;
+    private final int mDuration;
+
+    /**
+     * A Spanned that highlights a part of text by dimming another part of that text.
+     */
+    public class TextWithHighlighting implements Spanned {
+
+        private final DimmingSpan[] mSpans;
+        private boolean mDimmingEnabled;
+        private CharArrayBuffer mText;
+        private int mDimmingSpanStart;
+        private int mDimmingSpanEnd;
+        private String mString;
+
+        public TextWithHighlighting() {
+            mSpans = new DimmingSpan[] { mDimmingSpan };
+        }
+
+        public void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText) {
+            mText = baseText;
+
+            // TODO figure out a way to avoid string allocation
+            mString = new String(mText.data, 0, mText.sizeCopied);
+
+            int index = indexOf(baseText, highlightedText);
+
+            if (index == 0 || index == -1) {
+                mDimmingEnabled = false;
+            } else {
+                mDimmingEnabled = true;
+                mDimmingSpanStart = 0;
+                mDimmingSpanEnd = index;
+            }
+        }
+
+        /**
+         * An implementation of indexOf on CharArrayBuffers that finds the first match of
+         * the start of buffer2 in buffer1.  For example, indexOf("abcd", "cdef") == 2
+         */
+        private int indexOf(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+            char[] string1 = buffer1.data;
+            char[] string2 = buffer2.data;
+            int count1 = buffer1.sizeCopied;
+            int count2 = buffer2.sizeCopied;
+            int size = count2;
+            for (int i = 0; i < count1; i++) {
+                if (i + size > count1) {
+                    size = count1 - i;
+                }
+                int j;
+                for (j = 0; j < size; j++) {
+                    if (string1[i+j] != string2[j]) {
+                        break;
+                    }
+                }
+                if (j == size) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+
+        @SuppressWarnings("unchecked")
+        public <T> T[] getSpans(int start, int end, Class<T> type) {
+            if (mDimmingEnabled) {
+                return (T[])mSpans;
+            } else {
+                return (T[])sEmptySpans;
+            }
+        }
+
+        public int getSpanStart(Object tag) {
+            // We only have one span - no need to check the tag parameter
+            return mDimmingSpanStart;
+        }
+
+        public int getSpanEnd(Object tag) {
+            // We only have one span - no need to check the tag parameter
+            return mDimmingSpanEnd;
+        }
+
+        public int getSpanFlags(Object tag) {
+            // String is immutable - flags not needed
+            return 0;
+        }
+
+        public int nextSpanTransition(int start, int limit, Class type) {
+            // Never called since we only have one span
+            return 0;
+        }
+
+        public char charAt(int index) {
+            return mText.data[index];
+        }
+
+        public int length() {
+            return mText.sizeCopied;
+        }
+
+        public CharSequence subSequence(int start, int end) {
+            // Never called - implementing for completeness
+            return new String(mText.data, start, end);
+        }
+
+        @Override
+        public String toString() {
+            return mString;
+        }
+    }
+
+    /**
+     * A Span that modifies alpha of the default foreground color.
+     */
+    private static class DimmingSpan extends CharacterStyle {
+        private int mAlpha;
+
+        public void setAlpha(int alpha) {
+            mAlpha = alpha;
+        }
+
+        @Override
+        public void updateDrawState(TextPaint ds) {
+
+            // Only dim the text in the basic state; not selected, focused or pressed
+            int[] states = ds.drawableState;
+            if (states != null) {
+                int count = states.length;
+                for (int i = 0; i < count; i++) {
+                    switch (states[i]) {
+                        case R.attr.state_pressed:
+                        case R.attr.state_selected:
+                        case R.attr.state_focused:
+                            // We can simply return, because the supplied text
+                            // paint is already configured with defaults.
+                            return;
+                    }
+                }
+            }
+
+            int color = ds.getColor();
+            color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
+            ds.setColor(color);
+        }
+    }
+
+    /**
+     * Constructor.
+     */
+    public TextHighlightingAnimation(int duration) {
+        mDuration = duration;
+        mHandler = new Handler();
+        mDimmingSpan = new DimmingSpan();
+        mDimmingSpan.setAlpha(MAX_ALPHA);
+    }
+
+    /**
+     * Returns a Spanned that can be used by a text view to show text with highlighting.
+     */
+    public TextWithHighlighting createTextWithHighlighting() {
+        return new TextWithHighlighting();
+    }
+
+    /**
+     * Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
+     */
+    protected abstract void invalidate();
+
+    /**
+     * Starts the highlighting animation, which will dim portions of text.
+     */
+    public void startHighlighting() {
+        startAnimation(true);
+    }
+
+    /**
+     * Starts un-highlighting animation, which will brighten the dimmed portions of text
+     * to the brightness level of the rest of text.
+     */
+    public void stopHighlighting() {
+        startAnimation(false);
+    }
+
+    /**
+     * Called when the animation starts.
+     */
+    protected void onAnimationStarted() {
+    }
+
+    /**
+     * Called when the animation has stopped.
+     */
+    protected void onAnimationEnded() {
+    }
+
+    private void startAnimation(boolean dim) {
+        if (mDimming != dim) {
+            mDimming = dim;
+            long now = System.currentTimeMillis();
+            if (!mAnimating) {
+                mAnimating = true;
+                mTargetTime = now + mDuration;
+                onAnimationStarted();
+                mHandler.post(this);
+            } else  {
+
+                // If we have started dimming, reverse the direction and adjust the target
+                // time accordingly.
+                mTargetTime = (now + mDuration) - (mTargetTime - now);
+            }
+        }
+    }
+
+    /**
+     * Animation step.
+     */
+    public void run() {
+        long now = System.currentTimeMillis();
+        long timeLeft = mTargetTime - now;
+        if (timeLeft < 0) {
+            mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
+            mAnimating = false;
+            onAnimationEnded();
+            return;
+        }
+
+        // Start=1, end=0
+        float virtualTime = (float)timeLeft / mDuration;
+        if (mDimming) {
+            float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
+            mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
+        } else {
+            float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
+            mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
+        }
+
+        invalidate();
+
+        // Repeat
+        mHandler.post(this);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ui/ContactsPreferences.java b/src/com/android/contacts/ui/ContactsPreferences.java
new file mode 100644 (file)
index 0000000..187df37
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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.ui;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+
+/**
+ * Manages user preferences for contacts.
+ */
+public final class ContactsPreferences  {
+
+    private Context mContext;
+    private ContentResolver mContentResolver;
+    private int mSortOrder = -1;
+    private int mDisplayOrder = -1;
+    private SettingsObserver mSettingsObserver;
+
+    // TODO listen to locale changes
+    public ContactsPreferences(Context context) {
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+
+        mSettingsObserver = new SettingsObserver();
+        mSettingsObserver.register();
+    }
+
+    public boolean isSortOrderUserChangeable() {
+        // TODO this should be locale-specific
+        return true;
+    }
+
+    private int getDefaultSortOrder() {
+        // TODO this should be locale-specific
+        return ContactsContract.Preferences.SORT_ORDER_PRIMARY;
+    }
+
+    public int getSortOrder() {
+        if (mSortOrder == -1) {
+            try {
+                mSortOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        ContactsContract.Preferences.SORT_ORDER);
+            } catch (SettingNotFoundException e) {
+                mSortOrder = getDefaultSortOrder();
+            }
+        }
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+        Settings.System.putInt(mContext.getContentResolver(),
+                ContactsContract.Preferences.SORT_ORDER, sortOrder);
+    }
+
+    public boolean isDisplayOrderUserChangeable() {
+        // TOD this should be locale-specific
+        return true;
+    }
+
+    private int getDefaultDisplayOrder() {
+        // TODO this should be locale-specific
+        return ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY;
+    }
+
+    public int getDisplayOrder() {
+        if (mDisplayOrder == -1) {
+            try {
+                mDisplayOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        ContactsContract.Preferences.DISPLAY_ORDER);
+            } catch (SettingNotFoundException e) {
+                mDisplayOrder = getDefaultDisplayOrder();
+            }
+        }
+        return mDisplayOrder;
+    }
+
+    public void setDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+        Settings.System.putInt(mContext.getContentResolver(),
+                ContactsContract.Preferences.DISPLAY_ORDER, displayOrder);
+    }
+
+    private class SettingsObserver extends ContentObserver {
+
+        public SettingsObserver() {
+            super(null);
+        }
+
+        public void register() {
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(
+                            ContactsContract.Preferences.SORT_ORDER), false, this);
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(
+                            ContactsContract.Preferences.DISPLAY_ORDER), false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mSortOrder = -1;
+            mDisplayOrder = -1;
+
+            // TODO send a message to parent context to notify of the change
+        }
+    }
+}
index 16af321..39c46b9 100644 (file)
@@ -28,6 +28,7 @@ import com.google.android.collect.Lists;
 import android.accounts.Account;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.Dialog;
 import android.app.ExpandableListActivity;
 import android.app.ProgressDialog;
 import android.content.ContentProviderOperation;
@@ -61,6 +62,7 @@ import android.widget.BaseExpandableListAdapter;
 import android.widget.CheckBox;
 import android.widget.ExpandableListAdapter;
 import android.widget.ExpandableListView;
+import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
 
@@ -84,16 +86,28 @@ public final class DisplayGroupsActivity extends ExpandableListActivity implemen
 
     }
 
+    private static final int DIALOG_SORT_ORDER = 1;
+    private static final int DIALOG_DISPLAY_ORDER = 2;
+
     private ExpandableListView mList;
     private DisplayAdapter mAdapter;
 
     private SharedPreferences mPrefs;
+    private ContactsPreferences mContactsPrefs;
 
     private CheckBox mDisplayPhones;
 
     private View mHeaderPhones;
     private View mHeaderSeparator;
 
+    private View mSortOrderView;
+    private TextView mSortOrderTextView;
+    private int mSortOrder;
+
+    private View mDisplayOrderView;
+    private TextView mDisplayOrderTextView;
+    private int mDisplayOrder;
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -101,11 +115,12 @@ public final class DisplayGroupsActivity extends ExpandableListActivity implemen
 
         mList = getExpandableListView();
         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mContactsPrefs = new ContactsPreferences(this);
 
         final LayoutInflater inflater = getLayoutInflater();
 
         // Add the "Only contacts with phones" header modifier.
-        mHeaderPhones = inflater.inflate(R.layout.display_header, mList, false);
+        mHeaderPhones = inflater.inflate(R.layout.display_options_phones_only, mList, false);
         mHeaderPhones.setId(R.id.header_phones);
         mDisplayPhones = (CheckBox) mHeaderPhones.findViewById(android.R.id.checkbox);
         mDisplayPhones.setChecked(mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
@@ -116,8 +131,12 @@ public final class DisplayGroupsActivity extends ExpandableListActivity implemen
             text1.setText(R.string.showFilterPhones);
             text2.setText(R.string.showFilterPhonesDescrip);
         }
+
         mList.addHeaderView(mHeaderPhones, null, true);
 
+        addSortOrderView();
+        addDisplayOrderView();
+
         // Add the separator before showing the detailed group list.
         mHeaderSeparator = inflater.inflate(R.layout.list_separator, mList, false);
         {
@@ -133,10 +152,168 @@ public final class DisplayGroupsActivity extends ExpandableListActivity implemen
         mList.setOnItemClickListener(this);
         mList.setOnCreateContextMenuListener(this);
 
+        mSortOrder = mContactsPrefs.getSortOrder();
+        mDisplayOrder = mContactsPrefs.getDisplayOrder();
+
         // Start background query to find account details
         new QueryGroupsTask(this).execute();
     }
 
+    private void addSortOrderView() {
+        final LayoutInflater inflater = getLayoutInflater();
+        mSortOrderView = inflater.inflate(R.layout.preference_with_more_button, mList, false);
+        View preferenceLayout = mSortOrderView.findViewById(R.id.preference);
+        preferenceLayout.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View v) {
+                showDialog(DIALOG_SORT_ORDER);
+            }
+        });
+
+        TextView label = (TextView)preferenceLayout.findViewById(R.id.label);
+        label.setText(getString(R.string.display_options_sort_list_by));
+
+        mSortOrderTextView = (TextView)preferenceLayout.findViewById(R.id.data);
+        mList.addHeaderView(mSortOrderView, null, false);
+    }
+
+    private void addDisplayOrderView() {
+        final LayoutInflater inflater = getLayoutInflater();
+
+        mDisplayOrderView = inflater.inflate(R.layout.preference_with_more_button, mList, false);
+        View preferenceLayout = mDisplayOrderView.findViewById(R.id.preference);
+        preferenceLayout.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View v) {
+                showDialog(DIALOG_DISPLAY_ORDER);
+            }
+        });
+
+        TextView label = (TextView)preferenceLayout.findViewById(R.id.label);
+        label.setText(getString(R.string.display_options_view_names_as));
+
+        mDisplayOrderTextView = (TextView)preferenceLayout.findViewById(R.id.data);
+        mList.addHeaderView(mDisplayOrderView, null, false);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        bindView();
+    }
+
+    private void bindView() {
+        mSortOrderTextView.setText(
+                mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY
+                        ? getString(R.string.display_options_sort_by_given_name)
+                        : getString(R.string.display_options_sort_by_family_name));
+
+        mDisplayOrderTextView.setText(
+                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY
+                        ? getString(R.string.display_options_view_given_name_first)
+                        : getString(R.string.display_options_view_family_name_first));
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        switch (id) {
+            case DIALOG_SORT_ORDER:
+                return createSortOrderDialog();
+            case DIALOG_DISPLAY_ORDER:
+                return createDisplayOrderDialog();
+        }
+
+        return null;
+    }
+
+    private Dialog createSortOrderDialog() {
+        String[] items = new String[] {
+                getString(R.string.display_options_sort_by_given_name),
+                getString(R.string.display_options_sort_by_family_name),
+        };
+
+        return new AlertDialog.Builder(this)
+            .setIcon(android.R.drawable.ic_dialog_alert)
+            .setTitle(R.string.display_options_sort_list_by)
+            .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        setSortOrder(dialog);
+                        dialog.dismiss();
+                    }
+                })
+            .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        setSortOrder(dialog);
+                    }
+                })
+            .setNegativeButton(android.R.string.cancel, null)
+            .create();
+    }
+
+    private Dialog createDisplayOrderDialog() {
+        String[] items = new String[] {
+                getString(R.string.display_options_view_given_name_first),
+                getString(R.string.display_options_view_family_name_first),
+        };
+
+        return new AlertDialog.Builder(this)
+            .setIcon(android.R.drawable.ic_dialog_alert)
+            .setTitle(R.string.display_options_view_names_as)
+            .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        setDisplayOrder(dialog);
+                        dialog.dismiss();
+                    }
+                })
+            .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        setDisplayOrder(dialog);
+                    }
+                })
+            .setNegativeButton(android.R.string.cancel, null)
+            .create();
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog) {
+        switch (id) {
+            case DIALOG_SORT_ORDER:
+                setCheckedItem(dialog,
+                        mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY ? 0 : 1);
+                break;
+            case DIALOG_DISPLAY_ORDER:
+                setCheckedItem(dialog,
+                        mSortOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY ? 0 : 1);
+                break;
+        }
+    }
+
+    private void setCheckedItem(Dialog dialog, int position) {
+        ListView listView = ((AlertDialog)dialog).getListView();
+        listView.setItemChecked(position, true);
+        listView.setSelection(position);
+    }
+
+    protected void setSortOrder(DialogInterface dialog) {
+        ListView listView = ((AlertDialog)dialog).getListView();
+        int checked = listView.getCheckedItemPosition();
+        mSortOrder = checked == 0
+                ? ContactsContract.Preferences.SORT_ORDER_PRIMARY
+                : ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE;
+
+        bindView();
+    }
+
+    protected void setDisplayOrder(DialogInterface dialog) {
+        ListView listView = ((AlertDialog)dialog).getListView();
+        int checked = listView.getCheckedItemPosition();
+        mDisplayOrder = checked == 0
+                ? ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY
+                : ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE;
+
+        bindView();
+    }
+
     /**
      * Background operation to build set of {@link AccountDisplay} for each
      * {@link Sources#getAccounts(boolean)} that provides groups.
@@ -799,6 +976,9 @@ public final class DisplayGroupsActivity extends ExpandableListActivity implemen
     }
 
     private void doSaveAction() {
+        mContactsPrefs.setSortOrder(mSortOrder);
+        mContactsPrefs.setDisplayOrder(mDisplayOrder);
+
         if (mAdapter == null) return;
         setDisplayOnlyPhones(mDisplayPhones.isChecked());
         new UpdateTask(this).execute(mAdapter.mAccounts);