When the dashboard summary is being initialized, it will rebuild the UI while the summary loader tries to to go through the tiles to update the summary. Both is being done on a separate backgroud thread, and it will run into concurrent modification issue if the thread is being swapped while one is looping through the list. Instead of letting clients access the list of tiles directly, add a getter method in DashboardCategory to get a copy of the list of tiles for all read-only operations. Change-Id: I479669abd8d1d0a8ee9a4113d8ad2244da56f4d8 Fixes: 69677575 Test: make RunSettingsRoboTests
206 lines
8.2 KiB
Java
206 lines
8.2 KiB
Java
/*
|
|
* 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.
|
|
* <p/>
|
|
* 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<SiteMapPair> mPairs = new ArrayList<>();
|
|
|
|
private boolean mInitialized;
|
|
|
|
/**
|
|
* Given a fragment class name and its screen title, build a breadcrumb from Settings root to
|
|
* this screen.
|
|
* <p/>
|
|
* 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<String> buildBreadCrumb(Context context, String clazz,
|
|
String screenTitle) {
|
|
init(context);
|
|
final long startTime = System.currentTimeMillis();
|
|
final List<String> 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<String, String> 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<DashboardCategory> 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;
|
|
}
|
|
}
|
|
}
|