Add a component to load category/tiles by key.

And switch between using SettingDrawerActivity.getDashboardCategories()
and the new CategoryManager in different conditions.

Test: SettingsRoboTests for regression. Will write tests for new feature
soon once we are set on the data structure.
Bug: 31781480

Change-Id: I864e5aea869071df63ca89002fb378c235d0a1fe
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
new file mode 100644
index 0000000..9821fb8
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2016 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.settingslib.drawer;
+
+public final class CategoryKey {
+
+    // Activities in this category shows up in Settings homepage.
+    public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.homepage";
+
+    // Top level categor.
+    public static final String CATEGORY_NETWORK = "com.android.settings.category.wireless";
+    public static final String CATEGORY_DEVICE = "com.android.settings.category.device";
+    public static final String CATEGORY_APPS = "com.android.settings.category.apps";
+    public static final String CATEGORY_BATTERY = "com.android.settings.category.battery";
+    public static final String CATEGORY_DISPLAY = "com.android.settings.category.display";
+    public static final String CATEGORY_SOUND = "com.android.settings.category.sound";
+    public static final String CATEGORY_STORAGE = "com.android.settings.category.storage";
+    public static final String CATEGORY_SECURITY = "com.android.settings.category.security";
+    public static final String CATEGORY_ACCOUNT = "com.android.settings.category.accounts";
+    public static final String CATEGORY_SYSTEM = "com.android.settings.category.system";
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java
new file mode 100644
index 0000000..a8f286d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (C) 2016 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.settingslib.drawer;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settingslib.applications.InterestingConfigChanges;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class CategoryManager {
+
+    private static final String TAG = "CategoryManager";
+
+    private static CategoryManager sInstance;
+
+    private final InterestingConfigChanges mInterestingConfigChanges;
+
+    // Tile cache (key: <packageName, activityName>, value: tile)
+    private final Map<Pair<String, String>, Tile> mTileByComponentCache;
+
+    // Tile cache (key: category key, value: category)
+    private final Map<String, DashboardCategory> mCategoryByKeyMap;
+
+    private List<DashboardCategory> mCategories;
+
+    public static CategoryManager get() {
+        if (sInstance == null) {
+            sInstance = new CategoryManager();
+        }
+        return sInstance;
+    }
+
+    CategoryManager() {
+        mInterestingConfigChanges = new InterestingConfigChanges();
+        mTileByComponentCache = new ArrayMap<>();
+        mCategoryByKeyMap = new ArrayMap<>();
+    }
+
+    public DashboardCategory getTilesByCategory(Context context, String categoryKey) {
+        tryInitCategories(context);
+
+        final DashboardCategory category = mCategoryByKeyMap.get(categoryKey);
+        if (category == null) {
+            throw new IllegalStateException("Can't find category with key " + categoryKey);
+        }
+        return category;
+    }
+
+    public List<DashboardCategory> getCategories(Context context) {
+        tryInitCategories(context);
+        return mCategories;
+    }
+
+    public void reloadAllCategoriesForConfigChange(Context context) {
+        if (mInterestingConfigChanges.applyNewConfig(context.getResources())) {
+            mCategories = null;
+            tryInitCategories(context);
+        }
+    }
+
+    public void updateCategoryFromBlacklist(Set<ComponentName> tileBlacklist) {
+        if (mCategories == null) {
+            Log.w(TAG, "Category is null, skipping blacklist update");
+        }
+        for (int i = 0; i < mCategories.size(); i++) {
+            DashboardCategory category = mCategories.get(i);
+            for (int j = 0; j < category.tiles.size(); j++) {
+                Tile tile = category.tiles.get(j);
+                if (tileBlacklist.contains(tile.intent.getComponent())) {
+                    category.tiles.remove(j--);
+                }
+            }
+        }
+    }
+
+    private void tryInitCategories(Context context) {
+        if (mCategories == null) {
+            mTileByComponentCache.clear();
+            mCategoryByKeyMap.clear();
+            mCategories = TileUtils.getCategories(context, mTileByComponentCache,
+                    false /* categoryDefinedInManifest */);
+            for (DashboardCategory category : mCategories) {
+                mCategoryByKeyMap.put(category.key, category);
+            }
+        }
+    }
+
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
index 53be0e6..3fc999f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
@@ -16,15 +16,20 @@
 
 package com.android.settingslib.drawer;
 
+import android.content.ComponentName;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
 
 public class DashboardCategory implements Parcelable {
 
+    private static final String TAG = "DashboardCategory";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
     /**
      * Title of the category that is shown to the user.
      */
@@ -74,6 +79,22 @@
         return tiles.get(n);
     }
 
+    public boolean containsComponent(ComponentName component) {
+        for (Tile tile : tiles) {
+            if (TextUtils.equals(tile.intent.getComponent().getClassName(),
+                    component.getClassName())) {
+                if (DEBUG) {
+                    Log.d(TAG,  "category " + key + "contains component" + component);
+                }
+                return true;
+            }
+        }
+        if (DEBUG) {
+            Log.d(TAG,  "category " + key + " does not contain component" + component);
+        }
+        return false;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
index 05585e53e..50867eb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
@@ -33,7 +33,6 @@
 import android.provider.Settings;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.widget.DrawerLayout;
-import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -64,12 +63,9 @@
 
     public static final String EXTRA_SHOW_MENU = "show_drawer_menu";
 
-    private static List<DashboardCategory> sDashboardCategories;
-    private static HashMap<Pair<String, String>, Tile> sTileCache;
     // Serves as a temporary list of tiles to ignore until we heard back from the PM that they
     // are disabled.
     private static ArraySet<ComponentName> sTileBlacklist = new ArraySet<>();
-    private static InterestingConfigChanges sConfigTracker;
 
     private final PackageReceiver mPackageReceiver = new PackageReceiver();
     private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
@@ -80,6 +76,15 @@
     private boolean mShowingMenu;
     private UserManager mUserManager;
 
+    // Remove below after new IA
+    @Deprecated
+    private static List<DashboardCategory> sDashboardCategories;
+    @Deprecated
+    private static HashMap<Pair<String, String>, Tile> sTileCache;
+    @Deprecated
+    private static InterestingConfigChanges sConfigTracker;
+    // Remove above after new IA
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -105,7 +110,9 @@
             mDrawerLayout = null;
             return;
         }
-        getDashboardCategories();
+        if (!isDashboardFeatureEnabled()) {
+            getDashboardCategories();
+        }
         setActionBar(toolbar);
         mDrawerAdapter = new SettingsDrawerAdapter(this);
         ListView listView = (ListView) findViewById(R.id.left_drawer);
@@ -144,7 +151,11 @@
             filter.addDataScheme("package");
             registerReceiver(mPackageReceiver, filter);
 
-            new CategoriesUpdater().execute();
+            if (isDashboardFeatureEnabled()) {
+                new CategoriesUpdateTask().execute();
+            } else {
+                new CategoriesUpdater().execute();
+            }
         }
         final Intent intent = getIntent();
         if (intent != null) {
@@ -173,23 +184,23 @@
         if (componentName == null) {
             return false;
         }
-        // Look for a tile that has the same component as incoming intent
-        final List<DashboardCategory> categories = getDashboardCategories();
-        for (DashboardCategory category : categories) {
-            for (Tile tile : category.tiles) {
-                if (TextUtils.equals(tile.intent.getComponent().getClassName(),
-                        componentName.getClassName())) {
-                    if (DEBUG) {
-                        Log.d(TAG, "intent is for top level tile: " + tile.title);
-                    }
+        if (isDashboardFeatureEnabled()) {
+            final DashboardCategory homepageCategories = CategoryManager.get()
+                    .getTilesByCategory(this, CategoryKey.CATEGORY_HOMEPAGE);
+            return homepageCategories.containsComponent(componentName);
+        } else {
+            // Look for a tile that has the same component as incoming intent
+            final List<DashboardCategory> categories = getDashboardCategories();
+            for (DashboardCategory category : categories) {
+                if (category.containsComponent(componentName)) {
                     return true;
                 }
             }
+            if (DEBUG) {
+                Log.d(TAG, "Intent is not for top level settings " + intent);
+            }
+            return false;
         }
-        if (DEBUG) {
-            Log.d(TAG, "Intent is not for top level settings " + intent);
-        }
-        return false;
     }
 
     public void addCategoryListener(CategoryListener listener) {
@@ -255,7 +266,11 @@
             return;
         }
         // TODO: Do this in the background with some loading.
-        mDrawerAdapter.updateCategories();
+        if (isDashboardFeatureEnabled()) {
+            mDrawerAdapter.updateHomepageCategories();
+        } else {
+            mDrawerAdapter.updateCategories();
+        }
         if (mDrawerAdapter.getCount() != 0) {
             mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
         } else {
@@ -343,13 +358,6 @@
         }
     }
 
-    public HashMap<Pair<String, String>, Tile> getTileCache() {
-        if (sTileCache == null) {
-            getDashboardCategories();
-        }
-        return sTileCache;
-    }
-
     public void onProfileTileOpen() {
         finish();
     }
@@ -368,7 +376,11 @@
                     ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                     : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                     PackageManager.DONT_KILL_APP);
-            new CategoriesUpdater().execute();
+            if (isDashboardFeatureEnabled()) {
+                new CategoriesUpdateTask().execute();
+            } else {
+                new CategoriesUpdater().execute();
+            }
         }
     }
 
@@ -376,6 +388,10 @@
         void onCategoriesChanged();
     }
 
+    /**
+     * @deprecated remove after new IA
+     */
+    @Deprecated
     private class CategoriesUpdater extends AsyncTask<Void, Void, List<DashboardCategory>> {
         @Override
         protected List<DashboardCategory> doInBackground(Void... params) {
@@ -408,10 +424,39 @@
         }
     }
 
+    private class CategoriesUpdateTask extends AsyncTask<Void, Void, Void> {
+
+        private final CategoryManager mCategoryManager;
+
+        public CategoriesUpdateTask() {
+            mCategoryManager = CategoryManager.get();
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            mCategoryManager.reloadAllCategoriesForConfigChange(SettingsDrawerActivity.this);
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            mCategoryManager.updateCategoryFromBlacklist(sTileBlacklist);
+            onCategoriesChanged();
+        }
+    }
+
+    protected boolean isDashboardFeatureEnabled() {
+        return false;
+    }
+
     private class PackageReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
-            new CategoriesUpdater().execute();
+            if (isDashboardFeatureEnabled()) {
+                new CategoriesUpdateTask().execute();
+            } else {
+                new CategoriesUpdater().execute();
+            }
         }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java
index 1d6197a..e1216a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java
@@ -37,6 +37,10 @@
         mActivity = activity;
     }
 
+    /**
+     * @deprecated Remove after new IA
+     */
+    @Deprecated
     void updateCategories() {
         List<DashboardCategory> categories = mActivity.getDashboardCategories();
         mItems.clear();
@@ -64,6 +68,27 @@
         notifyDataSetChanged();
     }
 
+    public void updateHomepageCategories() {
+        DashboardCategory category =
+                CategoryManager.get().getTilesByCategory(mActivity, CategoryKey.CATEGORY_HOMEPAGE);
+        mItems.clear();
+        // Spacer.
+        mItems.add(null);
+        Item tile = new Item();
+        tile.label = mActivity.getString(R.string.home);
+        tile.icon = Icon.createWithResource(mActivity, R.drawable.home);
+        mItems.add(tile);
+        for (int j = 0; j < category.tiles.size(); j++) {
+            tile = new Item();
+            Tile dashboardTile = category.tiles.get(j);
+            tile.label = dashboardTile.title;
+            tile.icon = dashboardTile.icon;
+            tile.tile = dashboardTile;
+            mItems.add(tile);
+        }
+        notifyDataSetChanged();
+    }
+
     public Tile getTile(int position) {
         return mItems.get(position) != null ? mItems.get(position).tile : null;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
index e70cc29..81f0e84 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
@@ -94,6 +94,13 @@
     private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
 
     /**
+     * The key used to get the category from metadata of activities of action
+     * {@link #EXTRA_SETTINGS_ACTION}
+     * The value must be one of constants defined in {@code CategoryKey}.
+     */
+    private static final String EXTRA_IA_CATEGORY_KEY = "com.android.settings.iacategory";
+
+    /**
      * Name of the meta-data item that should be set in the AndroidManifest.xml
      * to specify the icon that should be displayed for the preference.
      */
@@ -113,8 +120,24 @@
 
     private static final String SETTING_PKG = "com.android.settings";
 
+    /**
+     * Build a list of DashboardCategory. Each category must be defined in manifest.
+     * eg: .Settings$DeviceSettings
+     * @deprecated
+     */
+    @Deprecated
     public static List<DashboardCategory> getCategories(Context context,
-            HashMap<Pair<String, String>, Tile> cache) {
+            Map<Pair<String, String>, Tile> cache) {
+        return getCategories(context, cache, true /*categoryDefinedInManifest*/);
+    }
+
+    /**
+     * Build a list of DashboardCategory.
+     * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
+     * represent this category (eg: .Settings$DeviceSettings)
+     */
+    public static List<DashboardCategory> getCategories(Context context,
+            Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest) {
         final long startTime = System.currentTimeMillis();
         boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
                 != 0;
@@ -134,11 +157,12 @@
                 getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
             }
         }
+
         HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
         for (Tile tile : tiles) {
             DashboardCategory category = categoryMap.get(tile.category);
             if (category == null) {
-                category = createCategory(context, tile.category);
+                category = createCategory(context, tile.category, categoryDefinedInManifest);
                 if (category == null) {
                     Log.w(LOG_TAG, "Couldn't find category " + tile.category);
                     continue;
@@ -157,9 +181,21 @@
         return categories;
     }
 
-    private static DashboardCategory createCategory(Context context, String categoryKey) {
+    /**
+     * Create a new DashboardCategory from key.
+     *
+     * @param context Context to query intent
+     * @param categoryKey The category key
+     * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
+     * represent this category (eg: .Settings$DeviceSettings)
+     */
+    private static DashboardCategory createCategory(Context context, String categoryKey,
+            boolean categoryDefinedInManifest) {
         DashboardCategory category = new DashboardCategory();
         category.key = categoryKey;
+        if (!categoryDefinedInManifest) {
+            return category;
+        }
         PackageManager pm = context.getPackageManager();
         List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
         if (results.size() == 0) {
@@ -204,14 +240,19 @@
             ActivityInfo activityInfo = resolved.activityInfo;
             Bundle metaData = activityInfo.metaData;
             String categoryKey = defaultCategory;
-            if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
-                    && categoryKey == null) {
+            if (metaData != null && categoryKey == null) {
+                // categoryKey is null, try to get it from metadata.
+                if (metaData.containsKey(EXTRA_IA_CATEGORY_KEY)) {
+                    categoryKey = metaData.getString(EXTRA_IA_CATEGORY_KEY);
+                } else if (metaData.containsKey(EXTRA_CATEGORY_KEY)) {
+                    categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
+                }
+            }
+            if (checkCategory && categoryKey == null) {
                 Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
                         + intent + " missing metadata "
                         + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
                 continue;
-            } else {
-                categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
             }
             Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
                     activityInfo.name);
@@ -238,16 +279,6 @@
         }
     }
 
-    private static DashboardCategory getCategory(List<DashboardCategory> target,
-            String categoryKey) {
-        for (DashboardCategory category : target) {
-            if (categoryKey.equals(category.key)) {
-                return category;
-            }
-        }
-        return null;
-    }
-
     private static boolean updateTileData(Context context, Tile tile,
             ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm) {
         if (applicationInfo.isSystemApp()) {