Android Settings 快速搜索

Settings 之 SearchIndexablesProvider
首先需要在清单文件中注册action为"android.content.action.SEARCH_INDEXABLES_PROVIDER"的provider,如下:
      <provider
            android:name=".search.SettingsSearchIndexablesProvider"
            android:authorities="com.android.settings"
            android:multiprocess="false"
            android:grantUriPermissions="true"
            android:permission="android.permission.READ_SEARCH_INDEXABLES"
            android:exported="true">
            <intent-filter>
                <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" /> //注册此 action
            </intent-filter>
        </provider>

搜索数据库路径:/data/user_de/0/com.android.settings/databases/search_index.db
//search_index.db 数据库的prefs_index表格中存放的就是搜索的设置选项
此数据库的初始化不是在开机阶段,而是在每一次打开settings或者当前切换用户(因为系统为每一个用户维护一个单独的search_index.db),或者是当前的语言发生变化会更新数据库.

数据库的初始化:
Index#update

    public void update() {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                // 查找系统中所有的配置了"android.content.action.SEARCH_INDEXABLES_PROVIDER"的Provider
                final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
                List<ResolveInfo> list =
                        mContext.getPackageManager().queryIntentContentProviders(intent, 0);

                final int size = list.size();
                for (int n = 0; n < size; n++) {
                    final ResolveInfo info = list.get(n);
                    if (!isWellKnownProvider(info)) {
                        continue;
                    }
                    final String authority = info.providerInfo.authority;
                    final String packageName = info.providerInfo.packageName;
                    //打印packageName为:
                    //01-01 17:38:09.257  5207  5511 E qcdds   : packageName= com.android.cellbroadcastreceiver
                    //01-01 17:38:09.678  5207  5511 E qcdds   : packageName= com.android.phone
                    //01-01 17:38:09.777  5207  5511 E qcdds   : packageName= com.android.settings

                    // 添加其他APP的设置项
                    addIndexablesFromRemoteProvider(packageName, authority); //主要方法
                    // 添加其他APP中不需要被搜索到的设置项
                    addNonIndexablesKeysFromRemoteProvider(packageName, authority);
                }

                mDataToProcess.fullIndex = true;
                // 上面的addIndexablesFromRemoteProvider会添加设置项到内存中的一个mDataToProcess对象里,updateInternal将该对象更新到数据库中
                updateInternal();
            }
        });
    }


    private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
        try {
            // rank是按照指定算法计算出的一个值,用来搜索的时候,展示给用户的优先级
            final int baseRank = Ranking.getBaseRankForAuthority(authority);
            // mBaseAuthority是com.android.settings,authority是其他APP的包名
            final Context context = mBaseAuthority.equals(authority) ?
                    mContext : mContext.createPackageContext(packageName, 0);
            // 构建搜索的URI
            final Uri uriForResources = buildUriForXmlResources(authority);
            // 两种添加到数据库的方式,我们以addIndexablesForXmlResourceUri为例
            addIndexablesForXmlResourceUri(context, packageName, uriForResources,
                    SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank);

            final Uri uriForRawData = buildUriForRawData(authority);
            addIndexablesForRawDataUri(context, packageName, uriForRawData,
                    SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
                    + Log.getStackTraceString(e));
            return false;
        }
     }


上面代码主要做了下面事情:

    根据当前包名创建对应包的context对象。
    根据当前包名构建指定URI,例如,settings:content://com.android.settings/settings/indexables_xml_res
    然后通过context对象查找对应的Provider的数据
    之所以构建出content://com.android.settings/settings/indexables_xml_res 这样的URI是因为所有的需要被搜索到的设置项所在的APP,其Provider都需要继承自SearchIndexablesProvider


//SearchIndexablesProvider 继承 ContentProvider
public abstract class SearchIndexablesProvider extends ContentProvider {
    ....
        //定义了查询路径
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_XML_RES_PATH,
                MATCH_RES_CODE);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_RAW_PATH,
                MATCH_RAW_CODE);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH,
                MATCH_NON_INDEXABLE_KEYS_CODE);
    ....

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {
        switch (mMatcher.match(uri)) {
            // 匹配不同的Uri进行查找
            case MATCH_RES_CODE:
                return queryXmlResources(null);
            case MATCH_RAW_CODE:
                return queryRawData(null);
            case MATCH_NON_INDEXABLE_KEYS_CODE:
                return queryNonIndexableKeys(null);
            default:
                throw new UnsupportedOperationException("Unknown Uri " + uri);
        }
    }

    @Override
    public String getType(Uri uri) {
        switch (mMatcher.match(uri)) {
            case MATCH_RES_CODE:
                return SearchIndexablesContract.XmlResource.MIME_TYPE;
            case MATCH_RAW_CODE:
                return SearchIndexablesContract.RawData.MIME_TYPE;
            case MATCH_NON_INDEXABLE_KEYS_CODE:
                return SearchIndexablesContract.NonIndexableKey.MIME_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }
    ....

}


//采用 MatrixCursor 构建虚拟的数据表
public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
    private static final String TAG = "SettingsSearchIndexablesProvider";

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor queryXmlResources(String[] projection) {
        MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
        //通过 SearchIndexableResources.values() 获取到所有添加到map集合中的 SearchIndexableResource
        /*
          SearchIndexableResource的路径为: /frameworks/base/core/java/android/provider/SearchIndexableResource.java
          其中定义了    this.rank = rank;
                        this.xmlResId = xmlResId;
                        this.className = className;
                        this.iconResId = iconResId;
          等属性
        */
        Collection<SearchIndexableResource> values = SearchIndexableResources.values(); 
        for (SearchIndexableResource val : values) {
            Object[] ref = new Object[7];
            ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
            ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
            ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
            ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
            ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = null; // intent action
            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package
            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
            cursor.addRow(ref);
        }
        return cursor;
    }

    @Override
    public Cursor queryRawData(String[] projection) {
        MatrixCursor result = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
        return result;
    }
    // 该方法返回当前布局不想被搜索到的设置项
    @Override
    public Cursor queryNonIndexableKeys(String[] projection) {
        MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
        return cursor;
    }
}



接着
//Index#addIndexablesForXmlResourceUri

private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
            Uri uri, String[] projection, int baseRank) {
        // 获取指定包对应的ContentResolver
        final ContentResolver resolver = packageContext.getContentResolver();
        final Cursor cursor = resolver.query(uri, projection, null, null, null);

        if (cursor == null) {
            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
            return;
        }

        try {
            final int count = cursor.getCount();
            if (count > 0) {
                while (cursor.moveToNext()) {
                    final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;

                    final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);

                    final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
                    final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);

                    final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
                    final String targetPackage = cursor.getString(
                            COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
                    final String targetClass = cursor.getString(
                            COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);

                    SearchIndexableResource sir = new SearchIndexableResource(packageContext);
                    sir.rank = rank;
                    sir.xmlResId = xmlResId;
                    sir.className = className;
                    sir.packageName = packageName;
                    sir.iconResId = iconResId;
                    sir.intentAction = action;
                    sir.intentTargetPackage = targetPackage;
                    sir.intentTargetClass = targetClass;
                    // 解析cursor数据,并且添加到内存UpdateData的dataToUpdate属性上, dataToUpdate属性是一个list集合
                    addIndexableData(sir);
                }
            }
        } finally {
            cursor.close();
        }
}



public void addIndexableData(SearchIndexableData data) {
        synchronized (mDataToProcess) {
            mDataToProcess.dataToUpdate.add(data);
        }
}

//Index#updateInternal更新到数据库中
private void updateInternal() {
        synchronized (mDataToProcess) {
            final UpdateIndexTask task = new UpdateIndexTask();
            // 拷贝一个mDataToProcess对象的副本,前面将数据添加到mDataToProcess对象中。
            UpdateData copy = mDataToProcess.copy();
            // 执行UpdateIndexTask,UpdateIndexTask会将copy对象保存到数据库里
            task.execute(copy);
            mDataToProcess.clear();
        }
}


//接下来的调用流程为:
Index$UpdateIndexTask
  doInBackground --> 
      processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, forceUpdate) --> // 插入或者更新当前数据库内容   
          indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys) -->  // 继续indexOneSearchIndexableData更新数据库



private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
            SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
        //两种方式添加数据库
        if (data instanceof SearchIndexableResource) {
            indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
        } else if (data instanceof SearchIndexableRaw) {
            indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
        }
}
        接下来调用:
        indexFromResource() -->  //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled) 获取当前布局不想被搜索到的设置项
                indexFromProvider() --> //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled)  需要解析的布局
                      indexFromResource() -->  //使用XmlResourceParser解析xml布局
                            updateOneRowWithFilteredData() --> 
                                  updateOneRow() -->  //此方法最终将解析的数据更新至数据库

//在settings中添加搜索项:

在SearchIndexableResources 中维护了一个sResMap,其中添加了所有的SearchIndexableResource,每一个子页面对应一个SearchIndexableResource,并且在SearchIndexableResources 提供了一个values()方
法,用来返回当前集合中的所有数据,其实SearchIndexableResources.values()是在SettingsSearchIndexablesProvider中用到的,SettingsSearchIndexablesProvider重写了queryXmlResources方法,并且通过SearchIndexableResources.values()会返回setting中所有的子页面,最后封装成一个cursor.

在SearchIndexableResources的静态代码块中初始化了:
    static {
        sResMap.put(WifiSettings.class.getName(),
                new SearchIndexableResource(
                        Ranking.getRankForClassName(WifiSettings.class.getName()),
                        NO_DATA_RES_ID,
                        WifiSettings.class.getName(),
                        R.drawable.ic_settings_wireless));
    
        sResMap.put(SavedAccessPointsWifiSettings.class.getName(),
                new SearchIndexableResource(
                        Ranking.getRankForClassName(SavedAccessPointsWifiSettings.class.getName()),
                        R.xml.wifi_display_saved_access_points,
                        SavedAccessPointsWifiSettings.class.getName(),
                        R.drawable.ic_settings_wireless));

    }
两种方式,一种是直接在new SearchIndexableResource() 时传入布局文件,另一种为"NO_DATA_RES_ID"表示此搜索项匹配没有需要解析的xml文件,此xml的解析在Index.java中,
采用第二种时,需要在对应的类中创建一个SEARCH_INDEX_DATA_PROVIDER,类型为SearchIndexProvider,继承BaseSearchIndexProvider并复写其两个方法: 
getXmlResourcesToIndex 和 getNonIndexableKeys.

以SecuritySettings为例:
    /**
     * For Search. Please keep it in sync when updating "createPreferenceHierarchy()"
     */
    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new SecuritySearchIndexProvider();

    private static class SecuritySearchIndexProvider extends BaseSearchIndexProvider {

        @Override
        public List<SearchIndexableResource> getXmlResourcesToIndex(
                Context context, boolean enabled) {
            final List<SearchIndexableResource> index = new ArrayList<SearchIndexableResource>();
            //返回需要解析的布局
            index.add(getSearchResource(context, R.xml.security_settings_misc));
            return index;
        }

        @Override
        public List<String> getNonIndexableKeys(Context context) {
            // 该方法返回当前布局不想被搜索到的设置项
            final List<String> keys = new ArrayList<String>();
            LockPatternUtils lockPatternUtils = new LockPatternUtils(context);

            // Do not display SIM lock for devices without an Icc card
            final UserManager um = UserManager.get(context);
            final TelephonyManager tm = TelephonyManager.from(context);
            if (!um.isAdminUser() || !tm.hasIccCard()) {
                keys.add(KEY_SIM_LOCK);
            }
            if (um.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) {
                keys.add(KEY_CREDENTIALS_MANAGER);
            }

            // TrustAgent settings disappear when the user has no primary security.
            if (!lockPatternUtils.isSecure(MY_USER_ID)) {
                // keys.add(KEY_TRUST_AGENT); //Move To LockScreen Settings
                keys.add(KEY_MANAGE_TRUST_AGENTS);
            }

            return keys;
        }



在搜索过程中,会从数据库中查找匹配,点击筛选结果,根据className启动对应的界面,代码实现在SearchResultsSummary类中:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        final View view = inflater.inflate(R.layout.search_panel, container, false);

        mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
        mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);

        mResultsListView = (ListView) view.findViewById(R.id.list_results);
        mResultsListView.setAdapter(mResultsAdapter);
        mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { // mResultsListView就是查询的结果列表
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                // We have a header, so we need to decrement the position by one
                position--;

                // Some Monkeys could create a case where they were probably clicking on the
                // List Header and thus the position passed was "0" and then by decrement was "-1"
                if (position < 0) {
                    return;
                }

                final Cursor cursor = mResultsAdapter.mCursor;
                cursor.moveToPosition(position);

                final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); 
                final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE);
                final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION);
                final String key = cursor.getString(Index.COLUMN_INDEX_KEY);

                final SettingsActivity sa = (SettingsActivity) getActivity();
                sa.needToRevertToInitialFragment();
                if (TextUtils.isEmpty(action)) {
                    Bundle args = new Bundle();
                    args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);

                    Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); // 通过className启动Settings中对应的Fragment界面
                } else {
                    final Intent intent = new Intent(action);

                    final String targetPackage = cursor.getString(
                            Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
                    final String targetClass = cursor.getString(
                            Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS);
                    if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
                        final ComponentName component =
                                new ComponentName(targetPackage, targetClass);
                        intent.setComponent(component);
                    }
                    intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);

                    sa.startActivity(intent);
                }

                saveQueryToDatabase();
            }
        });

    }

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值