/* * Copyright (C) 2017 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.settings.dashboard; import static android.provider.SearchIndexablesContract.SITE_MAP_COLUMNS; import static com.android.settings.dashboard.DashboardFragmentRegistry.CATEGORY_KEY_TO_PARENT_MAP; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.SearchIndexablesContract.SiteMapColumns; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.support.v4.util.ArrayMap; import android.text.TextUtils; import android.util.Log; import com.android.settings.SettingsActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.search.IndexDatabaseHelper.IndexColumns; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * A manager class that maintains a "site map" and look up breadcrumb for a certain page on demand. *

* The methods on this class can only be called on a background thread. */ public class SiteMapManager { private static final String TAG = "SiteMapManager"; private static final boolean DEBUG_TIMING = false; private static final String[] CLASS_TO_SCREEN_TITLE_COLUMNS = { IndexColumns.CLASS_NAME, IndexColumns.SCREEN_TITLE, }; private final List mPairs = new ArrayList<>(); private boolean mInitialized; /** * Given a fragment class name and its screen title, build a breadcrumb from Settings root to * this screen. *

* Not all screens have a full breadcrumb path leading up to root, it's because either some * page in the breadcrumb path is not indexed, or it's only reachable via search. */ @WorkerThread public synchronized List buildBreadCrumb(Context context, String clazz, String screenTitle) { init(context); final long startTime = System.currentTimeMillis(); final List breadcrumbs = new ArrayList<>(); if (!mInitialized) { Log.w(TAG, "SiteMap is not initialized yet, skipping"); return breadcrumbs; } breadcrumbs.add(screenTitle); String currentClass = clazz; String currentTitle = screenTitle; // Look up current page's parent, if found add it to breadcrumb string list, and repeat. while (true) { final SiteMapPair pair = lookUpParent(currentClass, currentTitle); if (pair == null) { if (DEBUG_TIMING) { Log.d(TAG, "BreadCrumb timing: " + (System.currentTimeMillis() - startTime)); } return breadcrumbs; } breadcrumbs.add(0, pair.parentTitle); currentClass = pair.parentClass; currentTitle = pair.parentTitle; } } /** * Initialize a list of {@link SiteMapPair}s. Each pair knows about a single parent-child * page relationship. * * We get the knowledge of such mPairs from 2 sources: * 1. Static indexing time: we know which page(s) a parent can open by parsing its pref xml. * 2. IA: We know from {@link DashboardFeatureProvider} which page can be dynamically * injected to where. */ @VisibleForTesting @WorkerThread synchronized void init(Context context) { if (mInitialized) { // Make sure only init once. return; } final long startTime = System.currentTimeMillis(); // First load site map from static index table. final Context appContext = context.getApplicationContext(); final SQLiteDatabase db = IndexDatabaseHelper.getInstance(appContext).getReadableDatabase(); Cursor sitemap = db.query(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, SITE_MAP_COLUMNS, null, null, null, null, null); while (sitemap.moveToNext()) { final SiteMapPair pair = new SiteMapPair( sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_CLASS)), sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_TITLE)), sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_CLASS)), sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_TITLE))); mPairs.add(pair); } sitemap.close(); // Then prepare a local map that contains class name -> screen title mapping. This is needed // to figure out the display name for any fragment if it's injected dynamically through IA. final Map classToTitleMap = new ArrayMap<>(); final Cursor titleQuery = db.query(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, CLASS_TO_SCREEN_TITLE_COLUMNS, null, null, null, null, null); while (titleQuery.moveToNext()) { classToTitleMap.put( titleQuery.getString(titleQuery.getColumnIndex(IndexColumns.CLASS_NAME)), titleQuery.getString(titleQuery.getColumnIndex(IndexColumns.SCREEN_TITLE))); } titleQuery.close(); // Loop through all IA categories and pages and build additional SiteMapPairs List categories = FeatureFactory.getFactory(context) .getDashboardFeatureProvider(context).getAllCategories(); for (DashboardCategory category : categories) { // Find the category key first. final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key); if (parentClass == null) { continue; } // Use the key to look up parent (which page hosts this key) final String parentName = classToTitleMap.get(parentClass); if (parentName == null) { continue; } // Build parent-child mPairs for all children listed under this key. for (Tile tile : category.getTiles()) { final String childTitle = tile.title.toString(); String childClass = null; if (tile.metaData != null) { childClass = tile.metaData.getString( SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); } if (childClass == null) { continue; } mPairs.add(new SiteMapPair(parentClass, parentName, childClass, childTitle)); } } // Done. mInitialized = true; if (DEBUG_TIMING) { Log.d(TAG, "Init timing: " + (System.currentTimeMillis() - startTime)); } } @WorkerThread private SiteMapPair lookUpParent(String clazz, String title) { for (SiteMapPair pair : mPairs) { if (TextUtils.equals(pair.childClass, clazz) && TextUtils.equals(title, pair.childTitle)) { return pair; } } return null; } /** * Data model for a parent-child page pair. */ private static class SiteMapPair { public final String parentClass; public final String parentTitle; public final String childClass; public final String childTitle; public SiteMapPair(String parentClass, String parentTitle, String childClass, String childTitle) { this.parentClass = parentClass; this.parentTitle = parentTitle; this.childClass = childClass; this.childTitle = childTitle; } } }