Android TV 应用可搜索性实现指南:Content Provider 搜索建议、searchable.xml 配置与详情页深链接
文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载Android TV 通过系统级搜索界面从已安装应用中检索内容数据并把结果呈现给用户。要让应用的内容出现在 TV 主屏幕的搜索结果中应用需要实现一个对外提供搜索建议的 Content Provider、一份描述内容提供者与搜索配置的searchable.xml文件以及一个处理用户点击建议后 Intent 的 Activity。本文基于仓库内 tv/discovery/searchable.md 课程结合 Android Leanback 示例应用Videos by Google的代码完整讲解从「识别列 → 提供建议数据 → 配置 searchable.xml → 处理搜索术语 → 详情页深链接」的整条链路读完即可在自己的 Android TV 媒体应用中接入全局搜索。图 1详情页为 Videos by Google (Leanback) 示例应用显示一个深链接Available On 按钮。图为短片《Sintel》© Blender Foundation, www.sintel.org的搜索结果详情页。前置知识Android TV 的搜索机制Android TV 使用 Android 的搜索接口从安装的应用中检索内容数据并把搜索结果释放给用户。当用户在搜索框中逐字符输入时系统会生成建议搜索结果你的应用内容数据只要满足要求就能被包含在这些结果里让用户即时访问应用中的内容。为此应用必须做三件事实现一个 Content Provider在用户输入字符时为搜索框提供建议数据提供一份searchable.xml配置文件描述 Content Provider 及其他对 Android TV 至关重要的信息提供一个处理 Intent 的 Activity当用户选中某个建议搜索结果时负责响应该 Intent。这套机制建立在 Android 常规搜索功能之上即 Adding Custom Suggestions 所描述的方案。本文讨论的代码摘自 Android Leanback 示例应用 专题的第二个主题前一篇 推荐TV内容 讲解了主屏推荐栏本篇则聚焦于搜索入口后续的 TV应用内搜索 会讲解应用内使用 Leanback 库的搜索界面。识别列把数据字段映射到 SearchManager 列SearchManager通过把期望的数据字段表示为SQLite 数据库的列来描述它们。无论你的数据采用何种格式都必须把数据字段映射到这些列上——通常是在存取内容数据的那个类里完成映射。关于构建映射类更完整的说明可参考 Building a suggestion table。SearchManager类为 Android TV 提供了多个列以下是其中比较重要的几个值描述SUGGEST_COLUMN_TEXT_1内容名字必须SUGGEST_COLUMN_TEXT_2内容的文本描述SUGGEST_COLUMN_RESULT_CARD_IMAGE图片 / 封面SUGGEST_COLUMN_CONTENT_TYPE媒体的 MIME 类型必须SUGGEST_COLUMN_VIDEO_WIDTH媒体的分辨率宽度SUGGEST_COLUMN_VIDEO_HEIGHT媒体的分辨率高度SUGGEST_COLUMN_PRODUCTION_YEAR内容的产品年份必须SUGGEST_COLUMN_DURATION媒体的时间长度搜索框架强制要求的列有三个SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_CONTENT_TYPESUGGEST_COLUMN_PRODUCTION_YEAR为什么这三列是必须的当这些列的值与 Google 服务器上其他 Provider 提供的同一内容的对应值匹配时系统就会在内容的详情视图里为你的应用提供一个深链接同时给出指向其他 Provider 应用的链接详见下文「深链接到应用的详情页」。也就是说这三列不仅驱动搜索建议本身还参与跨应用的内容匹配与聚合展示。数据库类中的列定义应用的数据类可以这样定义列示例来自 Leanback 示例应用的VideoDatabasepublic class VideoDatabase { //The columns well include in the video database table public static final String KEY_NAME SearchManager.SUGGEST_COLUMN_TEXT_1; public static final String KEY_DESCRIPTION SearchManager.SUGGEST_COLUMN_TEXT_2; public static final String KEY_ICON SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE; public static final String KEY_DATA_TYPE SearchManager.SUGGEST_COLUMN_CONTENT_TYPE; public static final String KEY_IS_LIVE SearchManager.SUGGEST_COLUMN_IS_LIVE; public static final String KEY_VIDEO_WIDTH SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH; public static final String KEY_VIDEO_HEIGHT SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT; public static final String KEY_AUDIO_CHANNEL_CONFIG SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG; public static final String KEY_PURCHASE_PRICE SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE; public static final String KEY_RENTAL_PRICE SearchManager.SUGGEST_COLUMN_RENTAL_PRICE; public static final String KEY_RATING_STYLE SearchManager.SUGGEST_COLUMN_RATING_STYLE; public static final String KEY_RATING_SCORE SearchManager.SUGGEST_COLUMN_RATING_SCORE; public static final String KEY_PRODUCTION_YEAR SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR; public static final String KEY_COLUMN_DURATION SearchManager.SUGGEST_COLUMN_DURATION; public static final String KEY_ACTION SearchManager.SUGGEST_COLUMN_INTENT_ACTION; ...这段代码清晰展示了推荐做法为SearchManager的每一个建议列起一个常量别名而不是在代码里散落魔法字符串既避免拼写错误也让列名与业务字段名的关系一目了然。除上述列外SUGGEST_COLUMN_IS_LIVE直播标志、SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG声道配置、SUGGEST_COLUMN_PURCHASE_PRICE/SUGGEST_COLUMN_RENTAL_PRICE购买/租赁价格、SUGGEST_COLUMN_RATING_STYLE/SUGGEST_COLUMN_RATING_SCORE评分体系与分数、SUGGEST_COLUMN_INTENT_ACTION自定义 Intent 动作都是 Android TV 媒体内容搜索中可用的增强字段可一并建模。构建列映射表从SearchManager列到数据字段建立映射时还必须为每行指定唯一的_ID... private static HashMap buildColumnMap() { HashMap map new HashMap(); map.put(KEY_NAME, KEY_NAME); map.put(KEY_DESCRIPTION, KEY_DESCRIPTION); map.put(KEY_ICON, KEY_ICON); map.put(KEY_DATA_TYPE, KEY_DATA_TYPE); map.put(KEY_IS_LIVE, KEY_IS_LIVE); map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH); map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT); map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG); map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE); map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE); map.put(KEY_RATING_STYLE, KEY_RATING_STYLE); map.put(KEY_RATING_SCORE, KEY_RATING_SCORE); map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR); map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION); map.put(KEY_ACTION, KEY_ACTION); map.put(BaseColumns._ID, rowid AS BaseColumns._ID); map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, rowid AS SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, rowid AS SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); return map; } ...注意其中对SUGGEST_COLUMN_INTENT_DATA_ID的映射它指向的是 URI 中指向本行特有内容的那一部分——即描述内容存储位置的 URI 末尾片段。URI 的前半部分如果对表中所有行都相同就在searchable.xml中通过android:searchSuggestIntentData属性设置见下文「处理搜索建议」。rowid AS _ID这种写法把 SQLite 的内部rowid直接暴露为_ID与SUGGEST_COLUMN_INTENT_DATA_ID保证每行都有稳定唯一的标识。每行 URI 不同的情况如果 URI 的第一部分对表中每一行都不同则改用SUGGEST_COLUMN_INTENT_DATA字段承载整条 URI。当用户选择该内容时系统启动的 Intent 会携带组合后的 intent data由SUGGEST_COLUMN_INTENT_DATA_ID与android:searchSuggestIntentData属性或SUGGEST_COLUMN_INTENT_DATA字段值之一共同构成。换句话说URI 公共前缀固定→ 写在searchable.xml的android:searchSuggestIntentData行内只存后缀SUGGEST_COLUMN_INTENT_DATA_IDURI 每行都不同→ 直接整条放进SUGGEST_COLUMN_INTENT_DATA列。提供搜索建议数据Content Provider 的 query()实现一个 Content Provider 向 Android TV 搜索框返回搜索术语建议。系统每输入一个字符就会通过调用query()方法向你的 Provider 查询建议。在query()的实现中你的 Provider 检索建议数据并返回一个指向你所指定建议行的CursorOverride public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Use the UriMatcher to see what kind of query we have and format the db query accordingly switch (URI_MATCHER.match(uri)) { case SEARCH_SUGGEST: Log.d(TAG, search suggest: selectionArgs[0] URI: uri); if (selectionArgs null) { throw new IllegalArgumentException( selectionArgs must be provided for the Uri: uri); } return getSuggestions(selectionArgs[0]); default: throw new IllegalArgumentException(Unknown Uri: uri); } } private Cursor getSuggestions(String query) { query query.toLowerCase(); String[] columns new String[]{ BaseColumns._ID, VideoDatabase.KEY_NAME, VideoDatabase.KEY_DESCRIPTION, VideoDatabase.KEY_ICON, VideoDatabase.KEY_DATA_TYPE, VideoDatabase.KEY_IS_LIVE, VideoDatabase.KEY_VIDEO_WIDTH, VideoDatabase.KEY_VIDEO_HEIGHT, VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG, VideoDatabase.KEY_PURCHASE_PRICE, VideoDatabase.KEY_RENTAL_PRICE, VideoDatabase.KEY_RATING_STYLE, VideoDatabase.KEY_RATING_SCORE, VideoDatabase.KEY_PRODUCTION_YEAR, VideoDatabase.KEY_COLUMN_DURATION, VideoDatabase.KEY_ACTION, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID }; return mVideoDatabase.getWordMatch(query, columns); } ...关键实现细节UriMatcher 分流先用URI_MATCHER.match(uri)判断查询类型只有匹配到SEARCH_SUGGEST分支时才走建议查询未知 URI 直接抛出IllegalArgumentException参数防御搜索建议的查询必须携带selectionArgs示例中显式检查并抛出异常提示避免空指针大小写归一化query.toLowerCase()保证搜索不区分大小写返回完整列集getSuggestions()返回的列包含前面buildColumnMap()定义的几乎全部字段以及SUGGEST_COLUMN_INTENT_DATA_ID底层由mVideoDatabase.getWordMatch(query, columns)执行模糊匹配查询。在 Manifest 中声明 Provider在 Manifest 文件中Content Provider 享受特殊处理它不被声明为activity而是provider。Provider 需要android:searchSuggestAuthority属性在searchable.xml中体现来告诉系统内容提供者的命名空间同时必须把android:exported设为true这样 Android 全局搜索才能使用它返回的结果provider android:namecom.example.android.tvleanback.VideoContentProvider android:authoritiescom.example.android.tvleanback android:exportedtrue /安全提示android:exportedtrue意味着任何应用都能调用该 Provider因此务必在query()中按 URI 严格分流只暴露搜索建议相关路径不要在这个 Provider 上暴露不受控的写操作。处理搜索建议配置 searchable.xml应用必须包含一个res/xml/searchable.xml文件来配置搜索建议设置。它通过android:searchSuggestAuthority属性告诉系统 Content Provider 的命名空间该值必须与AndroidManifest.xml中provider元素的android:authorities属性字符串完全一致。searchable.xml还必须包含值为android.intent.action.VIEW的android:searchSuggestIntentAction用于定义提供自定义建议指某条具体内容时的 Intent 动作。这与提供搜索术语整串查询文本的 Intent 动作不同——后者是android.intent.action.SEARCH将在下一节说明。其他声明方式可参考 Declaring the intent action。与 Intent 动作一起应用还必须通过android:searchSuggestIntentData属性提供 intent data。这是指向内容的 URI 的第一部分描述映射表里该内容所有行共同的那段 URI每行特有的部分由上文「识别列」中介绍的SUGGEST_COLUMN_INTENT_DATA_ID字段建立。其他声明方式参考 Declaring the intent data。另外android:searchSuggestSelection ?属性指定一个值该值作为query()方法的selection参数传入方法中的问号?占位符会被替换为查询文本。例如searchSuggestSelection ?配合 SQL 模板... WHERE KEY_NAME ?系统把用户输入自动绑定到问号处既安全又高效。最后还必须把android:includeInGlobalSearch属性设为true应用内容才会出现在 Android TV 的全局搜索中。完整的searchable.xml示例searchable xmlns:androidhttp://schemas.android.com/apk/res/android android:labelstring/search_label android:hintstring/search_hint android:searchSettingsDescriptionstring/settings_description android:searchSuggestAuthoritycom.example.android.tvleanback android:searchSuggestIntentActionandroid.intent.action.VIEW android:searchSuggestIntentDatacontent://com.example.android.tvleanback/video_database_leanback android:searchSuggestSelection ? android:searchSuggestThreshold1 android:includeInGlobalSearchtrue /searchable各属性作用小结属性作用android:label/android:hint搜索框显示的应用名与提示文案字符串资源android:searchSettingsDescription搜索设置的说明文字android:searchSuggestAuthorityContent Provider 的命名空间必须与 Manifest 中android:authorities一致android:searchSuggestIntentAction选中建议内容时的 Intent 动作如android.intent.action.VIEWandroid:searchSuggestIntentData内容 URI 的公共前缀部分android:searchSuggestSelection传入query()的selection模板?会被查询文本替换android:searchSuggestThreshold触发建议所需的最小输入字符数示例为 1即输入一个字符即开始建议android:includeInGlobalSearch是否纳入系统全局搜索必须为true处理搜索术语ACTION_SEARCH 与 Manifest 声明一旦搜索框中的字词与应用的某一列值见上文「识别列」匹配系统就会触发ACTION_SEARCHIntent。应用中处理该 Intent 的 Activity 会在仓库中检索包含给定字词的列并返回一批内容条目列表。在AndroidManifest.xml中像这样指定处理ACTION_SEARCH的 Activity... activity android:namecom.example.android.tvleanback.DetailsActivity android:exportedtrue !-- Receives the search request. -- intent-filter action android:nameandroid.intent.action.SEARCH / !-- No category needed, because the Intent will specify this class component -- /intent-filter !-- Points to searchable meta data. -- meta-data android:nameandroid.app.searchable android:resourcexml/searchable / /activity ... !-- Provides search suggestions for keywords against video meta data. -- provider android:namecom.example.android.tvleanback.VideoContentProvider android:authoritiescom.example.android.tvleanback android:exportedtrue / ...这段 Manifest 同时完成了三件事注册搜索接收者DetailsActivity声明android.intent.action.SEARCH的intent-filter作为全局搜索框查询入口注释也点明无需 category因为 Intent 会显式指定组件绑定搜索配置通过meta-data android:nameandroid.app.searchable android:resourcexml/searchable /把 Activity 与res/xml/searchable.xml关联起来系统据此知道搜索框配置与建议 Provider 的地址再次声明 Providerprovider元素的android:authorities必须与searchable.xml中的android:searchSuggestAuthority完全一致这是全局搜索能定位到建议数据源的前提。Activity 内如何消费搜索文本进入该 Activity 后调用getIntent()取出触发它的 Intent用intent.getStringExtra(SearchManager.QUERY)读取用户输入的搜索词再据此执行内容检索并渲染结果列表。这与仓库中 ux/app-indexing/deep-linking.md 描述的在onCreate()/onStart()中尽早读取getAction()、getData()的深度链接处理思路一致。深链接到应用的详情页如果已经按照「处理搜索建议」一节配置了搜索并按「识别列」一节映射了SUGGEST_COLUMN_TEXT_1、SUGGEST_COLUMN_CONTENT_TYPE、SUGGEST_COLUMN_PRODUCTION_YEAR三个字段那么当用户选中某个搜索结果时详情页中就会出现一个指向你应用内观看watch动作的深链接如图 1 所示。当用户点击详情页中标识你应用的Available On按钮时系统会启动处理ACTION_VIEW的 Activity——这个动作正是searchable.xml中android:searchSuggestIntentAction所设置的android.intent.action.VIEW。也可以设置自定义 Intent 来启动你的 Activity这一点在 Android Leanback 示例应用中有演示。注意示例应用启动它自己的LeanbackDetailsFragment来显示所选媒体的详情但更好的实践是直接启动播放该媒体的 Activity从而替用户省去一两次额外的点击。从源码结构看这套「建议列匹配 → 详情页深链接」的闭环依赖前文三处配置的严格一致searchable.xml的android:searchSuggestAuthority Manifestprovider的android:authoritiessearchable.xml的android:searchSuggestIntentAction 详情 Activity 处理的 Intent 动作内容表中SUGGEST_COLUMN_TEXT_1、SUGGEST_COLUMN_CONTENT_TYPE、SUGGEST_COLUMN_PRODUCTION_YEAR三列必须真实填充——它们既是建议列表的显示字段也是 Google 服务跨 Provider 匹配同一内容的比对键。小结与实施清单要让 TV 应用在主屏幕全局搜索中可被发现按以下顺序落地即可建模列在数据库/数据访问类中用SearchManager.SUGGEST_COLUMN_*常量定义列名并构建含_ID、SUGGEST_COLUMN_INTENT_DATA_ID的映射表实现 Provider在query()中通过 UriMatcher 识别SEARCH_SUGGEST分支toLowerCase()归一化后返回建议Cursor在 Manifest 中声明provider并设android:exportedtrue配置搜索编写res/xml/searchable.xml保证android:searchSuggestAuthority与 Manifest 一致设置android:searchSuggestIntentActionandroid.intent.action.VIEW、android:searchSuggestIntentData、android:searchSuggestSelection ?与android:includeInGlobalSearchtrue注册搜索入口为处理搜索的 Activity 声明android.intent.action.SEARCH的 intent-filter并通过android.app.searchablemeta-data 指向xml/searchable打通详情深链接确保三个必填列有值点击详情页 Available On 时启动ACTION_VIEW的 Activity建议直达播放页。本文是 帮助用户在 TV 上找到内容 专题的组成部分主屏推荐栏由 推荐TV内容 实现本篇实现全局搜索入口应用内搜索界面则可进一步参考 TV应用内搜索使用 Leanback 库的SearchFragment与BrowseFragment的setOnSearchClickedListener。赞分享文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载相关推荐Open edX 部署实战从本地跑通到上线的 5 步路径外加 4 个高频坑Open edX 部署实战从本地跑通到上线的 5 步路径外加 4 个高频坑 刚克隆 Open edX 源码的人经常卡在第一步仓库里有 lms/ 和 cm后端教育AutoBangumi 搜索源设置Search Provider完整指南WebUI 种子搜索、订阅与自定义搜索源配置AutoBangumi 搜索源设置Search Provider完整指南WebUI 种子搜索、订阅与自定义搜索源配置 搜索源Search Provide后端前端音视频3分钟上手AMD Qwen2.5-VL-7B-Instruct-da8w8-torchao-v0.17.0vLLM快速部署指南与示例代码3分钟上手AMD Qwen2.5 VL 7B Instruct da8w8 torchao v0.17.0vLLM快速部署指南与示例代码 AMD Qwen2.上一篇Telegram中文群组安全指南Jqs7Bot教你避免广告骚扰与恶意链接下一篇drizzle-orm-pg 0.15.1PostgreSQL Schema模式完整支持与使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考