Minor changes to apps view.

- Ensuring that apps with numbers and in other locals have a section header.
- Adding an empty state when there are no apps with the current filter
- Removing unnecessary call to check AppInfos

Change-Id: I9dc541c680475b98745fa257ad7e4af06e3966c9
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
index 2847afc..c1d2738 100644
--- a/src/com/android/launcher3/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -78,9 +78,7 @@
      * Returns the section name for the application.
      */
     public String getSectionNameForApp(AppInfo info) {
-        String title = info.title.toString();
-        String sectionName = mIndexer.getBucketLabel(mIndexer.getBucketIndex(title));
-        return sectionName;
+        return mIndexer.computeSectionName(info.title.toString().trim());
     }
 
     /**
@@ -91,6 +89,13 @@
     }
 
     /**
+     * Returns whether there are no filtered results.
+     */
+    public boolean hasNoFilteredResults() {
+        return (mFilter != null) && mFilteredApps.isEmpty();
+    }
+
+    /**
      * Sets the current filter for this list of apps.
      */
     public void setFilter(Filter f) {
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
index cc31e20..64b27ba 100644
--- a/src/com/android/launcher3/AppsContainerView.java
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -16,10 +16,12 @@
 package com.android.launcher3;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.support.v7.widget.RecyclerView;
 import android.text.Editable;
+import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
@@ -30,7 +32,6 @@
 import android.widget.EditText;
 import android.widget.FrameLayout;
 import android.widget.TextView;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
 
 import java.util.List;
 
@@ -76,6 +77,7 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+        Resources res = context.getResources();
 
         mLauncher = (Launcher) context;
         mApps = new AlphabeticalAppsList(context);
@@ -83,6 +85,7 @@
             mNumAppsPerRow = grid.appsViewNumCols;
             AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
                     mLauncher, this);
+            adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
             mLayoutManager = adapter.getLayoutManager(context);
             mItemDecoration = adapter.getItemDecoration();
             mAdapter = adapter;
@@ -90,6 +93,7 @@
         } else if (USE_LAYOUT == LIST_LAYOUT) {
             mNumAppsPerRow = 1;
             AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
+            adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
             mLayoutManager = adapter.getLayoutManager(context);
             mAdapter = adapter;
         }
@@ -163,10 +167,12 @@
         mAppsListView.setHasFixedSize(true);
         if (isRtl) {
             mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(),
-                    mAppsListView.getPaddingRight() + mContentMarginStart, mAppsListView.getPaddingBottom());
+                    mAppsListView.getPaddingRight() + mContentMarginStart,
+                    mAppsListView.getPaddingBottom());
         } else {
-            mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, mAppsListView.getPaddingTop(),
-                    mAppsListView.getPaddingRight(), mAppsListView.getPaddingBottom());
+            mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart,
+                    mAppsListView.getPaddingTop(), mAppsListView.getPaddingRight(),
+                    mAppsListView.getPaddingBottom());
         }
         if (mItemDecoration != null) {
             mAppsListView.addItemDecoration(mItemDecoration);
@@ -299,7 +305,15 @@
         if (s.toString().isEmpty()) {
             mApps.setFilter(null);
         } else {
-            final AlphabeticIndexCompat indexer = mApps.getIndexer();
+            String formatStr = getResources().getString(R.string.apps_view_no_search_results);
+            if (USE_LAYOUT == GRID_LAYOUT) {
+                ((AppsGridAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
+                        s.toString()));
+            } else {
+                ((AppsListAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
+                        s.toString()));
+            }
+
             final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
             mApps.setFilter(new AlphabeticalAppsList.Filter() {
                 @Override
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
index 6727e4f..028cd8f 100644
--- a/src/com/android/launcher3/AppsGridAdapter.java
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -10,6 +10,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 
 
@@ -22,6 +23,7 @@
 
     private static final int SECTION_BREAK_VIEW_TYPE = 0;
     private static final int ICON_VIEW_TYPE = 1;
+    private static final int EMPTY_VIEW_TYPE = 2;
 
     /**
      * ViewHolder for each icon.
@@ -29,11 +31,13 @@
     public static class ViewHolder extends RecyclerView.ViewHolder {
         public View mContent;
         public boolean mIsSectionRow;
+        public boolean mIsEmptyRow;
 
-        public ViewHolder(View v, boolean isSectionRow) {
+        public ViewHolder(View v, boolean isSectionRow, boolean isEmptyRow) {
             super(v);
             mContent = v;
             mIsSectionRow = isSectionRow;
+            mIsEmptyRow = isEmptyRow;
         }
     }
 
@@ -43,8 +47,14 @@
     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
         @Override
         public int getSpanSize(int position) {
+            if (mApps.hasNoFilteredResults()) {
+                // Empty view spans full width
+                return mAppsPerRow;
+            }
+
             AppInfo info = mApps.getApps().get(position);
             if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+                // Section break spans full width
                 return mAppsPerRow;
             } else {
                 return 1;
@@ -59,14 +69,13 @@
 
         @Override
         public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
-            AlphabeticIndexCompat indexer = mApps.getIndexer();
             for (int i = 0; i < parent.getChildCount(); i++) {
                 View child = parent.getChildAt(i);
                 ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
                 if (holder != null) {
                     GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
                             child.getLayoutParams();
-                    if (!holder.mIsSectionRow && !lp.isItemRemoved()) {
+                    if (!holder.mIsSectionRow && !holder.mIsEmptyRow && !lp.isItemRemoved()) {
                         if (mApps.getApps().get(holder.getPosition() - 1) ==
                                 AlphabeticalAppsList.SECTION_BREAK_INFO) {
                             // Draw at the parent
@@ -106,6 +115,7 @@
     private View.OnLongClickListener mIconLongClickListener;
     private int mAppsPerRow;
     private boolean mIsRtl;
+    private String mEmptySearchText;
 
     // Section drawing
     private int mStartMargin;
@@ -141,6 +151,13 @@
     }
 
     /**
+     * Sets the text to show when there are no apps.
+     */
+    public void setEmptySearchText(String query) {
+        mEmptySearchText = query;
+    }
+
+    /**
      * Returns the grid layout manager.
      */
     public GridLayoutManager getLayoutManager(Context context) {
@@ -167,8 +184,12 @@
     @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
+            case EMPTY_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
+                        false), false /* isSectionRow */, true /* isEmptyRow */);
             case SECTION_BREAK_VIEW_TYPE:
-                return new ViewHolder(new View(parent.getContext()), true);
+                return new ViewHolder(new View(parent.getContext()), true /* isSectionRow */,
+                        false /* isEmptyRow */);
             case ICON_VIEW_TYPE:
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         R.layout.apps_grid_row_icon_view, parent, false);
@@ -176,7 +197,7 @@
                 icon.setOnClickListener(mIconClickListener);
                 icon.setOnLongClickListener(mIconLongClickListener);
                 icon.setFocusable(true);
-                return new ViewHolder(icon, false);
+                return new ViewHolder(icon, false /* isSectionRow */, false /* isEmptyRow */);
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -184,21 +205,33 @@
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
-        AppInfo info = mApps.getApps().get(position);
-        if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
-            BubbleTextView icon = (BubbleTextView) holder.mContent;
-            icon.applyFromApplicationInfo(info);
+        switch (holder.getItemViewType()) {
+            case ICON_VIEW_TYPE:
+                AppInfo info = mApps.getApps().get(position);
+                BubbleTextView icon = (BubbleTextView) holder.mContent;
+                icon.applyFromApplicationInfo(info);
+                break;
+            case EMPTY_VIEW_TYPE:
+                TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
+                emptyViewText.setText(mEmptySearchText);
+                break;
         }
     }
 
     @Override
     public int getItemCount() {
+        if (mApps.hasNoFilteredResults()) {
+            // For the empty view
+            return 1;
+        }
         return mApps.getApps().size();
     }
 
     @Override
     public int getItemViewType(int position) {
-        if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+        if (mApps.hasNoFilteredResults()) {
+            return EMPTY_VIEW_TYPE;
+        } else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
             return SECTION_BREAK_VIEW_TYPE;
         }
         return ICON_VIEW_TYPE;
diff --git a/src/com/android/launcher3/AppsListAdapter.java b/src/com/android/launcher3/AppsListAdapter.java
index 8ac381e..e1f4d35 100644
--- a/src/com/android/launcher3/AppsListAdapter.java
+++ b/src/com/android/launcher3/AppsListAdapter.java
@@ -30,12 +30,14 @@
 
     private static final int SECTION_BREAK_VIEW_TYPE = 0;
     private static final int ICON_VIEW_TYPE = 1;
+    private static final int EMPTY_VIEW_TYPE = 2;
 
     private LayoutInflater mLayoutInflater;
     private AlphabeticalAppsList mApps;
     private View.OnTouchListener mTouchListener;
     private View.OnClickListener mIconClickListener;
     private View.OnLongClickListener mIconLongClickListener;
+    private String mEmptySearchText;
 
     public AppsListAdapter(Context context, AlphabeticalAppsList apps,
             View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
@@ -51,9 +53,19 @@
         return new LinearLayoutManager(context);
     }
 
+    /**
+     * Sets the text to show when there are no apps.
+     */
+    public void setEmptySearchText(String query) {
+        mEmptySearchText = query;
+    }
+
     @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
+            case EMPTY_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
+                        false));
             case SECTION_BREAK_VIEW_TYPE:
                 return new ViewHolder(new View(parent.getContext()));
             case ICON_VIEW_TYPE:
@@ -79,39 +91,51 @@
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
-        AppInfo info = mApps.getApps().get(position);
-        if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
-            ViewGroup content = (ViewGroup) holder.mContent;
-            String sectionDescription = mApps.getSectionNameForApp(info);
+        switch (holder.getItemViewType()) {
+            case ICON_VIEW_TYPE:
+                AppInfo info = mApps.getApps().get(position);
+                ViewGroup content = (ViewGroup) holder.mContent;
+                String sectionDescription = mApps.getSectionNameForApp(info);
 
-            // Bind the section header
-            boolean showSectionHeader = true;
-            if (position > 0) {
-                AppInfo prevInfo = mApps.getApps().get(position - 1);
-                showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO);
-            }
-            TextView tv = (TextView) content.findViewById(R.id.section);
-            if (showSectionHeader) {
-                tv.setText(sectionDescription);
-                tv.setVisibility(View.VISIBLE);
-            } else {
-                tv.setVisibility(View.INVISIBLE);
-            }
+                // Bind the section header
+                boolean showSectionHeader = true;
+                if (position > 0) {
+                    AppInfo prevInfo = mApps.getApps().get(position - 1);
+                    showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO);
+                }
+                TextView tv = (TextView) content.findViewById(R.id.section);
+                if (showSectionHeader) {
+                    tv.setText(sectionDescription);
+                    tv.setVisibility(View.VISIBLE);
+                } else {
+                    tv.setVisibility(View.INVISIBLE);
+                }
 
-            // Bind the icon
-            BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
-            icon.applyFromApplicationInfo(info);
+                // Bind the icon
+                BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
+                icon.applyFromApplicationInfo(info);
+                break;
+            case EMPTY_VIEW_TYPE:
+                TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
+                emptyViewText.setText(mEmptySearchText);
+                break;
         }
     }
 
     @Override
     public int getItemCount() {
+        if (mApps.hasNoFilteredResults()) {
+            // For the empty view
+            return 1;
+        }
         return mApps.getApps().size();
     }
 
     @Override
     public int getItemViewType(int position) {
-        if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+        if (mApps.hasNoFilteredResults()) {
+            return EMPTY_VIEW_TYPE;
+        } else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
             return SECTION_BREAK_VIEW_TYPE;
         }
         return ICON_VIEW_TYPE;
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index fabae57..8ef234b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -161,7 +161,8 @@
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
         }
-        setTag(info);
+        // We don't need to check the info since it's not a ShortcutInfo
+        super.setTag(info);
     }
 
     @Override
diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
index 602a845..47e1b7a 100644
--- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
+++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
@@ -27,7 +27,7 @@
     /**
      * Returns the index of the bucket in which the given string should appear.
      */
-    public int getBucketIndex(String s) {
+    protected int getBucketIndex(String s) {
         if (s.isEmpty()) {
             return UNKNOWN_BUCKET_INDEX;
         }
@@ -41,7 +41,7 @@
     /**
      * Returns the label for the bucket at the given index (as returned by getBucketIndex).
      */
-    public String getBucketLabel(int index) {
+    protected String getBucketLabel(int index) {
         return BUCKETS.substring(index, index + 1);
     }
 }
@@ -100,11 +100,29 @@
     }
 
     /**
+     * Computes the section name for an given string {@param s}.
+     */
+    public String computeSectionName(String s) {
+        String sectionName = getBucketLabel(getBucketIndex(s));
+        if (sectionName.trim().isEmpty() && s.length() > 0) {
+            boolean startsWithDigit = Character.isDigit(s.charAt(0));
+            if (startsWithDigit) {
+                // Digit section
+                return "#";
+            } else {
+                // Unknown section
+                return "\u2022";
+            }
+        }
+        return sectionName;
+    }
+
+    /**
      * Returns the index of the bucket in which {@param s} should appear.
      * Function is synchronized because underlying routine walks an iterator
      * whose state is maintained inside the index object.
      */
-    public int getBucketIndex(String s) {
+    protected int getBucketIndex(String s) {
         if (mHasValidAlphabeticIndex) {
             try {
                 return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s);
@@ -118,7 +136,7 @@
     /**
      * Returns the label for the bucket at the given index (as returned by getBucketIndex).
      */
-    public String getBucketLabel(int index) {
+    protected String getBucketLabel(int index) {
         if (mHasValidAlphabeticIndex) {
             try {
                 return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index);