Change Aizy to use a primary alphabet and fixed letter distances

Change-Id: I4da56785e99d51c893ba0e5097b37da751382fb7
diff --git a/res/layout-xlarge/contacts_list_content.xml b/res/layout-xlarge/contacts_list_content.xml
new file mode 100644
index 0000000..403a46a
--- /dev/null
+++ b/res/layout-xlarge/contacts_list_content.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout 
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/pinned_header_list_layout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        >
+
+    <com.android.contacts.list.ContactListAizyView
+        android:id="@+id/contacts_list_aizy"
+        android:layout_width="40dip"
+        android:layout_height="match_parent"
+    />
+
+    <LinearLayout
+            android:layout_width="0px"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:layout_weight="1"
+            >
+
+        <view
+            class="com.android.contacts.ContactEntryListView"
+            android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="0dip"
+            android:fastScrollEnabled="true"
+            android:layout_weight="1"
+        />
+
+        <include layout="@layout/contacts_list_empty"/>
+
+        <ViewStub android:id="@+id/footer_stub"
+            android:layout="@layout/footer_panel"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+        />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/aizy_popup_window.xml b/res/layout/aizy_popup_window.xml
index 470f116..801b481 100644
--- a/res/layout/aizy_popup_window.xml
+++ b/res/layout/aizy_popup_window.xml
@@ -22,11 +22,11 @@
     >
 
     <TextView
-        android:layout_width="wrap_content"
+        android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:id="@+id/caption"
         android:width="75dip"
-        android:textSize="50sp"
+        android:textSize="70sp"
         android:paddingLeft="20dip"
         android:gravity="center" />
 
diff --git a/res/layout/contacts_list_content.xml b/res/layout/contacts_list_content.xml
index f5bb785..8d1c50a 100644
--- a/res/layout/contacts_list_content.xml
+++ b/res/layout/contacts_list_content.xml
@@ -22,12 +22,6 @@
         android:orientation="horizontal"
         >
 
-    <com.android.contacts.list.ContactListAizyView
-        android:id="@+id/contacts_list_aizy"
-        android:layout_width="50dip"
-        android:layout_height="match_parent"
-    />
-
     <LinearLayout
             android:layout_width="0px"
             android:layout_height="match_parent"
@@ -52,4 +46,9 @@
             android:layout_height="wrap_content"
         />
     </LinearLayout>
+    <com.android.contacts.list.ContactListAizyView
+        android:id="@+id/contacts_list_aizy"
+        android:layout_width="30dip"
+        android:layout_height="match_parent"
+    />
 </LinearLayout>
diff --git a/res/values-xlarge/dimens.xml b/res/values-xlarge/dimens.xml
new file mode 100644
index 0000000..e99b257
--- /dev/null
+++ b/res/values-xlarge/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+    <!-- Size of the text in the aizy visual scroll control -->
+    <dimen name="aizy_text_size">17dip</dimen>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 5f39532..c475148 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -26,4 +26,10 @@
     <color name="pinned_header_background">#ff202020</color>
 
     <color name="translucent_search_background">#cc000000</color>
+
+    <!-- Color used in the Aizy visual scroll control for empty section -->
+    <color name="aizy_empty_section">#ff666666</color>
+
+    <!-- Color used in the Aizy visual scroll control for non-empty sections -->
+    <color name="aizy_non_empty_section">#ffcccccc</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index ff9e92e..b8b06ff 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -45,4 +45,8 @@
 
     <dimen name="aggregation_suggestion_icon_size">40dip</dimen>
 
+    <!-- Size of the text in the aizy visual scroll control -->
+    <dimen name="aizy_text_size">13dip</dimen>
+    <dimen name="aizy_preview_width">130dip</dimen>
+    <dimen name="aizy_preview_height">115dip</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5bdfdf8..085b9ea 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1268,4 +1268,13 @@
     <!-- The button next to the message about multiple contact aggregation suggestions in Contact editor. [CHAR LIMIT=12]-->
     <string name="aggregation_suggestion_view_button">View</string>
 
+    <!-- Primary alphabet of this language. Each of these characters always has its own section in
+         the visual scroll control next to the contact list. These letters must be uppercase.
+         While there is no hard limit on the number of characters, there should not be more than
+         40. If the language requires more, this string should instead be empty so that the
+         visual scroll control adapts to the contents.
+         This text must only contain letters that can appear as sections, otherwise they would
+         only be empty.
+         Translations require extensive QA! -->
+    <string name="visualScrollerAlphabet">A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z</string>
 </resources>
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index ee1cfe0..708cdd8 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -361,7 +361,7 @@
         mAdapter.changeCursor(partitionIndex, data);
         showCount(partitionIndex, data);
         if (partitionIndex == mAdapter.getIndexedPartition()) {
-            mAizy.setIndexer(mAdapter.getIndexer());
+            mAizy.readFromIndexer(mAdapter.getIndexer());
         }
 
         // TODO should probably only restore instance state after all directories are loaded
@@ -603,7 +603,12 @@
         }
 
         mAizy = (ContactListAizyView) mView.findViewById(R.id.contacts_list_aizy);
-        mAizy.setListView(mListView);
+        mAizy.setListener(new ContactListAizyView.Listener() {
+            @Override
+            public void onScroll(int position) {
+                mListView.setSelectionFromTop(position + mListView.getHeaderViewsCount(), 0);
+            }
+        });
 
         configureAizy();
         configurePhotoLoader();
@@ -652,11 +657,9 @@
         }
     }
 
+    @Override
     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
             int totalItemCount) {
-        if (isAizyEnabled()) {
-            mAizy.listOnScroll(firstVisibleItem);
-        }
     }
 
     public void onScrollStateChanged(AbsListView view, int scrollState) {
diff --git a/src/com/android/contacts/list/ContactListAizyView.java b/src/com/android/contacts/list/ContactListAizyView.java
index 07ce12c..a2fa706 100644
--- a/src/com/android/contacts/list/ContactListAizyView.java
+++ b/src/com/android/contacts/list/ContactListAizyView.java
@@ -17,21 +17,27 @@
 package com.android.contacts.list;
 
 import com.android.contacts.R;
+import com.android.contacts.util.PhonebookCollatorFactory;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.FontMetrics;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.widget.ListView;
 import android.widget.PopupWindow;
 import android.widget.SectionIndexer;
 import android.widget.TextView;
 
+import java.text.Collator;
+import java.util.ArrayList;
+
 /**
  * A View that displays the sections given by an Indexer and their relative sizes. For
  * English and similar languages, this is an A to Z list (where only the used letters are
@@ -41,23 +47,50 @@
 public class ContactListAizyView extends View {
     private static final String TAG = "ContactListAizyView";
 
-    // TODO: Put these into resource files or create from image resources
-    private static final int TEXT_WIDTH = 20;
-    private static final int CIRCLE_DIAMETER = 30;
-    private static final int PREVIEW_WIDTH = 130;
-    private static final int PREVIEW_HEIGHT = 115;
+    private static final int PREVIEW_TIME_DELAY_MS = 400;
 
-    private SectionIndexer mIndexer;
-
-    private boolean mCalculateYCoordinates;
-    private ListView mListView;
-    private float mPosition;
-    private float mFactor;
+    private Listener mListener;
     private PopupWindow mPreviewPopupWindow;
     private TextView mPreviewPopupTextView;
+
+    private ResourceValues mResourceValues;
+
+    /**
+     * True if the popup window is currently visible.
+     */
     private boolean mPreviewPopupVisible;
+
+    /**
+     * Time when the user started tapping. This is used to calculate the time delay before fading
+     * in the PopupWindow
+     */
+    private long mPreviewPopupStartTime;
+
+    /**
+     * Needed only inside {@link #onTouchEvent(MotionEvent)} to get the location of touch events.
+     */
     private int[] mWindowOffset;
-    private float[] yPositions = null;
+
+    /**
+     * Needed to measure text. Used inside {@link #onDraw(Canvas)}
+     */
+    private final Rect bounds = new Rect();
+
+    /**
+     * Used and cached inside {@link #onDraw(Canvas)}
+     */
+    private FontMetrics mFontMetrics;
+
+    /**
+     * Used and cached inside {@link #onDraw(Canvas)}
+     */
+    private Paint mPaint;
+
+    /**
+     * The list of displayed sections. "Virtual" sections can be empty and therefore don't show
+     * up as regular sections
+     */
+    private final ArrayList<VirtualSection> mVirtualSections = new ArrayList<VirtualSection>();
 
     public ContactListAizyView(Context context) {
         super(context);
@@ -75,41 +108,120 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
 
+        mResourceValues = new ResourceValues(getResources());
+
         final LayoutInflater inflater =
                 (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         mPreviewPopupWindow = new PopupWindow(
                 inflater.inflate(R.layout.aizy_popup_window, null, false),
-                PREVIEW_WIDTH, PREVIEW_HEIGHT);
+                (int) mResourceValues.previewWidth, (int) mResourceValues.previewHeight);
+        mPreviewPopupWindow.setAnimationStyle(android.R.style.Animation_Toast);
         mPreviewPopupTextView =
                 (TextView) mPreviewPopupWindow.getContentView().findViewById(R.id.caption);
     }
 
-    public void setIndexer(SectionIndexer indexer) {
-        mIndexer = indexer;
-        mCalculateYCoordinates = true;
+    /**
+     * Sets up the Aizy based on the indexer and completely reads its contents.
+     * This function has to be called everytime the data is changed.
+     */
+    public void readFromIndexer(SectionIndexer indexer) {
+        mVirtualSections.clear();
+        final String alphabetString = getResources().getString(R.string.visualScrollerAlphabet);
+        final String[] alphabet = alphabetString.split(";");
+
+        // We expect to get 10 additional items that the base alphabet
+        mVirtualSections.ensureCapacity(alphabet.length + 10);
+
+        if (indexer != null) {
+            // Add the real sections
+            final Object[] sections = indexer.getSections();
+            for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
+                final Object section = sections[sectionIndex];
+                final String caption = section == null ? "" : section.toString();
+                final int position = indexer.getPositionForSection(sectionIndex);
+                mVirtualSections.add(new VirtualSection(caption, sectionIndex, position));
+            }
+        }
+
+        final Collator collator = PhonebookCollatorFactory.getCollator();
+
+        // Add the base alphabet if missing
+        for (String caption : alphabet) {
+            boolean insertAtEnd = true;
+            VirtualSection previousVirtualSection = null;
+            for (int i = 0; i < mVirtualSections.size(); i++) {
+                final VirtualSection virtualSection = mVirtualSections.get(i);
+                final String virtualSectionCaption = virtualSection.getCaption();
+                final int comparison = collator.compare(virtualSectionCaption, caption);
+                if (comparison == 0) {
+                    // element is already in the list.
+                    insertAtEnd = false;
+                    break;
+                }
+                if (comparison > 0) {
+                    // we stepped too far. the element belongs before the element at i
+                    insertAtEnd = false;
+                    final int realSectionPosition = previousVirtualSection == null ? 0
+                            : previousVirtualSection.getRealSectionPosition();
+                    mVirtualSections.add(i, new VirtualSection(caption, -1, realSectionPosition));
+                    break;
+                }
+                previousVirtualSection = virtualSection;
+            }
+            if (insertAtEnd) {
+                final int realSectionPosition = previousVirtualSection == null ? 0
+                        : previousVirtualSection.getRealSectionPosition();
+                mVirtualSections.add(new VirtualSection(caption, -1, realSectionPosition));
+            }
+        }
+        invalidate();
     }
 
-    public void setListView(ListView listView) {
-        mListView = listView;
+    /**
+     * Sets the Listener that is called everytime the user taps on this control.
+     */
+    public void setListener(Listener listener) {
+        mListener = listener;
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        setMeasuredDimension(TEXT_WIDTH + CIRCLE_DIAMETER, resolveSize(0, heightMeasureSpec));
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        mCalculateYCoordinates = true;
+        setMeasuredDimension(resolveSize(0, widthMeasureSpec), resolveSize(0, heightMeasureSpec));
     }
 
     @Override
     protected void onDraw(Canvas canvas) {
-        if (mIndexer == null) return;
+        if (mPaint == null) {
+            mPaint = new Paint();
+            mPaint.setTextSize(mResourceValues.textSize);
+            mPaint.setAntiAlias(true);
+            mPaint.setTextAlign(Align.CENTER);
+        }
+        if (mFontMetrics == null) {
+            mFontMetrics = mPaint.getFontMetrics();
+        }
+        final float fontHeight = mFontMetrics.descent - mFontMetrics.ascent;
+        final int halfWidth = getWidth() / 2;
+        // Draw
+        float lastVisibleY = Float.NEGATIVE_INFINITY;
+        final float sectionHeight = (float) getHeight() / mVirtualSections.size();
+        for (int i = 0; i < mVirtualSections.size(); i++) {
+            final VirtualSection virtualSection = mVirtualSections.get(i);
+            final String caption = virtualSection.getCaption();
+            if (!virtualSection.isMeasured()) {
+                mPaint.getTextBounds(caption, 0, caption.length(), bounds);
+                virtualSection.setMeasuredSize(-bounds.top);
+            }
+            final float y = i * sectionHeight;
+            if (lastVisibleY + fontHeight < y) {
+                mPaint.setColor(virtualSection.getRealSectionIndex() != -1
+                        ? mResourceValues.nonEmptySectionColor : mResourceValues.emptySectionColor);
 
-        calcYCoordinates();
-
-        drawLineAndText(canvas);
+                canvas.drawText(caption, halfWidth,
+                        y + sectionHeight / 2 + virtualSection.getMeasuredSize() / 2, mPaint);
+                lastVisibleY = y;
+            }
+        }
     }
 
     @Override
@@ -119,10 +231,31 @@
             getLocationInWindow(mWindowOffset);
         }
 
+        // Scroll the list itself
+        final int boundedY = Math.min(Math.max(0, (int) (event.getY())), getHeight() - 1);
+        final int index = boundedY * mVirtualSections.size() / getHeight();
+        final VirtualSection virtualSection = mVirtualSections.get(index);
+        final int sectionY = index * getHeight() / mVirtualSections.size();
+        mPreviewPopupTextView.setText(virtualSection.getCaption());
+        mPreviewPopupTextView.setTextColor(virtualSection.getRealSectionIndex() != -1
+                ? mResourceValues.nonEmptySectionColor : mResourceValues.emptySectionColor);
+
+        // Draw popup window
         final int previewX = mWindowOffset[0] + getWidth();
-        final int previewY = (int) event.getY() + mWindowOffset[1]
-                - mPreviewPopupWindow.getHeight() / 2;
-        final boolean previewPopupVisible = event.getActionMasked() == MotionEvent.ACTION_MOVE;
+        final float sectionHeight = (float) getHeight() / mVirtualSections.size();
+        final int previewY = (int) (sectionY + mWindowOffset[1] + (sectionHeight -
+                mPreviewPopupWindow.getHeight()) / 2);
+        final int actionMasked = event.getActionMasked();
+        final boolean fingerIsDown = actionMasked == MotionEvent.ACTION_DOWN;
+        if (fingerIsDown) {
+            mPreviewPopupStartTime = System.currentTimeMillis();
+        }
+        final boolean fingerIsDownOrScrubbing =
+            actionMasked == MotionEvent.ACTION_MOVE || actionMasked == MotionEvent.ACTION_DOWN;
+
+        final boolean previewPopupVisible = fingerIsDownOrScrubbing &&
+                (System.currentTimeMillis() > mPreviewPopupStartTime + PREVIEW_TIME_DELAY_MS);
+
         if (previewPopupVisible != mPreviewPopupVisible) {
             if (previewPopupVisible) {
                 mPreviewPopupWindow.showAtLocation(this, Gravity.LEFT | Gravity.TOP,
@@ -134,90 +267,71 @@
         } else {
             mPreviewPopupWindow.update(previewX, previewY, -1, -1);
         }
-        final int position = Math.max(0, (int) (event.getY() / mFactor));
-        if (mIndexer != null) {
-            final int index = mIndexer.getSectionForPosition(position);
-            final Object[] sections = mIndexer.getSections();
-            final String caption =
-                    (index != -1 && index < sections.length) ? sections[index].toString() : "";
-            mPreviewPopupTextView.setText(caption);
-        }
-        if (mListView != null) {
-            mListView.setSelectionFromTop(position, 0);
-        }
+
+        // Perform the actual scrolling
+        if (mListener != null) mListener.onScroll(virtualSection.getRealSectionPosition());
 
         super.onTouchEvent(event);
         return true;
     }
 
-    private void calcYCoordinates() {
-        if (!mCalculateYCoordinates) return;
-        mCalculateYCoordinates = false;
+    /**
+     * Reads an provides all values from the resource files
+     */
+    private static class ResourceValues {
+        private final int emptySectionColor;
+        private final int nonEmptySectionColor;
+        private final float textSize;
+        private final float previewWidth;
+        private final float previewHeight;
 
-        // Get a String[] of the sections.
-        final Object[] sectionObjects = mIndexer.getSections();
-        final int sectionCount = sectionObjects.length;
-        final String[] sections;
-        if (sectionObjects instanceof String[]) {
-            sections = (String[]) sectionObjects;
-        } else {
-            sections = new String[sectionCount];
-            for (int i = 0; i < sectionCount; i++) {
-                sections[i] = sectionObjects[i] == null ? null : sectionObjects[i].toString();
-            }
+        private ResourceValues(Resources resources) {
+            emptySectionColor = resources.getColor(R.color.aizy_empty_section);
+            nonEmptySectionColor = resources.getColor(R.color.aizy_non_empty_section);
+            textSize = resources.getDimension(R.dimen.aizy_text_size);
+            previewWidth = resources.getDimension(R.dimen.aizy_preview_width);
+            previewHeight = resources.getDimension(R.dimen.aizy_preview_height);
         }
-
-        mFactor = (float) getHeight() / mListView.getCount();
     }
 
-    private void drawLineAndText(Canvas canvas) {
-        // TODO: Figure out how to set the text size and fetch the height in pixels. This
-        // behaviour is OK for prototypes, but has to be refined later
-        final float textSize = 20.0f;
+    private static class VirtualSection {
+        private final String mCaption;
+        private final int mRealSectionIndex;
+        private final int mRealSectionPosition;
+        private float mMeasuredSize = Float.NaN;
 
-        // Move A down, Z up
-        final Paint paint = new Paint();
-        paint.setColor(Color.LTGRAY);
-        paint.setTextSize(textSize);
-        paint.setAntiAlias(true);
-        final Object[] sections = mIndexer.getSections();
-        canvas.drawLine(
-                TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, 0.0f,
-                TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, getHeight(),
-                paint);
-        final int sectionCount = sections.length;
-        if (yPositions == null || yPositions.length != sectionCount) {
-            yPositions = new float[sectionCount];
+        public String getCaption() {
+            return mCaption;
         }
 
-        // Calculate Positions
-        for (int i = 0; i < sectionCount; i++) {
-            yPositions[i] = mIndexer.getPositionForSection(i) * mFactor;
+        public int getRealSectionIndex() {
+            return mRealSectionIndex;
         }
 
-        // Draw
-        float lastVisibleY = Float.MAX_VALUE;
-        for (int i = sectionCount - 1; i >= 0; i--) {
-            final float y = yPositions[i];
-            if (lastVisibleY - textSize > y) {
-                canvas.drawText(sections[i].toString(), 0.0f, y + 0.5f * textSize, paint);
-                lastVisibleY = y;
-            }
-            canvas.drawLine(
-                    TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f - 2, y,
-                    TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f + 2, y,
-                    paint);
+        public int getRealSectionPosition() {
+            return mRealSectionPosition;
         }
 
-        paint.setColor(Color.YELLOW);
-        canvas.drawLine(
-                TEXT_WIDTH + CIRCLE_DIAMETER * 0.0f, mPosition * mFactor,
-                TEXT_WIDTH + CIRCLE_DIAMETER * 1.0f, mPosition * mFactor,
-                paint);
+        public boolean isMeasured() {
+            return mMeasuredSize == Float.NaN;
+        }
+
+        public void setMeasuredSize(float value) {
+            mMeasuredSize = value;
+        }
+
+        public float getMeasuredSize() {
+            return mMeasuredSize;
+        }
+
+        public VirtualSection(String caption, int realSectionIndex, int realSectionPosition) {
+            mCaption = caption;
+            mRealSectionIndex = realSectionIndex;
+            mRealSectionPosition = realSectionPosition;
+        }
     }
 
-    public void listOnScroll(int firstVisibleItem) {
-        mPosition = firstVisibleItem;
-        invalidate();
+    public interface Listener {
+        void onScroll(int position);
     }
 }
diff --git a/src/com/android/contacts/util/PhonebookCollatorFactory.java b/src/com/android/contacts/util/PhonebookCollatorFactory.java
new file mode 100644
index 0000000..08ce27a
--- /dev/null
+++ b/src/com/android/contacts/util/PhonebookCollatorFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import java.text.Collator;
+import java.util.Locale;
+
+/**
+ * Returns the collator that can be used to sort contact list entries. This
+ * collator is the same as the one that is used in sqlite.
+ */
+public final class PhonebookCollatorFactory {
+    public static final Collator getCollator() {
+        final Locale defaultLocale = Locale.getDefault();
+        final String defaultLocaleString = defaultLocale.toString();
+        // For Japanese we use a special collator that puts japanese characters before foreign
+        // ones (this is called a dictionary collator)
+        // Warning: This function has to match the behavior in sqlite3_android.cpp (located in
+        // the framework)
+        final Locale locale;
+        if ("ja".equals(defaultLocaleString) || "ja_JP".equals(defaultLocaleString)) {
+            locale = new Locale("ja@collation=phonebook");
+        } else {
+            locale = defaultLocale;
+        }
+
+        return Collator.getInstance(locale);
+    }
+}