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.intelligence.search.sitemap;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.database.sqlite.SQLiteDatabase;
22 import androidx.annotation.WorkerThread;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
27 import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.SiteMapColumns;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 public class SiteMapManager {
33 
34     private static final String TAG = "SiteMapManager";
35     private static final String TOP_LEVEL_SETTINGS
36             = "com.android.settings.homepage.TopLevelSettings";
37     private static final boolean DEBUG_TIMING = false;
38 
39     public static final String[] SITE_MAP_COLUMNS = {
40             SiteMapColumns.PARENT_CLASS,
41             SiteMapColumns.PARENT_TITLE,
42             SiteMapColumns.CHILD_CLASS,
43             SiteMapColumns.CHILD_TITLE,
44             SiteMapColumns.HIGHLIGHTABLE_MENU_KEY
45     };
46 
47     private final List<SiteMapPair> mPairs = new ArrayList<>();
48 
49     private boolean mInitialized;
50 
51     /**
52      * Check whether the specified class is top level settings class.
53      */
isTopLevelSettings(String clazz)54     public static boolean isTopLevelSettings(String clazz) {
55         return TextUtils.equals(TOP_LEVEL_SETTINGS, clazz);
56     }
57 
58     /**
59      * Given a fragment class name and its screen title, build a breadcrumb from Settings root to
60      * this screen.
61      * <p/>
62      * Not all screens have a full breadcrumb path leading up to root, it's because either some
63      * page in the breadcrumb path is not indexed, or it's only reachable via search.
64      */
65     @WorkerThread
buildBreadCrumb(Context context, String clazz, String screenTitle)66     public synchronized List<String> buildBreadCrumb(Context context, String clazz,
67             String screenTitle) {
68         init(context);
69         final long startTime = System.currentTimeMillis();
70         final List<String> breadcrumbs = new ArrayList<>();
71         if (!mInitialized) {
72             Log.w(TAG, "SiteMap is not initialized yet, skipping");
73             return breadcrumbs;
74         }
75         if (!TextUtils.isEmpty(screenTitle)) {
76             breadcrumbs.add(screenTitle);
77         }
78         String currentClass = clazz;
79         String currentTitle = screenTitle;
80         // Look up current page's parent, if found add it to breadcrumb string list, and repeat.
81         while (true) {
82             final SiteMapPair pair = lookUpParent(currentClass, currentTitle);
83             if (pair == null) {
84                 if (DEBUG_TIMING) {
85                     Log.d(TAG, "BreadCrumb timing: " + (System.currentTimeMillis() - startTime));
86                 }
87                 return breadcrumbs;
88             }
89             final String parentTitle = pair.getParentTitle();
90             if (!TextUtils.isEmpty(parentTitle)) {
91                 breadcrumbs.add(0, parentTitle);
92             }
93             currentClass = pair.getParentClass();
94             currentTitle = pair.getParentTitle();
95         }
96     }
97 
getTopLevelPair(Context context, String clazz, String screenTitle)98     public synchronized SiteMapPair getTopLevelPair(Context context, String clazz,
99             String screenTitle) {
100         if (!mInitialized) {
101             init(context);
102         }
103 
104         // find the default pair
105         SiteMapPair currentPair = null;
106         if (!TextUtils.isEmpty(clazz)) {
107             for (SiteMapPair pair : mPairs) {
108                 if (TextUtils.equals(pair.getChildClass(), clazz)) {
109                     currentPair = pair;
110                     if (TextUtils.isEmpty(screenTitle)) {
111                         screenTitle = pair.getChildTitle();
112                     }
113                     break;
114                 }
115             }
116         }
117 
118         // recursively find the top level pair
119         String currentClass = clazz;
120         String currentTitle = screenTitle;
121         while (true) {
122             // look up parent by class and title
123             SiteMapPair pair = lookUpParent(currentClass, currentTitle);
124             if (pair == null) {
125                 // fallback option: look up parent only by title
126                 pair = lookUpParent(currentTitle);
127                 if (pair == null) {
128                     return currentPair;
129                 }
130             }
131             if (!TextUtils.isEmpty(pair.getHighlightableMenuKey())) {
132                 return pair;
133             }
134             currentPair = pair;
135             currentClass = pair.getParentClass();
136             currentTitle = pair.getParentTitle();
137         }
138     }
139 
140     /**
141      * Initialize a list of {@link SiteMapPair}s. Each pair knows about a single parent-child
142      * page relationship.
143      */
144     @WorkerThread
init(Context context)145     private synchronized void init(Context context) {
146         if (mInitialized) {
147             // Make sure only init once.
148             return;
149         }
150 
151         // Will init again if site map table updated, need clear the old data if it's not empty.
152         if (!mPairs.isEmpty()) {
153             mPairs.clear();
154         }
155 
156         final long startTime = System.currentTimeMillis();
157         // First load site map from static index table.
158         final Context appContext = context.getApplicationContext();
159         final SQLiteDatabase db = IndexDatabaseHelper.getInstance(appContext).getReadableDatabase();
160         Cursor sitemap = db.query(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, SITE_MAP_COLUMNS, null,
161                 null, null, null, null);
162         while (sitemap.moveToNext()) {
163             final SiteMapPair pair = new SiteMapPair(
164                     sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_CLASS)),
165                     sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_TITLE)),
166                     sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_CLASS)),
167                     sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_TITLE)),
168                     sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.HIGHLIGHTABLE_MENU_KEY))
169             );
170             mPairs.add(pair);
171         }
172         sitemap.close();
173         // Done.
174         mInitialized = true;
175         if (DEBUG_TIMING) {
176             Log.d(TAG, "Init timing: " + (System.currentTimeMillis() - startTime));
177         }
178     }
179 
180     @WorkerThread
lookUpParent(String clazz, String title)181     private SiteMapPair lookUpParent(String clazz, String title) {
182         for (SiteMapPair pair : mPairs) {
183             if (TextUtils.equals(pair.getChildClass(), clazz)
184                     && TextUtils.equals(title, pair.getChildTitle())) {
185                 return pair;
186             }
187         }
188         return null;
189     }
190 
191     @WorkerThread
lookUpParent(String title)192     private SiteMapPair lookUpParent(String title) {
193         if (TextUtils.isEmpty(title)) {
194             return null;
195         }
196         for (SiteMapPair pair : mPairs) {
197             if (TextUtils.equals(title, pair.getChildTitle())) {
198                 return pair;
199             }
200         }
201         return null;
202     }
203 
204     @WorkerThread
setInitialized(boolean initialized)205     public void setInitialized(boolean initialized) {
206         mInitialized = initialized;
207     }
208 }
209