Analysis of Android Settings Search scheme

Keywords: Android Google Database Fragment

Android development will encounter some self writing interfaces that need to be allowed to be searched, or three-party applications are attached to Settings, and users also want to be searched.
Before we know how to add, we need to understand the whole framework to better add our own code.

 

Here I sort out the whole process of how to index and load the search database data.

The Settings search interface is displayed by the search fragment. When the user clicks the search icon in the Settings home page, the search activity will be launched.

       <activity android:name=".search.SearchActivity"
                  android:label="@string/search_settings"
                  android:icon="@drawable/ic_search_24dp"
                  android:parentActivityName="Settings"
                  android:theme="@style/Theme.Settings.NoActionBar">
             <intent-filter>
                  <action android:name="com.android.settings.action.SETTINGS_SEARCH" />
                  <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
        </activity>

The first time you start Settings, the database is not actively loaded, but asynchronously when the first search occurs.

//com/android/settings/search/SearchFragment.java
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        long startTime = System.currentTimeMillis();
        setHasOptionsMenu(true);

        Log.d(TAG, "onCreate: ");
......

        final Activity activity = getActivity();
        // Run the Index update only if we have some space
        if (!Utils.isLowStorage(activity)) {
            mSearchFeatureProvider.updateIndexAsync(activity, this /* indexingCallback */);  // Create database and index
        } else {
            Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
        }
        if (SettingsSearchIndexablesProvider.DEBUG) {
            Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
        }
    }

Finally, go to a core class, DatabaseIndexingManager, which is responsible for setting all indexes associated.

The core methods are as follows:

    public void indexDatabase(IndexingCallback callback) {
        IndexingTask task = new IndexingTask(callback);
        task.execute();
    }

    /**
     * Accumulate all data and non-indexable keys from each of the content-providers.
     * Only the first indexing for the default language gets static search results - subsequent
     * calls will only gather non-indexable keys.
     */
    public void performIndexing() {
        final long startTime = System.currentTimeMillis();
        // Traverse all declarations in the query device android.content.action .SEARCH_ INDEXABLES_ ContentProvider for provider action.
        final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
        final List<ResolveInfo> providers =
                mContext.getPackageManager().queryIntentContentProviders(intent, 0);

        final String localeStr = Locale.getDefault().toString();
        final String fingerprint = Build.FINGERPRINT;
        final String providerVersionedNames =
                IndexDatabaseHelper.buildProviderVersionedNames(providers);

        final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, localeStr,
                fingerprint, providerVersionedNames);

        if (isFullIndex) {
            rebuildDatabase();
        }

        //Traverse the searchable and non searchable keys provided by SearchIndexableProvider corresponding to all customized self-write activities / fragments, and save them to the data structure UpdateData.
        for (final ResolveInfo info : providers) {
            if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
                continue;
            }
            final String authority = info.providerInfo.authority;
            final String packageName = info.providerInfo.packageName;

            Log.d(LOG_TAG, "knealq performIndexing: authority:"  + authority  + ",packageName:" + packageName + ",isFullIndex:" + isFullIndex + ", resolverInfo:" + info);
            if (isFullIndex) {
               //Query all searchable providers corresponding to all searchable keys, and save them to the data structure: UpdateData.dataToUpdate . 
                addIndexablesFromRemoteProvider(packageName, authority);
            }
            final long nonIndexableStartTime = System.currentTimeMillis();
            //Query all searchable providers (extension searchindexable providers) and save them to the data structure UpdateData.nonIndexableKeys . 
            addNonIndexablesKeysFromRemoteProvider(packageName, authority);
            if (SettingsSearchIndexablesProvider.DEBUG) {
                final long nonIndextableTime = System.currentTimeMillis() - nonIndexableStartTime;
                Log.d(LOG_TAG, "performIndexing update non-indexable for package " + packageName
                        + " took time: " + nonIndextableTime);
            }
        }
        final long updateDatabaseStartTime = System.currentTimeMillis();
        // After traversing the providers, convert the relevant index to SearchIndexableData and save it to the database (/ data/user_de/0/com.android.settings/databases/search_index.db )Medium.
        updateDatabase(isFullIndex, localeStr);
        if (SettingsSearchIndexablesProvider.DEBUG) {
            final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
            Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
        }

        //TODO(63922686): Setting indexed should be a single method, not 3 separate setters.
        IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
        IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
        IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames);

        if (SettingsSearchIndexablesProvider.DEBUG) {
            final long indexingTime = System.currentTimeMillis() - startTime;
            Log.d(LOG_TAG, "performIndexing took time: " + indexingTime
                    + "ms. Full index? " + isFullIndex);
        }
    }

First of all, you can see that the ContentProvider of the specified Action is registered through the PackageManager scanning query, and its transformation placement is assigned to a String.
A total of the following app s are registered.

We focus on com.android.settings ,

com.android.cellbroadcastreceiver:29,
com.android.emergency:29,
com.android.settings:29,
com.android.traceur:2,
com.google.android.apps.messaging:54087046,
com.google.android.apps.wellbeing:131132,
com.google.android.gms:200414038,
com.google.android.googlequicksearchbox:301068684,
com.google.android.inputmethod.latin:26881014,
com.google.android.permissioncontroller:291900801,
com.google.android.permissioncontroller:291900801,

The next step is to check whether the fullIndex meets three conditions,
That is to say, the current build fingerprint, locale environment, and the currently scanned ContentProvider have not been indexed.

In this application, there is a shared preference to keep relevant records, such as chestnuts.

AOSP:/data/user_de/0/com.android.settings/shared_prefs # cat index.xml                                                                                               
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="indexed_providers">com.android.cellbroadcastreceiver:29,com.android.emergency:29,com.android.settings:29,com.android.traceur:2,com.google.android.apps.messaging:54087046,com.google.android.apps.wellbeing:131132,com.google.android.gms:200414038,com.google.android.googlequicksearchbox:301068684,com.google.android.inputmethod.latin:26881014,com.google.android.permissioncontroller:291900801,com.google.android.permissioncontroller:291900801,</string>
    <boolean name="en_US" value="true" />
    <boolean name="Google/XXXXXXXX/AOSP:10/QP1A.190711.020/XXXXX:userdebug/release-keys" value="true" />
</map>
AOSP:/data/user_de/0/com.android.settings/shared_prefs # 

That's the judgment index.xml Whether the content inside already exists, or whether there are differences.

When fullindex is true, database and table need to be created, which is completed by IndexDatabaseHelper class.

    /**
     * Reconstruct the database in the following cases:
     * - Language has changed
     * - Build has changed
     */
    private void rebuildDatabase() {
        // Drop the database when the locale or build has changed. This eliminates rows which are
        // dynamically inserted in the old language, or deprecated settings.
        final SQLiteDatabase db = getWritableDatabase();
        IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
    }



    public void reconstruct(SQLiteDatabase db) {
        dropTables(db);
        bootstrapDB(db);
    }

    private void bootstrapDB(SQLiteDatabase db) {
        db.execSQL(CREATE_INDEX_TABLE);
        db.execSQL(CREATE_META_TABLE);
        db.execSQL(CREATE_SAVED_QUERIES_TABLE);
        db.execSQL(CREATE_SITE_MAP_TABLE);
        db.execSQL(INSERT_BUILD_VERSION);
        Log.i(TAG, "Bootstrapped database");
    }


    private void dropTables(SQLiteDatabase db) {
        clearCachedIndexed(mContext);
        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX);
        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX);
        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES);
        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP);
    }

After the database is created, first traverse all searchable and non searchable keys and save them.
Where do these data come from? We temporarily call it "general entrance" seal chIndexableResources.sResMap .

The corresponding Provider of Settings is SettingsSearchIndexableProvider, which declares interfaces such as queryXmlResources. The real data accessed is a sResMap statically initialized in the SearchIndexableResources class, which stores the SearchIndexableData information.

queryXmlResources can take the information in seMap directly.
Querynoindexablekeys finds the BaseSearchIndexProvider defined by the fragment/Activity internal static declaration through the databaseindexutilities class through the classname specified by sResMap (fragments and activities of the extensions indexable mentioned above). After finding it, call the getNonIndexableKeys (context) method given by it, which returns list < string >.

 

Take fingersettingsfragment as an example. It declares the fingersearchindexprovider, which inherits from BaseSearchIndexProvider and implements Indexable.SearchIndexProvider Interface. And statically instantiate a named search_ INDEX_ DATA_ FingerprintSearchIndexProvider for provider.

be careful:

Make a point, and use it here.

[1]
Self writing fragment and activity must be written according to the following framework.

// Step 1
public class SearchDefinedExt implements Indexable {// The classes registered to sResMap can be Activity, Fragment, or other similar tool classes, unlimited.

    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER
            = new BaseSearchIndexProvider() {  //Note that the name must be this. Override several methods you need. The DataIndexingUtils tool class uses this name to find the SearchIndexProvider.
        @Override
        public List<SearchIndexableRaw> getRawDataToIndex(Context context,
                                                          boolean enabled) {
            List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>();
            //Omit some
            return indexables;
        }

        @Override
        public List<String> getNonIndexableKeys(Context context) {
            List<String> keys = super.getNonIndexableKeys(context);
            final ArrayList<String> result = new ArrayList<String>();
            return result;
        }
    };

}


//Step 2
//Add a statement to the general entry of sResMap, which is convenient for others to find. It can be similar to the catalog of a book.

public final class SearchIndexableResources {

    static {
        Log.d("SearchIndexableResources", "static initializer: ");
        //Add what you need
        addIndex(SearchDefinedExt.class, NO_DATA_RES_ID, R.drawable.ic_settings_wireless);
    }

}

It is important to note that if step 2 is not added, the SearchIndexProvider defined in SearchDefinedExt cannot be indexed.
This "directory" index addition is important.

 

[2]

What about the application of three-way connection Settings?
Settings can't get another app resource xmlResId directly.

1) Implement your own ContentProvider and comply with Search rules, such as registration: android.content.action.SEARCH_INDEXABLES_PROVIDER action.
2) Use the SearchDefinedExt extended by Settings to add the intentAction, intenttargetpackage and intenttargetclass of your application in getRawDataToIndex() and package them into SearchIndexableRaw.

 

Returning to SearchFragment, we can see that this class is passed in when calling updateIndexAsync (this class implements IndexingCallback), that is, after the database indexingmanager index is created and the data is inserted.
A message will be sent back through callback to tell SearchFragment, and then let it update UI. UI is responsible for loading SearchResult through SearchResultsAdapter.

When SearchResultsAdapter onCreateViewHolder, the IntentSearchViewHolder or SavedQueryViewHolder will be generated according to the situation.
Where ViewHolder will register OnClickListener click event when onBind(), once clicked, it will jump to our index interface.

 

Posted by barteelamar on Mon, 22 Jun 2020 01:43:26 -0700