• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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