1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.dashboard; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.support.annotation.VisibleForTesting; 23 import android.support.annotation.WorkerThread; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import com.android.settings.SettingsActivity; 28 import com.android.settings.overlay.FeatureFactory; 29 import com.android.settings.search.IndexDatabaseHelper; 30 import com.android.settings.search.IndexDatabaseHelper.IndexColumns; 31 import com.android.settings.search.IndexDatabaseHelper.SiteMapColumns; 32 import com.android.settingslib.drawer.DashboardCategory; 33 import com.android.settingslib.drawer.Tile; 34 35 import java.util.ArrayList; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 40 import static com.android.settings.dashboard.DashboardFragmentRegistry.CATEGORY_KEY_TO_PARENT_MAP; 41 42 /** 43 * A manager class that maintains a "site map" and look up breadcrumb for a certain page on demand. 44 * <p/> 45 * The methods on this class can only be called on a background thread. 46 */ 47 public class SiteMapManager { 48 49 private static final String TAG = "SiteMapManager"; 50 private static final boolean DEBUG_TIMING = false; 51 52 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 53 public static final String[] SITE_MAP_COLUMNS = { 54 SiteMapColumns.PARENT_CLASS, 55 SiteMapColumns.PARENT_TITLE, 56 SiteMapColumns.CHILD_CLASS, 57 SiteMapColumns.CHILD_TITLE 58 }; 59 60 private static final String[] CLASS_TO_SCREEN_TITLE_COLUMNS = { 61 IndexColumns.CLASS_NAME, 62 IndexColumns.SCREEN_TITLE, 63 }; 64 65 private final List<SiteMapPair> mPairs = new ArrayList<>(); 66 67 private boolean mInitialized; 68 69 /** 70 * Given a fragment class name and its screen title, build a breadcrumb from Settings root to 71 * this screen. 72 * <p/> 73 * Not all screens have a full breadcrumb path leading up to root, it's because either some 74 * page in the breadcrumb path is not indexed, or it's only reachable via search. 75 */ 76 @WorkerThread buildBreadCrumb(Context context, String clazz, String screenTitle)77 public synchronized List<String> buildBreadCrumb(Context context, String clazz, 78 String screenTitle) { 79 init(context); 80 final long startTime = System.currentTimeMillis(); 81 final List<String> breadcrumbs = new ArrayList<>(); 82 if (!mInitialized) { 83 Log.w(TAG, "SiteMap is not initialized yet, skipping"); 84 return breadcrumbs; 85 } 86 breadcrumbs.add(screenTitle); 87 String currentClass = clazz; 88 String currentTitle = screenTitle; 89 // Look up current page's parent, if found add it to breadcrumb string list, and repeat. 90 while (true) { 91 final SiteMapPair pair = lookUpParent(currentClass, currentTitle); 92 if (pair == null) { 93 if (DEBUG_TIMING) { 94 Log.d(TAG, "BreadCrumb timing: " + (System.currentTimeMillis() - startTime)); 95 } 96 return breadcrumbs; 97 } 98 breadcrumbs.add(0, pair.parentTitle); 99 currentClass = pair.parentClass; 100 currentTitle = pair.parentTitle; 101 } 102 } 103 104 /** 105 * Initialize a list of {@link SiteMapPair}s. Each pair knows about a single parent-child 106 * page relationship. 107 * 108 * We get the knowledge of such mPairs from 2 sources: 109 * 1. Static indexing time: we know which page(s) a parent can open by parsing its pref xml. 110 * 2. IA: We know from {@link DashboardFeatureProvider} which page can be dynamically 111 * injected to where. 112 */ 113 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 114 @WorkerThread init(Context context)115 synchronized void init(Context context) { 116 if (mInitialized) { 117 // Make sure only init once. 118 return; 119 } 120 final long startTime = System.currentTimeMillis(); 121 // First load site map from static index table. 122 final Context appContext = context.getApplicationContext(); 123 final SQLiteDatabase db = IndexDatabaseHelper.getInstance(appContext).getReadableDatabase(); 124 Cursor sitemap = db.query(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, SITE_MAP_COLUMNS, null, 125 null, null, null, null); 126 while (sitemap.moveToNext()) { 127 final SiteMapPair pair = new SiteMapPair( 128 sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_CLASS)), 129 sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_TITLE)), 130 sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_CLASS)), 131 sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_TITLE))); 132 mPairs.add(pair); 133 } 134 sitemap.close(); 135 136 // Then prepare a local map that contains class name -> screen title mapping. This is needed 137 // to figure out the display name for any fragment if it's injected dynamically through IA. 138 final Map<String, String> classToTitleMap = new HashMap<>(); 139 final Cursor titleQuery = db.query(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, 140 CLASS_TO_SCREEN_TITLE_COLUMNS, null, null, null, null, null); 141 while (titleQuery.moveToNext()) { 142 classToTitleMap.put( 143 titleQuery.getString(titleQuery.getColumnIndex(IndexColumns.CLASS_NAME)), 144 titleQuery.getString(titleQuery.getColumnIndex(IndexColumns.SCREEN_TITLE))); 145 } 146 titleQuery.close(); 147 148 // Loop through all IA categories and pages and build additional SiteMapPairs 149 List<DashboardCategory> categories = FeatureFactory.getFactory(context) 150 .getDashboardFeatureProvider(context).getAllCategories(); 151 152 for (DashboardCategory category : categories) { 153 // Find the category key first. 154 final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key); 155 if (parentClass == null) { 156 continue; 157 } 158 // Use the key to look up parent (which page hosts this key) 159 final String parentName = classToTitleMap.get(parentClass); 160 if (parentName == null) { 161 continue; 162 } 163 // Build parent-child mPairs for all children listed under this key. 164 for (Tile tile : category.tiles) { 165 final String childTitle = tile.title.toString(); 166 String childClass = null; 167 if (tile.metaData != null) { 168 childClass = tile.metaData.getString( 169 SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); 170 } 171 if (childClass == null) { 172 continue; 173 } 174 mPairs.add(new SiteMapPair(parentClass, parentName, childClass, childTitle)); 175 } 176 } 177 // Done. 178 mInitialized = true; 179 if (DEBUG_TIMING) { 180 Log.d(TAG, "Init timing: " + (System.currentTimeMillis() - startTime)); 181 } 182 } 183 184 @WorkerThread lookUpParent(String clazz, String title)185 private SiteMapPair lookUpParent(String clazz, String title) { 186 for (SiteMapPair pair : mPairs) { 187 if (TextUtils.equals(pair.childClass, clazz) 188 && TextUtils.equals(title, pair.childTitle)) { 189 return pair; 190 } 191 } 192 return null; 193 } 194 195 /** 196 * Data model for a parent-child page pair. 197 */ 198 private static class SiteMapPair { 199 public final String parentClass; 200 public final String parentTitle; 201 public final String childClass; 202 public final String childTitle; 203 SiteMapPair(String parentClass, String parentTitle, String childClass, String childTitle)204 public SiteMapPair(String parentClass, String parentTitle, String childClass, 205 String childTitle) { 206 this.parentClass = parentClass; 207 this.parentTitle = parentTitle; 208 this.childClass = childClass; 209 this.childTitle = childTitle; 210 } 211 } 212 } 213