安卓 Kotlin 高级教程(一)
一、系统
Android 操作系统诞生于 2003 年的 Android Inc .公司,后来于 2005 年被谷歌公司收购。第一款运行安卓系统的设备于 2008 年上市。自那以来,它已经进行了多次更新,2018 年初的最新版本号为 8.1。
自从第一次构建以来,Android 操作系统的市场份额一直在不断增加,据说到 2018 年,它的市场份额超过了 80%。尽管这些数字因你使用的来源不同而不同,但 Android 操作系统的成功是不可否认的。这一胜利部分源于谷歌公司是全球智能手机市场上的一个聪明的参与者,但也源于 Android 操作系统经过精心定制,以满足智能手机和其他手持或类似手持设备的需求。
大多数以前或仍然在 PC 环境中工作的计算机开发人员会在完全忽视手持设备开发的情况下做不好工作,本书的目标是帮助开发人员理解 Android OS 并掌握其程序的开发。这本书还专注于使用 Kotlin 作为一种语言来实现开发需求,但首先我们将看看 Android 操作系统和辅助开发相关系统,让您了解 Android 的内部功能。
安卓操作系统
Android 基于一个特别定制的 Linux 内核。这个内核提供了处理硬件、程序执行环境和低级通信通道所需的所有低级驱动程序。
在内核之上,你会发现 Android 运行时 (ART)和几个用 c 编写的底层库。后者是应用相关库和内核之间的粘合剂。Android 运行时是 Android 程序运行的执行引擎。
作为开发人员,您几乎不需要了解这些底层库和 Android 运行时如何工作的细节,但是您将使用它们来完成基本的编程任务,例如处理音频子系统或数据库。
在底层库和 Android 运行时之上是应用框架,它定义了你为 Android 构建的任何应用的外部结构。它处理活动、GUI 部件、通知、资源等等。虽然理解底层库肯定有助于你编写好的程序,但是了解应用框架对于编写任何 Android 应用都是必不可少的。
最重要的是,你会发现你的用户为他们必须完成的任务而启动的应用。见图 1-1 。
图 1-1
安卓操作系统
作为开发人员,你将使用 Kotlin、Java 或 C++ 作为编程语言,或者它们的组合来创建 Android 应用。您将使用应用框架和库与 Android 操作系统和硬件对话。使用 C++ 作为较低层次的编程语言,解决目标体系结构的特殊性,导致合并了本地开发工具包 (NDK),这是 Android SDK 的可选部分。虽然出于特殊目的,可能有必要使用 NDK,但在大多数情况下,处理另一种语言及其带来的特殊挑战的额外努力并没有得到回报。因此,在本书中,我们将主要讨论 Kotlin,有时也会适当地讨论 Java。
开发系统
运行在手持设备上的操作系统是故事的一部分;作为开发者,你也需要一个创建 Android 应用的系统。后者发生在 PC 或笔记本电脑上,你使用的软件套件是 Android Studio 。
Android Studio 是您用于开发的 IDE,但是在您安装和操作它的同时,软件开发工具包(参见“SDK”)也会被安装,我们将在下面的部分中讨论这两者。我们还将讨论虚拟设备,它为在各种目标设备上测试你的应用提供了宝贵的帮助。
Android Studio
Android Studio IDE 是创建和运行 Android 应用的专用开发环境。图 1-2 显示了它的主窗口和仿真器视图。
Android Studio 提供以下功能:
图 1-2
Android Studio
-
管理 Kotlin、Java 和 C++ (NDK)的程序源
-
管理项目资源
-
在模拟器或连接的真实设备中测试运行应用的能力
-
更多测试工具
-
调试设备
-
性能和内存分析器
-
代码检查
-
用于构建本地或可发布应用的工具
studio 和在线资源中包含的帮助为掌握 Android Studio 提供了足够的信息。在本书中,我们将偶尔在专门的章节中讨论它。
虚拟设备
开发计算机软件总是包括创建一个能够处理所有可能的目标系统的程序的挑战。如今手持设备以如此多的不同形式出现,这一方面变得比以往任何时候都更加重要。你有尺寸在 3.9 英寸到 5.4 英寸及以上的智能手机设备,7 英寸到 14 英寸及以上的平板电脑,可穿戴设备,不同尺寸的电视等等,都运行 Android 操作系统。
当然,作为开发人员,您不可能购买覆盖所有可能尺寸的所有设备。这就是模拟器派上用场的地方。有了模拟器,你不必购买硬件,你仍然可以开发 Android 应用。
Android Studio 让您可以轻松使用仿真器开发和测试应用,使用软件开发套件中的工具,您甚至可以从 Android Studio 外部操作仿真器。
警告
你可以开发应用,而不需要拥有一个真正的设备。但是,不建议这样做。你应该至少有一部上一代的智能手机,如果你买得起的话,也许还有一部平板电脑。原因是与模拟器相比,操作真实设备的感觉不同。物理处理不是 100%相同,性能也不同。
要从 Android Studio 内部管理虚拟设备,请通过工具➤Android➤avd 管理器打开 Android 虚拟设备管理器。在这里,您可以调查、更改、创建、删除和启动虚拟设备。见图 1-3 。
图 1-3
AVD 经理
创建新的虚拟设备时,您将能够从电视、穿戴设备、电话或平板设备中进行选择;您可以选择要使用的 API 级别(并下载新的 API 级别);在设置中,您可以指定如下内容:
-
图形性能
-
相机模式(高级设置)
-
网络速度(高级设置)
-
引导选项(高级设置;设备首次启动后,快速启动可显著提高启动速度)
-
模拟 CPU 的数量(高级设置)
-
内存和存储设置(高级设置)
用于创建虚拟映像的虚拟设备基础映像和皮肤可以在以下位置找到:
SDK_INST/system-images
SDK_INST/skins
安装了应用和用户数据的实际虚拟设备位于以下位置:
∼/.android/avd
警告
虚拟设备不会模拟真实设备支持的所有硬件。即 2018 年第一季度,不支持以下内容:
-
API 等级 25 之前的 WiFi
-
蓝牙
-
国家足球联盟
-
SD 卡弹出和插入
-
连接到设备的耳机
-
通用串行总线
因此,如果您想使用模拟器,您必须在应用中采取预防措施,以避免这些问题的出现。
处理正在运行的虚拟设备也可以通过各种命令行工具来完成;更多信息见第十八章。
SDK
与 Android Studio 相反,软件开发工具包(SDK)是一个松散耦合的工具选择,这些工具要么是 Android 开发的基本工具,因此直接由 Android Studio 使用,要么至少对一些开发任务有帮助。它们都可以从 shell 中启动,有或没有自己的 GUI。
如果你在安装 Android Studio 的时候不知道 SDK 安装在哪里,你可以很容易地问 Android Studio:从菜单中选择文件➤项目结构➤ SDK 位置。
属于 SDK 一部分的命令行工具在第十八章中描述。
二、应用
一个 Android 应用由活动、服务、广播接收器和内容提供者等组件组成,如图 2-1 所示。活动用于与设备用户交互,服务用于在没有专用用户界面的情况下运行的程序部分,广播接收器监听来自其他应用和组件的标准化消息,内容供应器允许其他应用和组件访问由组件提供的一定数量和种类的数据。
图 2-1
Android 操作系统中的一个应用
组件由 Android 运行时启动,如果你喜欢,也可以由执行引擎启动,或者由它自己启动,或者代表其他创建启动触发器的组件启动。组件何时启动取决于它的类型和给它的元信息。在生命周期结束时,所有正在运行的组件都将从进程执行列表中删除,因为它们已经完成了工作,或者因为 Android OS 已经决定可以删除某个组件,因为不再需要该组件,或者因为设备资源短缺而必须删除该组件。
为了让你的应用或组件尽可能稳定地运行,并让你的用户对其可靠性有一个良好的感觉,深入了解 Android 组件的生命周期是有帮助的。在这一章中,我们将着眼于组件的系统特征及其生命周期。
简单的应用和 Android 组件易于构建;只需参考 Android 官方网站上的一个教程,或者网上其他地方成千上万个教程中的一个。不过,一个简单的应用不一定是专业级的稳定应用,因为就应用而言,Android 状态处理与桌面应用不同。这样做的原因是,你的 Android 设备可能会决定关闭你的应用以节省系统资源,特别是当你因为使用一个或多个其他应用一段时间而临时暂停有问题的应用时。
当然,Android 很可能永远不会杀死你目前正在使用的应用,但你必须采取预防措施。任何被 Android 终止的应用都可以在定义的数据和处理状态下重新启动,包括用户当前输入的大多数数据,并尽可能少地干扰用户当前的工作流程。
从文件的角度来看,Android 应用是一个带有后缀.apk的单个 zip 存档文件。它包含您的完整应用,包括所有元信息,这是在 Android 设备上运行应用所必需的。里面最重要的控制工件是描述应用和应用组成的组件的文件AndroidManifest.xml。
我们在这里不详细介绍这个归档文件结构,因为在大多数情况下,Android Studio 会为您正确地创建归档文件,所以您通常不需要了解它的内在功能。但是你可以很容易地看到里面。随便打开一个*.apk文件;例如,以您已经使用 Android Studio 构建的示例应用为例,如下所示:
图 2-2
解压后的 APK 文件
AndroidStudioProject/[YOUR-APP]/release/app-release.apk
然后拉开拉链。APK 文件只是普通的压缩文件。您可能需要临时将后缀改为.zip,这样您的解压缩程序就可以识别它。图 2-2 显示了一个解压缩的 APK 文件的例子。
这个.dex文件包含了以 Dalvik 可执行文件格式编译的类,类似于 Java 中的 JAR 文件。
我们将很快讨论与应用相关的工件,但是首先我们将看看任务是什么的更概念性的想法。
任务
一个任务是一组相互交互的活动,最终用户将它们视为应用的元素。用户启动一个应用,看到主活动,在那里做一些工作,打开和关闭子活动,可能切换到另一个应用,返回,并最终关闭应用。
再深入一点,一个任务展示的主要结构是它的后台栈,或者简称为栈,在那里一个应用的活动堆积起来。这个堆栈中简单应用的标准行为是,当你启动一个应用时,第一个活动构建这个堆栈的根,从应用内部启动的下一个活动位于它的顶部,另一个子活动位于两者的顶部,以此类推。每当一个活动因为您向后导航而关闭时(这就是名称 back stack 的来源),该活动就会从堆栈中删除。当根活动被移除时,栈作为一个整体被关闭,你的应用被认为是关闭的。
在AndroidManifest.xml文件的<application>元素中,在线文本指南的“应用声明”部分有更详细的描述,我们可以看到几个改变任务堆栈标准行为的设置,我们将在第三章中看到更多。通过这种方式,定制的任务堆栈可以成为帮助您的最终用户理解和流畅使用您的应用的强大手段。请记住,对于开始使用你的应用的用户来说,复杂的堆栈行为可能很难理解,所以你的目标应该是在功能和易用性之间找到一个良好的平衡。
应用清单
你可以在任何 Android 应用中看到的一个重要的中央应用配置文件是文件AndroidManifest.xml。它描述了应用并声明了应用的所有组件。这种清单文件的大纲可能如下所示:
<manifest xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:tools=
"http://schemas.android.com/tools"
package="de.pspaeth.tinqly">
...
<application
android:allowBackup="true"
android:icon="@mipmap/my_icon"x
android:label="@string/app_name"
android:roundIcon="@mipmap/my_round_icon"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity ... />
</application>
</manifest>
根条目<manifest>最重要的属性叫做package。它声明您的应用的 ID,如果您计划发布您的应用,这必须是它的全球唯一 ID。一个好主意是颠倒使用您的域(或您公司的域),然后使用唯一的应用标识符,如前面的代码所示。
表 2-1 描述了<manifest>所有可能的属性。请注意,对于最简单的应用,您需要的只是一个package属性和一个<application>子元素。
表 2-1
清单主要属性
|名字
|
描述
|
| --- | --- |
| android: installLocation | 定义安装位置。使用internalOnly仅安装在内部存储器中,使用auto让操作系统决定使用内部存储器(用户可以稍后在系统设置中切换),或者使用preferExternal让操作系统决定使用外部存储器。默认为internalOnly。请注意,为此目的使用外部存储有一些限制;参见<manifest>的在线文档。对于具有大量空闲内部存储的现代设备,您应该永远不需要在这里指定preferExternal。 |
| package | 定义您的应用的全球唯一 ID,是一个类似于abc.def.ghi.[...]的字符串,其中非点字符可能包含字母 A–Z 和 A–Z、数字 0–9 和下划线。不要在圆点后使用数字!这也是默认的进程名称和默认的任务关联性;请参阅在线文本指南,了解它们的含义。请注意,应用发布后,您将无法在谷歌 Play 商店中更改此包的名称。没有违约;您必须设置该属性。 |
| android: sharedUserId | 定义分配给应用的 Android 操作系统用户 id 的名称。在 Android 8.0 或 API level 26 之前,你可以为不同的应用分配相同的用户 id,让它们自由交换数据。这些应用必须使用相同的证书进行签名。然而,你通常不需要设置这个属性,但是如果你设置了它,确保你知道你在做什么。 |
| android: sharedUserLabel | 如果您还设置了sharedUserId,您可以在这里为共享用户 ID 设置一个用户可读的标签。该值必须是对字符串资源的引用(例如,@string/myUserLabel)。 |
| android: targetSandboxVersion | 用作安全级别,为 1 或 2。从 Android 8.0 或 API level 26 开始,你必须将其设置为 2。对于 2,用户 ID 不再能在不同的应用之间共享,并且usesClearTextTraffic(参见在线文本伴侣)的默认值被设置为 false。 |
| android: versionCode | 定义应用的内部版本号。这不向用户显示,仅用于比较版本。此处使用整数。这默认为undefined。 |
| android: versionName | 定义用户可见的版本字符串。这要么是字符串本身,要么是指向字符串资源的指针("@string/...")。这除了通知用户之外,没有其他用途。 |
在线文本指南的“清单顶级条目”一节中列出了所有可能作为<manifest>元素的子元素的元素。最重要的一个是<application>,它描述了应用,在在线文本指南的“应用声明”一节中有详细介绍。
三、活动
活动代表应用的用户界面入口点。任何需要以直接方式与用户进行功能性交互的应用,通过让用户输入东西或以图形方式告诉用户应用的功能状态,都会向系统暴露至少一个活动。我之所以说是功能性的,是因为通过祝酒词或状态栏的通知也可以告诉用户事件的发生,而这并不需要活动。
应用可以有零个、一个或多个活动,它们以两种方式开始:
-
在
AndroidManifest.xml中声明的主活动通过启动应用开始。这有点类似于传统应用的main()函数。 -
所有活动都可以被配置为由一个显式或隐式的意图启动,就像在
AndroidManifest.xml中配置的那样。Intents 既是一个类的对象,也是 Android 中的一个新概念。有了明确的意图,通过触发意图,组件指定它需要由专用应用的专用组件来完成某些事情。对于隐式意图,组件只是告诉需要做什么,而没有指定应该由哪个组件来做。Android 操作系统或用户决定哪个应用或组件能够满足这样的隐式请求。
从用户的角度来看,活动表现为可以从应用启动器内部启动的东西,无论是标准启动器还是专门的第三方启动器应用。一旦它们开始运行,它们就会出现在任务堆栈中,当用户使用后退按钮时就会看到它们。
申报活动
要声明一个活动,您可以在AndroidManifest.xml中编写以下内容,例如:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...
package="com.example.myapp">
<application ... >
<activity android:name=".ExampleActivity" />
...
</application ... >
...
</manifest >
如这个特殊的例子所示,您可以用一个点作为名称的开头,这将导致应用包名称的前置。在这种情况下,活动的全称是com.example.myapp.ExampleActivity。也可以写全名,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... package="com.example.myapp" ...>
<application ... >
<activity android:name=
"com.example.myapp.ExampleActivity" />
...
</application ... >
...
</manifest>
您可以添加到<activity>元素的所有属性都列在在线文本指南的“活动相关清单条目”一节中。
以下元素可以是activity元素中的子元素:
-
<意图过滤>
这是一个意图过滤器。有关详细信息,请参阅“与活动相关的清单条目”中的在线文本指南。您可以指定零个、一个或多个意图过滤器。
-
<布局>
从 Android 7.0 开始,您可以在多窗口模式下指定布局属性,如下所示,当然您可以使用自己的数字:
<layout android:defaultHeight="500dp"
android:defaultWidth="600dp"
android:gravity="top|end"
android:minHeight="450dp"
android:minWidth="300dp" />
属性defaultWidth和defaultHeight指定默认尺寸,属性gravity指定活动在自由形式模式下的初始位置,属性minHeight和maxHeight表示最小尺寸。
-
<元数据>
这是一个任意的名称值对,形式为
<meta-data android:name="..." android:resource="..." android:value="..." />。你可以有几个这样的元素,它们被放入一个叫做PackageItemInfo.metaData的android.os.Bundle元素中。
警告
编写一个没有任何活动的应用是可能的。该应用仍然可以作为内容供应器提供服务、广播接收器和数据内容。作为应用开发人员,你需要记住的一件事是,用户不一定理解这些没有用户界面的组件实际上是做什么的。在大多数情况下,建议提供一个简单的主活动来提供信息,这样可以改善用户体验。然而,在企业环境中,提供没有活动的应用是可以接受的。
开始活动
活动可以通过两种方式之一启动。首先,如果活动被标记为应用的可启动主活动,则可以从应用启动器启动该活动。要将一个活动声明为可启动的主活动,在AndroidManifest.xml文件中应该编写以下内容:
<activity android:name=
"com.example.myapp.ExampleActivity">
<intent-filter>
<action android:name=
"android.intent.action.MAIN" />
<category android:name=
"android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android.intent.action.MAIN告诉 Android 它是主活动,将转到一个任务的底部,android.intent.category.LAUNCHER指定它必须列在启动器内部。
第二,一个活动可以由来自同一个 app 或任何其他 app 的意向启动。为此,在清单中声明一个意图过滤器,如下所示:
<activity android:name=
"com.example.myapp.ExampleActivity">
<intent-filter>
<action android:name=
"com.example.myapp.ExampleActivity.START_ME" />
<category android:name=
"android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
处理这个意图过滤器并实际启动活动的相应代码现在如下所示:
val intent = Intent()
intent.action =
"com.example.myapp.ExampleActivity.START_ME"
startActivity(intent)
必须为其他应用的调用设置标志exported="false"。过滤器中的类别规范android.intent.category.DEFAULT负责即使在启动代码中没有设置类别也可以启动的活动。
在前面的例子中,我们使用了一个显式的意图来调用一个活动。我们精确地告诉 Android 要调用哪个活动,我们甚至期望只有一个活动,它通过意图过滤器以这种方式被处理。另一种类型的意图被称为隐式意图,它的作用是,与精确地调用一个活动相反,告诉系统我们实际上想做什么,而无需指定使用哪个应用或哪个组件。例如,这样的隐式调用看起来像这样:
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, "Give me a Quote")
startActivity(intent)
这个代码片段调用一个活动,该活动能够处理Intent.ACTION_SEND动作,接收 MIME 类型的文本text/plain,并传递文本“给我一个报价”然后,Android 操作系统将向用户呈现一个活动列表,这些活动来自这个或其他能够接收这种意图的应用。
活动可以有与之相关的数据。只需使用 intent 类的一个重载的putExtra(...)方法。
活动和任务
与任务堆栈相关的已启动活动的实际情况由这里列出的属性决定,如<activity>元素的属性所示:
-
taskAffinity -
launchMode -
allowTaskReparenting -
clearTaskOnLaunch -
alwaysRetainTaskState -
finishOnTaskLaunch
和意向调用标志,如下所示:
-
FLAG_ACTIVITY_NEW_TASK -
FLAG_ACTIVITY_CLEAR_TOP -
FLAG_ACTIVITY_SINGLE_TOP
您可以指定Intent.flags = Intent.<FLAG>,其中<FLAG>是列表中的一个。如果活动属性和调用者标志相矛盾,调用者标志获胜。
返回数据的活动
如果您使用以下命令开始一项活动:
startActivityForResult(intent:Intent, requestCode:Int)
这意味着您希望被调用的活动在返回的同时返回一些东西。您在调用的活动中使用的构造如下所示:
val intent = Intent()
intent.putExtra(...)
intent.putExtra(...)
setResult(Activity.RESULT_OK, intent)
finish()
在.putExtra(...)方法调用中,您可以添加从活动中返回的任何数据。例如,您可以将这些行添加到onBackPressed()事件处理程序方法中。
对于setResult()的第一个参数,您可以使用以下任何一种:
-
Activity.RESULT_OK如果你想告诉调用者被调用的活动成功完成了它的工作。 -
Activity.RESULT_CANCELED如果你想告诉调用者被调用的活动没有成功完成它的工作。您仍然可以通过.putExtra(...)输入额外的信息来指明哪里出错了。 -
Activity.RESULT_FIRST_USER + N,N为 0、1、2、...,用于要定义的任何自定义结果代码。N实际上没有限制(最大值为 2311)。
注意,如果你有一个工具栏,你还需要处理回压事件。一种可能是添加如下的onCreate()方法行:
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
// The navigation button from the toolbar does not
// do the same as the BACK button, more precisely
// it does not call the onBackPressed() method.
// We add a listener to do it ourselves
toolbar.setNavigationOnClickListener { onBackPressed() }
当被调用的意图以前面描述的方式返回时,调用组件需要被告知该事件。这是异步完成的,因为startActivityForResult()方法会立即返回,而不会等待被调用的活动完成。然而,捕获该事件的方法是重写onActivityResult()方法,如下所示:
override
fun onActivityResult(requestCode:Int, resultCode:Int,
data:Intent) {
// do something with 'requestCode' and 'resultCode'
// returned data is inside 'data'
}
requestCode是你在startActivityForResult()里面设置为requestCode的东西,resultCode是你在被调用活动的setResult()里面作为第一个参数写的东西。
警告
在某些设备上,requestCode的最高有效位设置为 1,无论之前设置了什么。为了安全起见,您可以在onActivityResult()中使用 Kotlin 构造,如下所示:val requestCodeFixed = requestCode and 0xFFFF
意图过滤器
意图是告诉 Android 需要做一些事情的对象,如果我们不指定被调用的组件,而是让 Android 决定哪个应用和哪个组件可以响应请求,则意图可以是明确的或隐含的*。如果有一些歧义,Android 无法决定调用哪个组件来表达隐含的意图,Android 会询问用户。*
为了让隐含意图发挥作用,可能的意图接收者需要声明它能够接收哪些意图。例如,一个活动可能能够显示一个文本文件的内容,而一个呼叫者说“我需要一个可以显示文本文件的活动”可能正好连接到这个活动。现在,意向接收方声明其响应意向请求的能力的方式是在其应用的AndroidManifest.xml文件中指定一个或多个意向过滤器*。这种声明的语法如下:
<intent-filter android:icon="drawable resource"
android:label="string resource"
android:priority="integer" >
...
</intent-filter>
这里,icon指向图标的可绘制资源 ID,label指向标签的字符串资源 ID。如果未指定,将使用父元素的图标或标签。priority属性是一个介于-999 和 999 之间的数字,对于 intents 指定它处理这种 intent 请求的能力,对于 receiver 指定几个 receiver 的执行顺序。较高的优先级在较低的优先级之前。
警告
应该谨慎使用priority属性。一个组件不可能知道来自其他应用的其他组件的优先级。因此,你在应用之间引入了某种依赖,这不是设计意图。
这个<intent-filter>元素可以是下列元素的子元素:
-
<activity>和<activity-alias> -
<service> -
<receiver>
因此,意图可以用来启动活动和服务,并发射广播消息。
元素必须包含子元素,如下所示:
-
<action>(必须) -
<category>(可选) -
<data>(可选)
意图动作
过滤器的<action>子过滤器(或者多个子过滤器,因为可以有多个子过滤器)指定要执行的动作。语法如下:
<action android:name="string" />
这将是表示诸如查看、选择、编辑、拨号等动作的东西。通用动作的完整列表是由类android.content.Intent中名称类似ACTION_*的常量指定的;您可以在联机文本指南的“意图构成部分”一节中找到一个列表。除了这些通用操作之外,您还可以定义自己的操作。
注意
使用任何标准操作并不一定意味着您的设备上有任何应用能够响应相应的意图。
意图类别
过滤器的<category>子级指定了过滤器的类别。语法如下:
<category android:name="string" />
此属性可用于指定意图应解决的组件类型。您可以指定几个类别,但是该类别并不用于所有目的,您也可以省略它。只有当所有要求的类别都存在时,过滤器才会匹配意图。
当调用方使用一个意图时,您可以通过编写以下代码来添加类别,例如:
val intent:Intent = Intent(...)
intent.addCategory("android.intent.category.ALTERNATIVE")
标准类别对应于名称类似于android.content.Intent类中的CATEGORY_*的常量。您可以在联机文本指南的“意图构成部分”一节中找到它们。
警告
对于隐式意图,您必须使用过滤器内的DEFAULT类别。这是因为方法startActivity()和startActivityForResult()默认使用这个类别。
意向数据
过滤器的<data>子级是过滤器的数据类型规范。语法如下:
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string" />
您可以指定以下任一项或两项:
-
仅由
mimeType元素指定的数据类型,例如text/plain或text/html。所以,你可以这样写: -
由方案、主机、端口和一些路径规范指定的数据类型:
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]。这里的<path>表示完整路径,<pathPrefix>是路径的起点,<pathPattern>类似于路径,但带有通配符:X*是字符 X 的零次或多次出现,.*是任何字符的零次或多次出现。由于转义规则的原因,写\*表示星号,写\\表示反斜杠。
<data android:mimeType="text/html" />
在调用方,您可以使用setType()、setData()和setDataAndType()来设置任何数据类型组合。
警告
对于隐式意图过滤器,如果调用者在intent.data = <some URI>中指定了 URI data部分,那么在过滤器声明中仅指定方案/主机/端口/路径可能是不够的。在这些情况下,您还必须指定 MIME 类型,就像在mimeType="*/*"中一样。否则,过滤器可能不匹配。这通常发生在内容提供者环境中,因为内容提供者的getType()方法被指定的 URI 调用,结果被设置为意图的 MIME 类型。
意向额外数据
除了由<data>子元素指定的数据之外,任何 intent 都可以添加额外的数据来发送数据。
虽然您可以使用各种putExtra(...)方法中的一种来添加任何种类的额外数据,但是也有一些标准的额外数据字符串是由putExtra(String,Bundle)发送的。您可以在联机文本指南的“意图构成部分”一节中找到这些关键字。
意图标志
您可以通过调用以下内容来设置特殊意图处理标志:
intent.flags = Intent.<FLAG1> or Intent.<FLAG2> or ...
这些标志中的大部分指定了 Android 操作系统如何处理意图。具体来说,FLAG_ACTIVITY_*形式的标志是针对Context.startActivity(..)调用的活动的,类似FLAG_RECEIVER_*的标志是和Context.sendBroadCast(...)一起使用的。在线文本指南的“意图构成部分”一节中的表格显示了详细信息。
系统意图过滤器
系统应用(即您购买智能手机时已经安装的应用)具有意图过滤器,您可以使用该过滤器从您的应用中调用它们。不幸的是,猜测如何从系统应用中调用活动并不容易,相关的文档也很难找到。一个解决办法是从他们的 APK 档案中提取这些信息。对于 API 级别 26,这已经为您完成了,其结果在“系统意图过滤器”一节中的在线文本指南中列出
举个例子,假设你想发送一封电子邮件。在网文伴侣里看系统意图表,可以找到很多PrebuiltGmail的动作。我们用哪一个?首先,通用接口不应该有太多的输入参数。其次,我们还可以查看动作名称,以找到似乎合适的内容。一个有希望的候选者是SEND_TO行动;显然,它所需要的只是一个mailto:数据规范。碰巧的是,这是我们真正需要的行动。使用精心设计的mailto:... URL 允许我们指定更多的收件人、抄送和密件抄送收件人、主题,甚至邮件正文。然而,你也可以只使用“mailto: master@universe.com”并通过使用额外的字段来添加收件人、正文等等。因此,要发送电子邮件,同时可能让用户在设备上安装的几个电子邮件应用中进行选择,请编写以下内容:
val emailIntent:Intent = Intent(Intent.ACTION_SENDTO,
Uri.fromParts("mailto","abc@gmail.com", null))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Subject")
emailIntent.putExtra(Intent.EXTRA_TEXT, "Body")
startActivity(Intent.createChooser(
emailIntent, "Send email..."))
// or startActivity(emailIntent) if you want to use
// the standard chooser (or none, if there is only
// one possible receiver).
警告
如何准确处理意图 URIs 和额外数据由接收应用决定。设计糟糕的电子邮件可能根本不允许您指定电子邮件标题数据。为了安全起见,您可能希望将所有标题数据都添加到mailto: URI 和中作为额外数据。
活动生命周期
活动有一个生命周期,与传统的桌面应用相反,当 Android 操作系统决定终止活动时,它们会被有意地终止。所以,作为一名开发者,你需要采取特别的预防措施来保证应用的稳定性。更准确地说,活动发现自己处于以下状态之一:
-
关闭:活动不可见,不做任何处理。尽管如此,包含该活动的应用可能还活着,因为它有一些其他组件在运行。
-
已创建:要么该活动是主活动,由用户或其他组件启动,要么它是一个活动,不管它是否是主活动,由其他组件启动,从同一应用或其他应用内部启动(如果安全考虑允许的话)。此外,例如,当你翻转屏幕时,活动创建就会发生,应用需要用不同的屏幕特征来构建。在创建过程中,回调方法
onCreate()被调用。您必须实现这个方法,因为那里需要构建 GUI。您还可以使用这个回调方法来启动或连接到服务,或者提供内容提供者数据。你可以使用这些 API 让准备播放音乐,操作相机,或者做这个应用为之而生的任何事情。这也是一个初始设置数据库或应用需要的其他数据存储的好地方。 -
已启动:一旦完成创建(以及在停止后重新启动的情况下),活动进入已启动状态。在这里,活动将对用户可见。在启动过程中,回调方法
onStart()被调用。这是启动广播接收器、启动服务、重建内部状态和活动进入停止状态时退出的进程的好地方。 -
Resumed :在对用户可见之前不久,活动经历了恢复过程。在这个过程中,回调
onResume()被调用。 -
运行:活动完全可见,用户可以与之交互。这种状态紧跟在恢复过程之后。
-
暂停:活动失去焦点,但至少部分可见。例如,当用户点击后退或最近按钮时,就会失去焦点。活动可能会继续向 UI 发送更新,或者继续发出声音,但是在大多数情况下,活动会进入停止状态。在暂停期间,
onPause()回调被调用。暂停状态之后是停止状态或恢复状态。 -
停止:活动对用户不可见。它以后可能会被重新启动、销毁,并从活动进程列表中删除。在停止期间,
onStop()回调被调用。停止之后,要么毁灭,要么开始。在这里你可以,例如,停止你在onStart()中启动的服务。 -
销毁:活动被移除。回调
onDestroy()被调用,您应该实现它并在那里做一切事情来释放资源和做其他清理动作。
表 3-1 列出了活动状态之间可能的转换,如图 3-1 所示。
图 3-1
活动状态转换
表 3-1
活动状态转换
|从
|
到
|
描述
|
实施
|
| --- | --- | --- | --- |
| 停工 | 创造 | 一个活动在第一次或销毁后被调用。 | onCreate():调用super.onCreate(),准备 UI,启动服务。 |
| 创造 | 出发 | 活动在创建后开始。 | 您可以在这里启动仅在活动可见时才需要的服务。 |
| 出发 | 重新开始 | 恢复状态自动跟随开始状态。 | 使用onResume。 |
| 重新开始 | 运转 | 运行状态自动跟随恢复状态。 | 包括 UI 活动在内的活动在这里运行。 |
| 运转 | 暂停 | 该活动失去焦点,因为用户点击了“后退”或“最近”按钮。 | 使用onPause。 |
| 暂停 | 重新开始 | 该活动尚未停止,用户导航回该活动。 | 使用onResume()。 |
| 暂停 | 停止 | 该活动对用户是不可见的,例如,因为另一个活动开始了。 | 您可以在这里停止仅在活动可见时才需要的服务。 |
| 停止 | 出发 | 停止的活动再次开始。 | 您可以在这里启动仅在活动可见时才需要的服务。 |
| 停止 | 破坏 | 停止的活动将被删除。 | onDestroy():释放所有资源,进行清理,停止onCreate中启动的服务。 |
在活动中保留状态
我已经强调了你需要采取预防措施,以确保当你的应用被 Android 操作系统强行停止时,它能以良好的方式重新启动。在这里,我给你一些如何做到这一点的建议。
查看活动的生命周期,我们可以看到一个即将被 Android OS 终止的活动调用了方法onStop()。但是还有两次试镜我们还没有谈到。它们的名字是onSaveInstanceState()和onRestoreInstanceState(),每当 Android 决定需要保存或恢复某项活动的数据时,就会调用它们。这与onStart()和onStop()不一样,因为有时没有必要保存应用的状态。例如,如果一个活动不会被销毁,而只是被挂起,那么无论如何状态都会被保持,并且onSaveInstanceState()和onRestoreInstanceState()不会被调用。
Android 在这里帮助了我们:onSaveInstanceState()和onRestoreInstanceState()的默认实现已经保存和恢复了有 id 的 UI 元素。所以,如果这就是你所需要的,你不需要做任何事情。当然,您的活动可能更复杂,可能包含其他需要保留的字段。在这种情况下,您可以覆盖onSaveInstanceState()和onRestoreInstanceState()。只要确保你调用了超类的方法;否则,您必须自己处理所有 UI 元素。
override
fun onSaveInstanceState(outState:Bundle?) {
super.onSaveInstanceState(outState)
// add your own data to the Bundle here...
// you can use one of the put* methods here
// or write your own Parcelable types
}
override
fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
// restore your own data from the Bundle here...
// use one of the get* methods here
}
注意,保存的状态也会被onCreate()回调,所以您可以决定是使用onRestoreInstanceState()还是onCreate()方法来恢复状态。
在这种情况下,保存和恢复状态的标准机制可能不适合您的需要。例如,当你停止应用时,它不能保留数据。在这种情况下,不会调用onSaveInstanceState()方法。如果你需要在这种情况下保存数据,你可以使用onDestroy()将你的应用的数据保存在数据库中,并在onCreate()回调时读取数据库。更多信息见第八章。*
四、服务
服务是在没有用户界面的情况下运行的组件,并且在概念上与长期运行的流程密切相关。它们独立于状态栏或祝酒词中的通知。服务可以由应用启动,也可以由应用绑定,或者两者都有。
服务有两种风格:前台服务和后台服务。虽然乍一看,谈论“前台”服务似乎是矛盾的,因为许多人倾向于说“服务在后台运行”,但前台服务确实存在。前台和后台服务之间的区别是至关重要的,因为它们的行为是不同的。
警告
不要将服务误解为运行任何需要在后台计算的东西的构造,换句话说,不要干扰 GUI 活动。如果你需要一个不干扰 GUI 的进程,但是在你的应用不活动的时候没有资格运行,也不能在你的应用之外使用,考虑使用线程。更多信息见第十章。
前台服务
不同 Android 版本的前台服务的内在功能有所不同。Android 8.0 (API level 26)之前的前台服务只是在状态栏中有一个条目的后台服务,对 Android 操作系统如何处理它们没有严格的影响,而在 Android 8.0 (API level 26)中,前台服务遵循一种特殊的符号,并得到 Android 操作系统的更多关注,使它们不太可能因资源短缺而被终止。以下是一些细节:
-
Android 8.0(API 26 级)之前的前台服务是在状态栏中只是呈现一个通知条目的服务。需要使用服务的客户端组件不知道启动的服务是否是前台服务;它只是通过
startService(intent)启动服务。参见第十二章的。 -
从 Android 8.0 (API 等级 26)开始的前台服务在用户知道它们的情况下运行。他们必须通过状态栏中的通知来干预操作系统。客户端组件通过调用
startForeroundService(intent)显式启动前台服务,服务本身必须在几秒钟内通过调用startForeground(notificationId, notification)告诉 Android OS 它想要作为前台服务运行。
前台服务的一个显著的生命周期特征是它不太可能因为可用资源短缺而被 Android 杀死。然而,文件对此并不明确。有时你会读到“不会被杀死”,有时会读到“不太可能被杀死”此外,Android 处理这些事情的方式会随着新版本的 Android 而改变。一般来说,你应该保守,做最坏的打算。在这种情况下,阅读“不太可能被杀死”,并采取预防措施,如果服务在您的应用执行某些工作时停止运行。
后台服务
后台服务在后台运行;也就是说,它们不会在状态栏中显示条目。然而,他们被允许使用祝酒辞向用户发送短通知消息。与前台服务相比,后台服务更脆弱,因为 Android 希望它们与用户活动的联系更松散,因此当资源短缺时,更容易决定终止它们。
从 Android 8.0 (API level 26)开始,如果您以旧的方式实例化后台服务,会有一些限制,并且建议转向使用 JobScheduler 方法。如果以下情况不成立,运行在 Android 8.0 或更新版本上的应用将被视为在后台运行:
-
该应用有一个可见的活动,当前活动或暂停。
-
app 有一个前台服务,换句话说,一个服务在运行过程中调用了
startForegound()。 -
另一个前台应用连接到它,要么通过使用它的服务之一,要么通过将其用作内容供应器。
一旦一个 Android 8.0 应用作为后台应用开始它的生命,或者被切换到后台应用,它在被认为空闲之前有几分钟的时间。一旦空闲,应用的后台服务就会停止。作为一个例外,一个后台应用将进入白名单,如果它处理用户可见的任务,就被允许执行后台服务。示例包括处理“Firebase Cloud Messaging”消息、接收广播(如 SMS 或 MMS 消息)、执行通知中的PendingIntent(在原始应用的许可下由不同应用执行的意图)或启动VpnService。
从 Android 8.0 开始,以前通过执行后台作业完成的大多数事情都被认为有资格由 JobScheduler API 来处理;更多信息见第八章。
声明服务
服务在应用的AndroidManifest.xml文件中声明如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ...>
<activity ...>
</activity>
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
</service>
</application>
</manifest>
可用标志见表 4-1 。
表 4-1
服务的清单标志
|名字
|
描述
|
| --- | --- |
| android:description | 这是指向服务描述的资源 ID。你应该使用它,因为用户可以杀死服务,但如果你告诉他们你的服务是做什么的,这种情况就不太可能发生。 |
| android:directBootAware | 这个可以是true也可以是false。默认是false。如果true,即使重启后设备尚未解锁,服务也可以运行。Android 7.0(API 24 级)引入了直接引导模式。请注意,直接引导感知服务必须将其数据存储在设备的受保护存储中。 |
| android:enabled | 这个可以是true也可以是false。默认是true。如果false,服务被有效禁用。对于生产服务,通常不会将它设置为false。 |
| android:exported | 这个可以是true也可以是false。这指定了其他应用是否可以使用该服务。如果没有意图过滤器,默认为false,否则为true。意图过滤器的存在意味着外部使用,因此这种区别。 |
| android:icon | 这是图标资源 ID。默认为应用的图标。 |
| android:isolatedProcess | 这个可以是true也可以是false。默认是false。如果true,服务无法与系统通信,只能通过服务方法。使用这个标志实际上是一个好主意,但是在大多数情况下你的服务需要和系统对话,所以你不得不离开它false,除非这个服务真的是自包含的。 |
| android:label | 这是向用户显示的服务标签。默认为应用的标签。 |
| android:name | 这是服务类的名称。如果您使用一个点作为第一个字符,它会自动加上在manifest元素中指定的包名。 |
| android:permission | 这是伴随此服务的权限名称。默认值是application元素中的permission属性。如果未指定并且默认值不适用,则服务将不受保护。 |
| android:service | 这是服务进程的名称。如果指定,服务将在其自己的进程中运行。如果它以冒号(:)开头,该过程将是应用的私有过程。如果它以小写字母开头,那么产生的进程将是一个全局进程。可能会有安全限制。 |
<service>元素允许以下子元素:
-
<意图过滤>
这可以是零个、一个或多个意图过滤器。第三章对它们进行了描述。
-
<元数据>
这是一个任意的名称值对,形式为
<meta-data android:name="..." android:resource="..." android:value="..." />。你可以有几个这样的元素,它们被放入一个叫做PackageItemInfo.metaData的android.os.Bundle元素中。
作为一名专业开发人员,理解什么是进程以及 Android 操作系统如何处理它是非常重要的;见过程控制清单中的android:service标志。这可能很棘手,因为过程内部往往会随着新的 Android 版本而变化,如果你读博客,它们似乎每分钟都在变化。事实上,进程是一个计算单元,由 Android 操作系统启动以执行计算任务。此外,当 Android 决定耗尽系统资源时,它会停止运行。如果你决定停止使用某个特定的应用,并不意味着相应的进程会自动终止。每当您第一次启动一个应用,并且您没有明确地告诉该应用使用另一个应用的进程时,就会创建并启动一个新的进程,并且随着后续计算任务的存在,进程会被使用或新的进程会被启动,这取决于它们的设置以及彼此之间的关系。
除非您在清单文件中明确指定服务特征,否则由应用启动的服务将在应用的进程中运行。这意味着服务可能会随着应用而存在,也不可避免地会消亡。一个进程需要启动才能真正活起来,但是在 app 主进程中运行的时候,app 死了就死了。这意味着服务的资源需求关系到应用的资源需求。在以前资源稀缺的时候,这比现在更强大的设备更重要,但知道这一点还是有好处的。如果一项服务需要大量资源,而资源短缺,那么是否需要杀死整个应用或只是那个贪婪的服务来释放资源就有所不同了。
然而,如果您通过android:service manifest 条目告诉服务使用它自己的进程,服务的生命周期可以由 Android 操作系统独立处理。你必须做出决定:要么让它使用自己的进程,并接受一个应用可能出现的进程激增,要么让它们在一个进程中运行,并更紧密地耦合生命周期。
让几个计算单元在一个进程中运行还有另一个后果:它们不会并发运行!这对于 GUI 活动和进程来说是至关重要的,因为我们知道 GUI 活动必须很快才能不妨碍用户交互,而且服务在概念上是绑定到长时间运行的计算的。摆脱这种困境的一种方法是使用异步任务或线程。第十章将会更多地讨论并发性。
如果服务需要寻址受设备保护的存储,就像清单中的android:directBootAware标志触发的直接引导模式一样,它需要访问一个特殊的上下文。
val directBootContext:Context =
appContext.createDeviceProtectedStorageContext()
// For example open a file from there:
val inStream:FileInputStream =
directBootContext.openFileInput(filename)
通常情况下,您不应该使用此上下文,只有在特殊服务需要在引导过程后立即激活时才使用。
服务类别
服务必须扩展下列类或其子类之一:
android.app.Service
如前所述,它们必须在应用的AndroidManifest.xml文件中声明。
来自android.app.Service的接口方法在在线文本指南的“意图组成部分”一节中有所描述。
请注意,有两种方法可以停止通过startService()或startForeroundService显式启动的服务:服务通过调用stopSelf()或stopSelfResult()或从外部调用stopService()来自行停止。
启动服务
服务可以从任何组件显式启动,这些组件是android.content.Context的子类或者可以访问Context。活动、其他服务、广播接收器和内容供应器都是如此。
要显式启动服务,您需要一个适当的意图。我们基本上有两种情况:首先,如果服务与服务的客户端(调用者)在同一个应用中,您可以为从 Android 8.0 (API 级别 26)开始定义的前台服务编写以下代码:
val intent = Intent(this, TheService::class.java)
startService(intent)
对于正常服务,或者
val intent = Intent(this, TheService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
所以,我们可以直接引用服务类。如果您是一名新的 Kotlin 开发人员,乍一看,TheService::class.java符号可能看起来很奇怪;这就是 Kotlin 提供 Java 类作为参数的方式。(对于 Android 8.0 (API 级别 26)之前的版本,您可以正常启动它。)
注意
因为意图通过使用各种putExtra()方法之一允许通用的额外属性,所以我们也可以将数据传递给服务。
第二种情况是,如果我们想要启动的服务是另一个应用的一部分,因此是一个外部服务。然后,您必须在服务声明中添加一个意图过滤器。这里有一个例子:
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="<PCKG_NAME>.START_SERVICE" />
</intent-filter>
</service>
在这个例子中,<PCKG_NAME>是 app 的包名,如果你愿意,你可以写一个不同的标识符来代替START_SERVICE。现在,在服务客户端内部,您可以编写以下代码来启动和停止外部服务,其中在 intent 构造函数内部,您必须编写与服务的 intent filter 声明中相同的字符串:
val intent = Intent("<PCKG_NAME>.START_SERVICE")
intent.setPackage("<PCKG_NAME>")
startService(intent)
// ... do something ...
stopService(intent)
setPackage()语句在这里很重要(当然您必须替换服务的包名);否则,将应用安全限制,并且您会收到一条错误消息。
绑定到服务
开始一项服务是故事的一部分。另一部分是在它们运行时使用它们。这就是服务的绑定的用途。
要创建可以绑定到同一个应用或从同一个应用绑定的服务,请编写如下代码:
/**
* Class used for binding locally, i.e. in the same App.
*/
class MyBinder(val servc:MyService) : Binder() {
fun getService():MyService {
return servc
}
}
class MyService : Service() {
// Binder given to clients
private val binder: IBinder = MyBinder(this)
// Random number generator
private val generator: Random = Random()
override
fun onBind(intent: Intent):IBinder {
return binder
}
/** method for clients */
fun getRandomNumber():Int {
return generator.nextInt(100)
}
}
要从同一应用内部绑定到此服务,请在使用客户端的服务内部编写以下代码:
val servcConn = object : ServiceConnection {
override
fun onServiceDisconnected(compName: ComponentName?) {
Log.e("LOG","onServiceDisconnected: " + compName)
}
override
fun onServiceConnected(compName: ComponentName?,
binder: IBinder?) {
Log.e("LOG","onServiceConnected: " + compName)
val servc = (binder as MyBinder).getService()
Log.i("LOG", "Next random number from service: " +
servc.getRandomNumber())
}
override
fun onBindingDied(compName:ComponentName) {
Log.e("LOG","onBindingDied: " + compName)
}
}
val intent = Intent(this, MyService::class.java)
val flags = BIND_AUTO_CREATE
bindService(intent, servcConn, flags)
在这里,object: ServiceConnection {...}构造是 Kotlin 通过创建匿名内部类的对象来实现接口的方式,就像 Java 中的new ServiceConnection(){...}。这个构造在 Kotlin 中被称为对象表达式。在这种情况下,意图构造函数中的this指的是一个Context对象。你可以像这样在活动中使用它。如果变量中有Context,请在此处使用该变量的名称。
当然,除了日志记录,你应该做更有意义的事情。特别是在onSeviceConnected()方法中,您可以将绑定器或服务保存在一个变量中以备将来使用。尽管如此,还是要确保对死亡的绑定或死亡的服务连接做出适当的反应。例如,您可以尝试再次绑定服务,告诉用户,或者两者都做。
前面的代码会在您绑定到服务时自动启动该服务,但它还不存在。这是通过以下陈述实现的:
val flags = BIND_AUTO_CREATE
[...]
如果因为确定服务正在运行而不需要,可以省略。然而,在大多数情况下,最好包含该标志。以下是可用于设置绑定特征的其他标志:
-
我们刚刚用了那个。这意味着如果服务还没有启动,它会自动启动。有时你会读到,如果你绑定到一个服务,那么显式地启动它是不必要的,但是只有当你设置了这个标志时,这才是正确的。
-
BIND_DEBUG_UNBIND:这导致保存下一个unbindService()的调用栈,以防后续的 unbind 命令出错。如果发生这种情况,将会显示更详细的诊断输出。因为这会造成内存泄漏,所以该特性只应用于调试目的。 -
BIND_NOT_FOREGROUND:仅当客户端运行在前台进程中,目标服务运行在后台进程中时才适用。使用此标志,绑定过程不会将服务提升到前台调度优先级。 -
BIND_ABOVE_CLIENT:用这个标志,我们指定服务比客户端(即服务调用者)更重要。在资源短缺的情况下,系统将在调用服务之前终止客户端。 -
这个标志告诉 Android 操作系统,你愿意接受 Android 将绑定视为非关键的,并在内存不足的情况下终止服务。
-
BIND_WAIVE_PRIORITY:这个标志导致将服务调用的调度留给服务运行的流程。
只需将它们添加到适合您需求的组合中。
注意
从BroadcastReceiver组件内部绑定是不可能的,除非BroadcastReceiver已经通过registerReceiver(receiver.intentfilter)注册。在后一种情况下,接收器的寿命与注册组件相关。但是,您可以从广播接收器传递用于启动(换句话说,不绑定)服务的 intent 内部的指令字符串。
要绑定到外部服务,换句话说,绑定到属于另一个应用的服务,您不能使用与内部服务相同的绑定技术。原因是我们使用的IBinder接口不能直接访问服务类,因为该类在进程边界上是不可见的。然而,我们可以将在服务和服务客户机之间传输的数据包装到一个android.os.Handler对象中,并使用该对象将数据从服务客户机发送到服务。为了实现这一点,我们首先需要为服务定义一个用于接收消息的Handler。这里有一个例子:
internal class InHandler(val ctx: Context) : Handler() {
override
fun handleMessage(msg: Message) {
val s = msg.data.getString("MyString")
Toast.makeText(ctx, s, Toast.LENGTH_SHORT).show()
}
}
[...]
class MyService : Service() {
val myMessg:Messenger = Messenger(InHandler(this))
[...]
}
除了创建一个Toast消息,当消息到达时,你当然可以做更多有趣的事情。现在在服务的onBind()方法中,我们返回 messenger 提供的 binder 对象。
override
fun onBind(intent:Intent):IBinder {
return myMessg.binder
}
至于在AndroidManifest.xml文件中的条目,我们可以写得和启动远程服务时一样。
在服务客户机中,您可以添加一个Messenger属性和一个ServiceConnection对象。这里有一个例子:
var remoteSrvc:Messenger? = null
private val myConnection = object : ServiceConnection {
override
fun onServiceConnected(className: ComponentName,
service: IBinder) {
remoteSrvc = Messenger(service)
}
override
fun onServiceDisconnected(className: ComponentName) {
remoteSrvc = null
}
}
要实际执行绑定,您可以像处理内部服务一样进行。例如,在活动的onCreate()方法中,您可以编写以下代码:
val intent:Intent = Intent("<PCKG_NAME>.START_SERVICE")
intent.setPackage("<PCKG_NAME>")
bindService(intent, myConnection, Context.BIND_AUTO_CREATE)
这里,适当地用服务包的名称代替<PCKG_NAME>。
现在,要跨越流程边界从客户端向服务发送消息,您可以编写以下代码:
val msg = Message.obtain()
val bundle = Bundle()
bundle.putString("MyString", "A message to be sent")
msg.data = bundle
remoteSrvc?.send(msg)
注意,在这个例子中,您不能将这些行添加到活动的onCreate()方法中的bindService()语句之后,因为remoteSrvc只有在连接启动后才会获得一个值。但是你可以把它添加到ServiceConnection类的onServiceConnected()方法中。
注意
在前面的代码中,没有采取任何预防措施来确保连接的完整性。您应该为生产性代码添加健全性检查。此外,在onStop()方法中解除服务绑定。
服务发送的数据
到目前为止,我们一直在讨论从服务客户端发送到服务的消息。从服务到服务客户机的相反方向发送数据也是可能的;最好是通过在客户端内部使用一个额外的Messenger、一个广播消息或者一个ResultReceiver类来实现。
对于第一种方法,在服务客户端中提供另一个Handler和Messenger,一旦客户端接收到一个onServiceConnected()回调,发送一个Message给服务,第二个Messenger由replyTo参数传递。
internal class InHandler(val ctx: Context) : Handler() {
override
fun handleMessage(msg: Message) {
// do something with the message from the service
}
}
class MainActivity : AppCompatActivity() {
private var remoteSrvc:Messenger? = null
private var backData:Messenger? = null
private val myConn = object : ServiceConnection {
override
fun onServiceConnected(className: ComponentName,
service: IBinder) {
remoteSrvc = Messenger(service)
backData = Messenger(
InHandler(this@MainActivity))
// establish backchannel
val msg0 = Message.obtain()
msg0.replyTo = backData
remoteSrvc?.send(msg0)
// handle forward (client -> service)
// connectivity...
}
override
fun onServiceDisconnected(clazz: ComponentName) {
remoteSrvc = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// bind to the service, use ID from the manifest!
val intent = Intent("<PCKG>.START_SERVICE")
intent.setPackage("<PCKG>")
val flags = Context.BIND_AUTO_CREATE
bindService(intent, myConn, flags)
}
}
然后,服务可以使用这个消息,提取replyTo属性,并使用它向服务客户机发送消息。
internal class IncomingHandler(val ctx: Context) :
Handler() {
override
fun handleMessage(msg: Message) {
val s = msg.data.getString("MyString")
val repl = msg.replyTo
Toast.makeText(ctx, s, Toast.LENGTH_SHORT).show()
Log.e("IncomingHandler", "!!! " + s)
Log.e("IncomingHandler", "!!! replyTo = " + repl)
// If not null, we can now use the 'repl' to send
// messages to the client. Of course we can save
// it elsewhere and use it later as well
if(repl != null) {
val thr = Thread( object : Runnable {
override fun run() {
Thread.sleep(3000)
val msg = Message.obtain()
val bundle = Bundle()
bundle.putString("MyString",
"A reply message to be sent")
msg.data = bundle
repl?.send(msg)
}
} )
thr.start()
}
}
}
其他两种方法,使用广播消息或ResultReceiver类,在第 5 和 12 章节中处理。
服务子类
到目前为止,我们一直使用android.app.Service作为我们描述的服务的基类。不过,Android 提供的其他类也可以用作基类,只是语义不同。对于 Android 8.0,有不少于 20 个服务类或基类可以使用。你可以在 Android API 文档的“已知直接子类”部分看到它们。
注意
在写这本书的时候,你可以在 https://developer.android.com/reference/android/app/Service.html 找到这个文档。
最重要的服务类别有以下三种:
-
这是我们到目前为止一直在用的一款。这是最基本的服务类。除非您在中使用多线程*,否则服务类或服务被显式配置为在另一个进程中执行,服务将在服务调用者的主线程中运行。如果这是 GUI 线程,并且您不希望服务调用运行得很快,那么强烈建议您将服务活动发送到后台线程。*
-
android.app.IntentService:虽然服务在设计上不会自然地同时处理主线程传入的启动请求,但是IntentService使用一个专用的工作线程来接收多个启动消息。尽管如此,它只使用一个线程来处理启动请求,所以它们被一个接一个地执行。IntentService类负责正确地停止服务,所以你自己不需要关心这个。您必须在被覆盖的onHandleIntent()方法中为每个启动请求提供服务要完成的工作。因为基本上你不需要其他任何东西,所以IntentService服务很容易实现。注意,从 Android 8.0 (API 级别 26)开始,限制适用于后台进程,因此在适当的情况下,可以考虑使用JobIntentService类来代替。 -
android.support.v4.app.JobIntentService:这使用了一个JobScheduler来对服务执行请求进行排队。从 Android 8.0(API 26 级)开始,考虑使用这个服务基类进行后台服务。要实现这样的服务,基本上必须创建一个JobIntentService的子类,并覆盖方法onHandleWork(intent: Intent): Unit来包含服务的工作负载。
服务生命周期
在前面几节中描述了各种服务特征之后,从鸟瞰的角度来看,服务的实际生命周期可能比活动的生命周期更容易。但是,要注意服务可能会在后台运行。此外,因为服务更容易受到 Android 操作系统强制停止的影响,所以在与服务客户端通信时,它们可能需要特别注意。
在您的服务实现中,您可以覆盖这里列出的任何生命周期回调,例如,在开发或调试时记录服务调用信息:
-
onCreate() -
onStartCommand() -
onBind() -
onUnbind() -
onRebind() -
onDestroy()
图 4-1 显示了服务生命周期的概述
图 4-1
服务生命周期
更多服务特征
以下是关于服务的更多观察:
-
服务与
AndroidManifest.xml中的活动一起被声明。一个常见的问题是它们如何相互作用。有人需要调用服务来使用它们,但这也可以通过其他服务、其他活动甚至其他应用来完成。 -
出于性能和稳定性原因,不要在活动的
onResume()和onPause()方法期间绑定或解除绑定。如果您只需要在活动可见时与服务交互,请在onStart()和onStop()方法中绑定和解除绑定。如果在活动停止时和在后台也需要服务连接,请在onCreate()和onRestore()方法中绑定和解除绑定。 -
在远程连接操作中(服务存在于另一个应用中),捕捉并处理
DeadObjectException异常。 -
如果您覆盖了服务的
onStartCommand(intent: Intent, flags: Int, startId: Int)方法,首先确保也调用方法super.onStartCommand(),除非您有充分的理由不这样做。接下来,对传入的flags参数做出适当的反应,该参数告知这是否是一个自动后续启动请求,因为之前的启动尝试失败了。最终这个方法在离开onStartCommand()方法后返回一个描述服务状态的整数;有关详细信息,请参见 API 文档。 -
从服务外部调用
stopService()或从服务内部调用stopSelf()并不能保证服务立即停止。预计这项服务会持续一段时间,直到 Android 真的停止它。 -
如果服务没有被设计成对绑定请求做出反应,并且您覆盖了服务的
onBind()方法,那么它应该返回null。 -
虽然没有明确禁止,但是对于设计用于通过绑定与服务客户端通信的服务,可以考虑禁止由
startService()启动该服务。在这种情况下,您必须在bindService()方法调用中提供Context.BIND_AUTO_CREATE标志。
五、广播
Android 广播是遵循发布-订阅模式的消息。它们通过 Android 操作系统发送,内部被 Android 操作系统隐藏,因此发布者和订阅者只能看到一个简单的异步接口来发送和接收消息。广播可以由 Android 操作系统本身、标准应用以及系统上安装的任何其他应用发布。同样,任何应用都可以配置或编程为接收他们感兴趣的广播消息。像活动一样,广播可以显式或隐式路由,这是广播发送方决定的责任。
广播接收器要么在AndroidManifest.xml文件中声明,要么以编程方式声明。从 Android 8.0 (API level 26)开始,Android 的开发人员已经放弃了 XML 和广播接收器的编程声明之间通常的对称性,转而使用隐含的意图。原因是对在后台模式下运行的进程(尤其是与广播相关的进程)施加限制的总体想法导致了 Android 操作系统上的高负载,大大降低了设备的速度,并导致了糟糕的用户体验。出于这个原因,AndroidManifest.xml内部广播接收器的声明现在被限制在一个更小的用例集合中。
注意
你会想要编写可以在 Android 8.0 和更新版本中运行的现代应用。出于这个原因,认真对待这个隐含意图的广播限制,并使你的应用在这个限制内运行。
明确的广播
显式广播是以这样一种方式发布的广播,即只有一个接收方被其寻址。这通常只有在广播发布者和订阅者都是同一个应用的一部分时才有意义,或者如果它们之间有很强的功能依赖性,则是同一个应用集合的一部分。
本地广播和远程广播是有区别的:本地广播接收者必须驻留在同一个 app 中,它们运行速度很快,接收者不能在AndroidManifest.xml内部声明。相反,本地广播接收机必须使用程序化注册方法。此外,您必须使用以下内容来发送本地广播消息:
// send local broadcast
LocalBroadcastManager.getInstance(Context).
sendBroadcast(...)
远程广播接收机,另一方面,可以驻留在同一个 app 中,它们比较慢,可以用AndroidManifest.xml来声明。要发送远程广播,请编写以下内容:
// send remote broadcast (this App or other Apps)
sendBroadcast(...)
注意
出于性能原因,本地广播应优先于远程广播。不能使用AndroidManifest.xml来声明本地接收器的明显缺点并没有太大关系,因为从 Android 8.0 (API level 26)开始,在清单文件中声明广播接收器的用例无论如何都是有限的。
明确的本地广播
要在同一个应用中向本地广播接收器发送本地广播消息,您需要编写以下代码:
val intent = Intent(this, MyReceiver::class.java)
intent.action = "de.pspaeth.simplebroadcast.DO_STH"
intent.putExtra("myExtra", "myExtraVal")
Log.e("LOG", "Sending broadcast")
LocalBroadcastManager.getInstance(this).
sendBroadcast(intent)
Log.e("LOG", "Broadcast sent")
这里,MyReceiver是接收器类。
class MyReceiver : BroadcastReceiver() {
override
fun onReceive(context: Context?, intent: Intent?) {
Toast.makeText(context, "Intent Detected.",
Toast.LENGTH_LONG).show()
Log.e("LOG", "Received broadcast")
Thread.sleep(3000)
// or real work of course...
Log.e("LOG", "Broadcast done")
}
}
对于本地广播,接收者必须在代码中声明。为了避免资源泄漏,我们在onCreate()中创建和注册了接收者,并在onDestroy()中取消注册。
class MainActivity : AppCompatActivity() {
private var bcReceiver:BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
bcReceiver = MyReceiver()
val ifi:IntentFilter =
IntentFilter("de.pspaeth.myapp.DO_STH")
LocalBroadcastManager.getInstance(this).
registerReceiver(bcReceiver, ifi)
}
override fun onDestroy() {
super.onDestroy()
// ...
LocalBroadcastManager.getInstance(this).
unregisterReceiver(bcReceiver)
}
}
显式远程广播
我们已经指出,我们可以将远程类型的广播消息发送到其他应用或接收者所在的同一应用。区别在于数据是如何发送的。对于远程消息,数据通过 IPC 通道传输。现在,要向同一个应用发送这样的远程广播消息,您需要编写以下代码:
val intent = Intent(this, MyReceiver::class.java)
intent.action = "de.pspaeth.myapp.DO_STH"
intent.putExtra("myExtra", "myExtraVal")
sendBroadcast(intent)
在接收端,对于远程消息,必须在清单文件中声明接收者和。
<application ...>
...
<receiver android:name=".MyReceiver">
<intent-filter>
<action android:name=
"de.pspaeth.myapp.DO_STH">
</action>
</intent-filter>
</receiver>
</application>
了解本地和远程广播之间的差异,记住以下几点很有帮助:
-
本地显式广播:
发送方使用显式的接收方类,接收方必须以编程方式声明,发送方和接收方都使用
LocalBroadcastManager来发送消息和注册接收方。 -
远程显式广播:
发送方使用显式的接收方类,接收方必须在
AndroidManifest.xml中声明。
对于负责处理接收到的广播的类,与显式本地广播相比没有区别。
class MyReceiver : BroadcastReceiver() {
override
fun onReceive(context: Context?, intent: Intent?) {
// handle incoming broadcasts...
}
}
发送到其他应用的明确广播
显性广播的发送者和接收者可以生活在不同的应用中。为此,您不能再使用我们之前使用的 intent 构造函数。
val intent = Intent(this, MyReceiver::class.java)
intent.action = "de.pspaeth.myapp.DO_STH"
// add other coords...
sendBroadcast(intent)
这是因为接收类(这里是MyReceiver)不是类路径的一部分。然而,我们可以使用另一种结构来代替。
val intent = Intent()
intent.component = ComponentName("de.pspaeth.xyz",
"de.pspaeth.xyz.MyReceiver")
intent.action = "de.pspaeth.simplebroadcast.DO_STH"
// add other coords...
sendBroadcast(intent)
这里,ComponentName的第一个参数是接收包的包字符串,第二个参数是类名。
警告
除非你正在向自己构建的应用广播,否则这种发送明确广播的方式只有有限的用途。另一个应用的开发人员可能很容易决定更改类名,然后您使用广播与另一个应用的通信将会中断。
隐式广播
隐式广播是具有不确定数量的可能接收者的广播。对于显式广播,您了解到我们必须通过使用指向接收方组件的构造函数来构建相应的意图:val intent = Intent(this, TheReceiverClass::class.java)。与此相反,对于隐式广播,我们不再指定接收者,而是提示哪些组件可能对接收它感兴趣。这里有一个例子:
val intent = Intent()
intent.action = "de.pspaeth.myapp.DO_STH"
sendBroadcast(intent)
在这里,我们实际上表达如下:“向所有对动作de.pspaeth.myapp.DO_STH感兴趣的接收者发送广播消息。”Android OS 确定哪些组件有资格接收这样的广播消息;这可能导致零个、一个或多个实际收件人。
在开始编程隐式广播之前,您必须做出三个决定。
-
我们想听系统广播吗?
Android 存在大量预定义的广播消息类型。在你用 Android Studio 安装的 Android SDK 里面,在
SDK_INST_DIR/platforms/VERSION/data/broadcast_actions.txt,你可以找到一个系统广播动作的列表。如果我们想要收听这样的消息,我们只需要按照本章后面的描述对适当的广播接收机进行编程。在在线文本指南的“系统广播”部分,您会找到系统广播的完整列表。 -
我们如何对广播消息类型进行分类?
广播发送者和广播接收者通过意图过滤器匹配来连接,就像活动一样。正如在第三章中所讨论的,当描述活动的意图过滤器时,广播的分类是三重的:首先是一个强制的动作说明符,其次是一个类别,第三是一个可以用来定义过滤器的数据和类型说明符。我们将在本章后面描述这个匹配过程。
-
我们是在向本地广播还是远程广播前进?
如果所有的广播都完全发生在你的应用中,你应该使用本地广播来发送和接收消息。对于隐式广播,这种情况可能不会太常见,但对于大型复杂的应用,这是完全可以接受的。如果涉及系统广播或来自其他应用的广播,您必须使用远程广播。后者是大多数例子中的默认情况,所以您会经常看到这种模式。
意图过滤器匹配
广播接收机通过声明动作、类别、数据说明符来表示接受广播。
先说动作。这些只是没有任何语法限制的字符串。更彻底地观察它们,您会看到我们首先有一组或多或少严格定义的预定义动作名称。我们在第三章中列出了它们。此外,您可以定义自己的操作。惯例是使用包名加一个点,然后是一个动作说明符。你不一定要遵循这个惯例,但是强烈建议你这样做,这样你的应用就不会和其他应用混淆。如果不指定任何其他筛选条件,指定您在筛选器中指定的特定操作的发件人将会到达所有匹配的收件人。
-
为了匹配意图过滤器,接收方指定的动作必须与发送方指定的动作相匹配。对于隐式广播,一次广播可以寻址零个、一个或多个接收器。
-
接收者可以指定一个以上的过滤器。如果其中一个过滤器包含指定的动作,这个特定的过滤器将匹配广播。
表 5-1 显示了一些例子。
表 5-1
动作匹配
|接收器
|
发报机
|
比赛
|
| --- | --- | --- |
| 一个过滤器action = "com.xyz.ACTION1" | action = "com.xyz.ACTION1" | 是 |
| 一个过滤器action = "com.xyz.ACTION1" | action = "com.xyz.ACTION2" | 不 |
| 两个过滤器action = "com.xyz.ACTION1"``action = "com.xyz.ACTION2" | action = "com.xyz.ACTION1" | 是 |
| 两个过滤器action = "com.xyz.ACTION1"``action = "com.xyz.ACTION2" | action = "com.xyz.ACTION3" | 不 |
除了动作,一个类别说明符可以用来限制一个意图过滤器。我们在第三章中列出了几个预定义的类别,但是你也可以定义自己的类别。就像对于动作一样,对于你自己的类别你应该遵循将你的应用的包名加到你的类别名前面的命名惯例。一旦在意图匹配过程中发现动作中的匹配,发送者声明的所有类别也必须出现在接收者的意图过滤器中,以使匹配更进一步。
- 一旦意图过滤器内的动作匹配广播,并且过滤器也包含类别列表,则只有这样的广播将匹配发送者指定的类别全部包含在接收者的类别列表中的过滤器。
表 5-2 显示了一些例子(只有一个过滤器;如果有几个过滤器,匹配以“或”为基础。
表 5-2
类别匹配
|接收器动作
|
接收者类别
|
发报机
|
比赛
|
| --- | --- | --- | --- |
| com.xyz.ACT1 | com.xyz.cate1 | action = "com.xyz.ACT1" | 是 |
| com.xyz.ACT1 | | action = "com.xyz.ACT1"``categ = "com.xyz.cate1" | 不 |
| com.xyz.ACT1 | com.xyz.cate1 | action = "com.xyz.ACT1"``categ = "com.xyz.cate1" | 是 |
| com.xyz.ACT1 | com.xyz.cate1 | action = "com.xyz.ACT1"``categ = "com.xyz.cate1"``categ = "com.xyz.cate2" | 不 |
| com.xyz.ACT1 | com.xyz.cate1``com.xyz.cate2 | action = "com.xyz.ACT1"``categ = "com.xyz.cate1"``categ = "com.xyz.cate2" | 是 |
| com.xyz.ACT1 | com.xyz.cate1``com.xyz.cate2 | action = "com.xyz.ACT1"``categ = "com.xyz.cate1" | 是 |
| com.xyz.ACT1 | any | action = "com.xyz.ACT2"``categ = any | 不 |
第三,数据和类型说明符允许过滤数据类型。这种说明符是下列说明符之一:
-
类型:MIME 类型,例如
"text/html"或"text/plain" -
数据:某数据 URI,例如“
http://xyz.com/type1" -
数据和类型:两者都有
这里,data元素允许通配符匹配。
-
假定的 动作 和 类别 **匹配:**如果发送方指定的 MIME 类型包含在接收方允许的 MIME 类型列表中,则类型过滤器元素匹配。
-
假定的 动作 和 类别 匹配: A 数据过滤元素匹配如果发送方指定的数据 URI 匹配接收方允许的数据 URIs 列表中的任何一个(通配符匹配可能适用)。
-
假定 动作 和 类别 **匹配:**如果 MIME 类型和数据 URI 匹配,即包含在接收者的指定列表中,则数据和类型过滤元素匹配。
表 5-3 显示了一些例子(只有一个过滤器;如果有几个过滤器,匹配以“或”为基础。
表 5-3
数据匹配
|接收器类型
|
接收者 URI
。* =任何字符串
|
发报机
|
比赛
|
| --- | --- | --- | --- |
| text/html | | type = "text/html" | 是 |
| text/html``text/plain | | type = "text/html" | 是 |
| text/html``text/plain | | type = "image/jpeg" | 不 |
| | http://a.b.c/xyz | data = "http://a.b.c/xyz" | 是 |
| | http://a.b.c/xyz | data = "http://a.b.c/qrs" | 不 |
| | http://a.b.c/xyz/.* | data = "http://a.b.c/xyz/3" | 是 |
| | http://.*/xyz | data = "http://a.b.c/xyz" | 是 |
| | http://.*/xyz | data = "http://a.b.c/qrs" | 不 |
| text/html | http://a.b.c/xyz/.* | type = "text/html"``data = "http://a.b.c/xyz/1" | 是 |
| text/html | http://a.b.c/xyz/.* | type = "image/jpeg"``data = "http://a.b.c/xyz/1" | 不 |
主动或等待接听
应用必须处于哪个状态才能接收隐式广播?如果我们希望广播接收器只在系统中注册,并且只在匹配的广播到达时按需启动,则必须在应用的清单文件中指定侦听器。然而,对于隐式广播,这不能自由地进行。它仅适用于预定义的系统广播,如在线文本指南的“系统广播”一节所列。
注意
对清单中指定的隐式意图过滤器的这种限制是在 Android 8.0 (API 级别 26)中引入的。在此之前,可以在清单文件中指定任何隐式过滤器。
但是,如果您从应用内部以编程方式启动广播侦听器,并且该应用正在运行,则您可以根据需要定义任意数量的隐式广播侦听器,并且对于广播来自系统、您的应用还是其他应用没有任何限制。同样,对于可用的动作或类别名称也没有限制。
因为监听引导完成事件包含在清单文件中允许的监听器列表中,所以您可以自由地启动应用作为活动或服务,并且在这些应用中,您可以注册任何隐式监听器。但这意味着从 Android 8.0 开始,你可以合法地绕过这些限制。只是要注意,如果出现资源短缺,这类应用可能会被 Android OS 杀死,所以你必须采取适当的预防措施。
发送隐式广播
要准备发送隐式广播,请按如下方式指定操作、类别、数据和额外数据:
val intent = Intent()
intent.action = "de.pspaeth.myapp.DO_STH"
intent.addCategory("de.pspaeth.myapp.CATEG1")
intent.addCategory("de.pspaeth.myapp.CATEG2")
// ... more categories
intent.type = "text/html"
intent.data = Uri.parse("content://myContent")
intent.putExtra("EXTRA_KEY", "extraVal")
intent.flags = ...
只有动作是强制性的;所有其他的都是可选的。现在要发送广播,您需要为远程广播编写以下代码:
sendBroadcast(intent)
对于本地广播,您应该这样写:
LocalBroadcastManager.getInstance(this).
sendBroadcast(intent)
this必须是Context或其子类;如果代码来自一个活动或服务类内部,它将完全像这里显示的那样工作。
对于远程消息,还有一种变体,每次向一个适用的接收器发送广播。
...
sendOrderedBroadcast(...)
...
这使得接收者顺序地获得消息,并且每个接收者可以取消,通过使用BroadcastReceiver.abortBroadcast()将消息转发给队列中的下一个接收者。
接收隐式广播
要接收隐式广播,对于一组有限的广播类型(参见在线文本指南的“系统意图过滤器”一节),您可以在AndroidManifest.xml中指定一个BroadcastListener,如下所示:
<application ...>
...
<receiver android:name=".MyReceiver">
<intent-filter>
<action android:name=
"com.xyz.myapp.DO_STH" />
<category android:name=
"android.intent.category.DEFAULT"/>
<category android:name=
"com.xyz.myapp.MY_CATEG"/>
<data android:scheme="http"
android:port="80"
android:host="com.xyz"
android:path="items/7"
android:mimeType="text/html" />
</intent-filter>
</receiver>
</application>
这里显示的<data>元素只是一个例子;参见第三章了解所有可能性。
与此相反,向代码中添加隐式广播的编程侦听器是不受限制的。
class MainActivity : AppCompatActivity() {
private var bcReceiver:BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
bcReceiver = MyReceiver()
val ifi:IntentFilter =
IntentFilter("de.pspaeth.myapp.DO_STH")
registerReceiver(bcReceiver, ifi)
}
override fun onDestroy() {
super.onDestroy()
// ...
unregisterReceiver(bcReceiver)
}
}
MyReceiver是类android.content.BroadcastReceiver的一个实现。
收听系统广播
要收听系统广播,请参阅在线文本指南的“系统广播”一节中的列表。您可以像前面展示的那样使用编程注册。对于它们中的大多数,您不能使用自 Android 8.0 (API 级别 26)以来强加的后台执行限制的清单注册方法。但是,对于其中的一些,您也可以使用清单文件来指定侦听器。
-
ACTION_LOCKED_BOOT_COMPLETED,ACTION_BOOT_COMPLETED:应用可能需要这些来安排作业、警报等等。
-
ACTION_USER_INITIALIZE、"android.intent.action.USER_ADDED"、"android.intent.action.USER_REMOVED":这些受特权权限的保护,所以用例是有限的。
-
"android.intent.action.TIME_SET"、ACTION_TIMEZONE_CHANGED、ACTION_NEXT_ALARM_CLOCK_CHANGED:这些都是时钟 app 需要的。
-
ACTION_LOCALE_CHANGED:区域设置已更改,发生这种情况时,应用可能需要更新其数据。
-
ACTION_USB_ACCESSORY_ATTACHED、ACTION_USB_ACCESSORY_DETACHED、ACTION_USB_DEVICE_ATTACHED、、、ACTION_USB_DEVICE_DETACHED:这些都是 USB 相关的事件。
-
ACTION_CONNECTION_STATE_CHANGED、ACTION_ACL_ CONNECTED、ACTION_ACL_DISCONNECTED:这些是蓝牙事件。
-
ACTION_CARRIER_CONFIG_CHANGED,TelephonyIntents。ACTION_*_SUBSCRIPTION_CHANGED,"TelephonyIntents. SECRET_CODE_ACTION":OEM 电话应用可能需要接收这些广播。
-
LOGIN_ACCOUNTS_CHANGED_ACTION:一些应用需要这一点来为新帐户和更改的帐户设置预定操作。
-
ACTION_PACKAGE_DATA_CLEARED:操作系统设置应用清除数据;一个正在运行的应用可能对此感兴趣。
-
ACTION_PACKAGE_FULLY_REMOVED:如果某些应用被卸载并且其数据被移除,可能需要通知相关应用。
-
ACTION_NEW_OUTGOING_CALL:这会拦截呼出的电话。
-
ACTION_DEVICE_OWNER_CHANGED:一些应用可能需要接收此消息,以便知道设备的安全状态已更改。
-
ACTION_EVENT_REMINDER:这是由日历供应器发送的,用于向日历应用发布事件提醒。
ACTION_MEDIA_MOUNTED,ACTION_MEDIA_CHECKING、ACTION_MEDIA_UNMOUNTED、ACTION_MEDIA_EJECT、ACTION_MEDIA_UNMOUNTABLE、ACTION_MEDIA_REMOVED、ACTION_MEDIA_BAD_REMOVAL:应用可能需要了解用户与设备的物理交互。
-
SMS_RECEIVED_ACTION,WAP_PUSH_RECEIVED_ACTION:这些都是短信接收应用所需要的。
增加广播的安全性
广播消息的安全性由权限系统处理,该系统将在第七章中得到更详细的处理。
在下面几节中,我们将区分显式和隐式广播。
保护明确的广播
对于非本地广播(即不使用LocalBroadcastManager),权限可以在双方指定,接收者和发送者。对于后者,广播发送方法具有重载版本,包括一个权限说明符:
...
val intent = Intent(this, MyReceiver::class.java)
...
sendBroadcast(intent, "com.xyz.theapp.PERMISSION1")
...
这表示向受com.xyz.theapp.PERMISSION1保护的接收器发送广播。当然,您应该在这里编写自己的包名,并使用适当的权限名。
相反,在没有许可说明的情况下发送广播可能会针对具有和不具有许可保护的接收者:
...
val intent = Intent(this, MyReceiver::class.java)
...
sendBroadcast(intent)
...
这意味着在发送方指定权限并不意味着告诉接收方发送方受到任何形式的保护。
为了向接收方添加权限,我们首先需要在应用级别的AndroidManifest.xml中声明使用它。
<manifest ...>
<uses-permission android:name=
"com.xyz.theapp.PERMISSION1"/>
...
<application ...
接下来,我们将它显式地添加到同一个清单文件中的 receiver 元素中。
<receiver android:name=".MyReceiver"
android:permission="com.xyz.theapp.PERMISSION1">
<intent-filter>
<action android:name=
"com.xyz.theapp.DO_STH" />
</intent-filter>
</receiver>
这里,MyReceiver是android.content.BroadcastReceiver的一个实现。
第三,由于这是一个自定义权限,您必须在清单文件中声明它自己。
<manifest ...>
<permission android:name=
"com.xyz.theapp.PERMISSION1"/>
...
<permission>允许更多的属性;请参阅联机文本指南中的“清单顶级条目”一节,了解有关保护级别的更多信息。第七章详细解释了它的细节和含义。
对于非定制权限,不需要使用<permission>元素。
警告
当您尝试发送广播时,在发送方指定权限而在接收方没有匹配的权限会自动失败。也没有日志记录条目,所以要小心发送端的权限。
如果通过LocalBroadcastManager使用本地广播,则不能在发送方或接收方指定权限。
保护隐式广播
像非本地显式广播一样,隐式广播中的权限可以在广播发送方和接收方指定。在发送方,您应该编写以下内容:
val intent = Intent()
intent.action = "de.pspaeth.myapp.DO_STH"
// ... more intent coordinates
sendBroadcast(intent, "com.xyz.theapp.PERMISSION1")
这表示向受com.xyz.theapp.PERMISSION1额外保护的所有匹配接收器发送广播。当然,您应该在这里写下自己的包名,并使用适当的权限名。至于用于隐式广播的通常的发送者-接收者匹配过程,添加许可种类作为附加的匹配标准,因此如果有几个接收者候选查看意图过滤器,为了实际接收该广播,只有那些额外提供该许可标志的将被挑选出来。
隐式广播需要注意的另一件事是在AndroidManifest.xml中指定权限用法。因此,为了使该发件人能够使用权限,请将以下内容添加到清单文件中:
<uses-permission android:name="com.xyz.theapp.
PERMISSION1"/>
和露骨的广播一样。在没有许可说明的情况下发送广播可以寻址具有和不具有许可保护的接收器。
...
sendBroadcast(intent)
...
这意味着在发送方指定权限不应该告诉接收方发送方受到任何形式的保护。
为了使接收器能够获得这样的广播,必须将许可添加到代码中,如下所示:
private var bcReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
bcReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?,
intent: Intent?) {
// do s.th. when receiving...
}
}
val ifi: IntentFilter =
IntentFilter("de.pspaeth.myapp.DO_STH")
registerReceiver(bcReceiver, ifi,
"com.xyz.theapp.PERMISSION1", null)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(bcReceiver)
}
此外,您必须在接收方的清单文件中定义权限并声明使用它。
...
<uses-permission android:name=
"com.xyz.theapp.PERMISSION1" />
<permission android:name=
"com.xyz.theapp.PERMISSION1" />
...
同样,对于非定制权限,您不需要使用<permission>元素。有关权限的更多信息,请参见第七章。
注意
作为提高安全性的附加手段,在适用的情况下,您可以使用Intent.setPackage()来限制可能的接收者。
从命令行发送广播
对于可以通过 Android 调试桥 (ADB)连接的设备,可以在开发 PC 上使用 shell 命令发送广播消息(参见第十八章)。这里有一个向包de.pspaeth.simplebroadcast的专用接收者MyReceiver发送动作de.pspaeth.myapp.DO_STH的例子(这是一个明确的广播消息):
./adb shell am broadcast -a de.pspaeth.myapp.DO_STH \
de.pspaeth.simplebroadcast MyReceiver
要获得以这种方式发送广播的完整概要,可以使用如下的 shell:
./adb shell
am
该命令将向您展示使用该am命令创建广播消息和做其他事情的所有可能性。
广播随笔
以下是一些关于广播的附加信息:
-
您还可以在回调方法
onPause()和onResume()中注册和注销编程管理的接收器。显然,与使用onCreate()/onDestroy()对相比,注册和注销会更频繁。 -
一个当前正在执行的
onReceive()方法会将进程优先级升级到“前台”级别,防止 Android OS 杀死接收进程。只有在极度资源短缺的情况下,这种情况才会发生。 -
如果您在
onReceive()中有长时间运行的进程,您可能会想到在后台线程上运行它们,提前完成onReceive()。但是由于完成onReceive()后进程优先级会恢复到正常水平,所以你的后台进程被杀的可能性比较大,坏了你的 app。你可以通过使用Context.goAsync()然后启动一个AsyncTask来防止这种情况(在最后你必须在从goAsync()获得的PendingResult对象上调用finish()来最终释放资源),或者你可以使用一个JobScheduler。 -
自定义权限,就像我们在“保护隐式广播”一节中使用的一样,在安装应用时注册。因此,定义自定义权限的应用必须在应用使用它们之前安装。
-
小心通过隐式广播发送敏感信息。潜在的恶意应用也可能试图接收它们。至少,您可以通过在发送方指定权限来保护广播。
-
为了清晰起见,也为了不与其他应用混淆,请始终使用名称空间作为广播操作和权限名称。
-
避免从广播开始活动。这违背了 Android 的可用性原则。
六、内容供应器
本章将介绍内容供应器。
内容供应器框架
内容供应器框架允许以下内容:
-
使用其他应用提供的(结构化)数据
-
提供(结构化)数据供其他应用使用
-
将数据从一个应用复制到另一个应用
-
向搜索框架提供数据
-
向与数据相关的特殊 UI 小部件提供数据
-
凭借定义良好的标准化接口完成所有这些工作
传递的数据可以具有严格定义的结构,例如数据库中具有定义的列名和类型的行,但它也可以是没有任何关联语义的文件或字节数组。
如果您的应用关于数据存储的要求不符合前面的任何情况,您就不需要实现内容提供者组件。请改用普通的数据存储选项。
注意
没有严格禁止应用向自己的组件提供数据或使用自己的数据提供者来访问内容;然而,在考虑内容供应器时,您通常会想到应用间的数据交换。但是如果你需要的话,你总是可以把应用内的数据交换模式看作是应用间通信的一个简单的特例。
如果我们想要创建内容感知型应用,无论是提供内容还是消费内容,都需要考虑以下主要问题:
-
app 如何提供内容?
-
应用如何访问其他应用提供的内容?
-
应用如何处理其他应用提供的内容?
-
我们如何保护所提供的数据?
我们将在接下来的章节中探讨这些主题。图 6-1 为轮廓图。
图 6-1
内容供应器框架
提供内容
内容可以由您的应用以及系统应用提供。想想相机拍摄的照片或联系人列表中的联系人。如果我们首先看内容提供方,那么内容提供者框架就更容易理解了。在后面的部分中,我们还将研究消费者和其他主题。
首先,我们需要知道数据存储在哪里。然而,内容提供者框架并不假设数据实际上来自哪里。它可以存在于文件、数据库、内存存储或任何你能想到的地方。这改善了应用的维护。例如,在早期阶段,数据可能来自文件,但后来你可能会转向数据库或云存储,潜在的消费者不必关心这些变化,因为他们不必改变他们访问你的内容的方式。因此,内容提供者框架为您的数据提供了一个抽象层。
您需要实现来提供内容的单一接口是下面的抽象类:
android.content.ContentProvider
在接下来的部分中,我们将从用例的角度来看这个类的实现。
正在初始化提供程序
您必须实现以下方法:
ContentProvider.onCreate()
当内容提供者被实例化时,这个方法被 Android 操作系统调用。您可以在这里初始化内容提供者。但是,您应该避免将耗时的初始化过程放在这里,因为实例化并不一定意味着内容提供者将被实际使用。
如果你在这里没有什么有趣的事情要做,就把它实现为一个空方法。
为了在实例化时找到更多关于内容提供者运行环境的信息,您可以覆盖它的attachInfo()方法。在那里,您将被告知内容提供者运行的上下文,并获得一个ProviderInfo对象。只是别忘了也从内部呼叫super.attachInfo()。
查询数据
对于查询类似数据库的数据集,有一个方法必须实现,另外两个方法可以选择实现。
abstract fun query( // ----- Variant A -----
uri:Uri,
projection:Array<String>,
selection:String,
selectionArgs:Array<String>,
sortOrder:String) : Cursor
// ----- Variant B -----
// You don't have to implement this. The default
// implementation calls variant A, but disregards the
// 'cancellationSignal' argument.
fun query(
uri:Uri,
projection:Array<String>,
selection:String,
selectionArgs:Array<String>,
String sortOrder:String,
cancellationSignal:CancellationSignal) : Cursor
// ----- Variant C -----
// You don't have to implement this. The default
// implementation converts the bundle argument to
// appropriate arguments for calling variant B.
// The bundle keys used are:
// ContentResolver.QUERY_ARG_SQL_SELECTION
// ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS
// ContentResolver.QUERY_ARG_SQL_SORT_ORDER -or-
// ContentResolver.QUERY_ARG_SORT_COLUMNS
// (this being a String array)
fun query(
uri:Uri,
projection:Array<String>,
queryArgs:Bundle,
cancellationSignal:CancellationSignal) : Cursor
这些方法并不用于呈现图像和声音等文件数据。但是,返回文件数据的链接或标识符是可以接受的。
在下面的列表中,我按名称和变量描述了所有参数:
-
uri:这是一个重要的参数,指定查询在数据空间中的类型坐标。通过适当地指定这个参数,内容消费者将会知道他们对什么样的数据感兴趣。因为 URIs 是如此重要,我们在他们自己的部分描述他们;请参见下面“提供内容”一节中的“设计内容 URIs”。该参数对于变型 A、B 和 c 具有相同的含义。 -
projection:这将告诉实现请求者对哪些列感兴趣。看看存储数据的 SQL 数据库类型,它列出了应该包含在结果中的列名。然而,对于一对一的映射没有严格的要求。请求者可能会要求一个选择参数X,而X的值可能会以你可能想到的任何方式计算出来。如果null,返回所有字段。该参数对于变型 A、B 和 c 具有相同的含义。 -
selection:这仅适用于变量 A 和 b。这为要返回的数据指定了一个选择。内容提供者框架没有假设这个选择参数应该是什么样子。这完全取决于实现,内容请求者必须服从实现的定义。然而,在许多情况下,您会有类似于 SQL 选择字符串的东西,比如这里的name = Jean AND age < 45。如果null,返回所有数据集。 -
selectionArgs:选择参数可能包含类似?的占位符。如果是这样,要为占位符插入的值在该数组中指定。同样,这个框架在这里没有做严格的假设,但是在大多数情况下,?充当占位符,就像在 SQL 中的name = ? AND age < ?一样。如果没有选择占位符,这可能是null。 -
sortOrder:这仅适用于变量 A 和 b。这指定了要返回的数据的排序顺序。内容提供者框架在这里没有规定语法,但是对于类似 SQL 的访问,这通常类似于name DESC, or ASC。 -
queryArgs:这仅适用于变量 c。所有三个选择、选择参数和排序顺序可以使用android.os.Bundle对象指定,也可以不指定。按照惯例,对于类似 SQL 的查询,包键如下:-
ContentResolver.QUERY_ARG_SQL_SELECTION -
ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS -
ContentResolver.QUERY_ARG_SQL_SORT_ORDER
-
-
cancellationSignal:这仅适用于变体 B 和 c,如果这不是null,您可以用它来取消当前操作。Android 操作系统会适当地通知请求者。
所有的查询方法都应该返回一个android.database.Cursor对象。这允许您迭代数据集,约定每个数据集包含一个_id键控技术 ID。参见下面“提供内容”一节中的“基于 AbstractCursor 的光标类”和“设计内容 URIs ”,了解如何设计合适的光标对象。
修改内容
内容供应器不仅允许你阅读内容,还允许你修改内容。为此,存在以下方法:
abstract fun insert(
Uri uri:Uri,
values:ContentValues) : Uri
// You don't have to overwrite this, the default
// implementation correctly iterates over the input
// array and calls insert(...) on each element.
fun bulkInsert(
uri:Uri,
values:Array<ContentValues>) : Int
abstract fun update(
uri:Uri,
values:ContentValues,
selection:String,
selectionArgs:Array<String>) : Int
abstract fun delete(
uri:Uri,
selection:String,
selectionArgs:Array<String>) : Int
这些参数及其含义如下:
-
uri:指定数据在数据空间中的类型坐标。通过适当地指定这个参数,内容消费者将会知道他们的目标是哪种类型的数据。注意,对于删除或更新单个数据集,通常假设 URI 包含 URI 路径末端的数据的(技术)ID,例如content://com.android.contacts/contact/42。 -
values:这些是要插入或更新的值。您可以使用这个类的各种get*()方法来访问这些值。 -
selection:指定选择要更新或删除的数据。内容提供者框架没有假设这个选择参数应该是什么样子。这完全取决于实现,内容请求者必须服从实现的定义。然而,在许多情况下,您会有类似于 SQL 选择字符串的东西,比如这里的name = Jean AND age < 45。如果选择null,数据集的所有项目都将被处理。 -
selectionArgs:选择参数可能包含类似?的占位符。如果是这样,要为占位符插入的值在该数组中指定。同样,框架在这里没有做严格的假设,但是在大多数情况下,?充当占位符,就像在name = ? AND age < ?中一样,就像 SQL 一样。如果没有选择占位符,可能是null。
insert()方法应该返回指定插入数据的 URI。这个返回值可能是null,所以这里没有严格要求返回什么东西。如果它返回一些东西,这应该包含技术 ID。所有的Int-返回方法都应该返回受影响数据集的数量。
如果您不希望内容提供者能够修改任何数据,那么您可以为所有的插入、更新和删除方法提供空的实现,并让它们返回0或null。
完成 ContentProvider 类
为了完成您的ContentProvider类的实现,除了查询、插入、更新和删除方法之外,您必须再实现一个方法。
abstract getType(uri:Uri) : String
这将任何可用的 URI 映射到适当的 MIME 类型。例如,对于一个可能的实现,您可以按如下方式使用 URIs:
ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.<name>.<type>"
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.<name>.< type>"
URIs 分别指可能多项,或至多一项。对于<name>,使用一个全球唯一的名称,可以是反向公司域名或包名,也可以是其重要部分。对于<type>,使用定义表名或数据域的标识符。
注册内容供应器
一旦您完成了ContentProvider实现,您必须将它注册到AndroidManifest.xml文件中,如下所示:
<provider android:authorities="list"
android:directBootAware=["true" | "false"]
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:grantUriPermissions=["true" | "false"]
android:icon="drawable resource"
android:initOrder="integer"
android:label="string resource"
android:multiprocess=["true" | "false"]
android:name="string"
android:permission="string"
android:process="string"
android:readPermission="string"
android:syncable=["true" | "false"]
android:writePermission="string" >
...
</provider>
表 6-1 描述了这些属性。
表 6-1
元素
|属性(在每个属性前添加android:)
|
描述
|
| --- | --- |
| authorities | 这是以分号分隔的权威列表。在许多情况下,这将只是一个,并且您通常使用 app(包)名称或ContentProvider实现的完整类名。没有默认值,您必须至少有一个。 |
| directBootAware | 这指定了内容供应器是否可以在用户解锁设备之前运行。默认为false。 |
| enabled | 这指定是否启用内容提供者。默认为true。 |
| exported | 这指定了其他应用是否可以访问此处的内容。根据您的应用的架构,访问可能被限制为来自同一应用的组件,但通常您希望其他应用访问内容,因此将其设置为true。从 API 级开始,默认为false。在此之前,该标志并不存在,应用的行为是这样设置为true。 |
| grantUriPermissions | 这指定是否可以临时授予其他应用通过 URI 访问此供应器的内容的权限。临时授予是指由<permission>、<readPermission>或writePermission定义的许可拒绝,如果内容访问客户端被带有intent.addFlags(Intent.FLAG_GRANT_*_URI_PERMISSION)的意图调用,则该许可被临时覆盖。如果将该属性设置为false,仍然可以通过设置一个或多个<grant-uri-permission>子元素来授予更细粒度的临时权限。默认为false。 |
| icon | 这指定了用于提供程序的图标的资源 ID。默认情况下使用父组件的图标。 |
| initOrder | 在这里,您可以为同一个应用的内容提供者实例化强加一些顺序。较大的数字首先被实例化。请小心使用,因为启动顺序依赖关系可能表明应用设计不好。 |
| label | 这指定了标签要使用的字符串资源 ID。默认是 app 的label。 |
| multiprocess | 如果设置为true,在多个进程中运行的应用可能会运行多个内容提供者实例。否则,至多存在一个内容提供者的实例。默认为false。 |
| name | 这指定了ContentProvider实现的完全限定类名。 |
| permission | 这是设置readPermission和writePermission的便捷方式。指定后者之一具有优先权。 |
| process | 如果希望内容提供者在应用本身之外的另一个进程中运行,请指定一个进程名称。如果以冒号(:)开头,则该进程将是应用的私有进程;如果以小写字母开头,将使用全局流程(需要许可)。默认是在应用的进程中运行。 |
| readPermission | 这是客户端必须拥有的读取内容供应器的内容的许可。借助于grantUriPermissions属性,您可以让没有该权限客户机仍然访问内容。 |
| syncable | 这指定了内容供应器的数据是否应该与服务器同步。 |
| readPermission | 这是客户端必须拥有的对内容供应器的内容进行写入的许可。借助于grantUriPermissions属性,您可以让没有该权限客户机仍然访问内容。 |
如果您使用grantUriPermissions将 URI 权限临时授予由隐式意图调用的其他应用的组件,您必须小心地定制这样的意图。首先添加标志Intent.FLAG_GRANT_READ_URI_PERMISSION,然后在 intent 的data字段中添加您希望允许访问的 URI。这里有一个例子:
intent.action =
"com.example.app.VIEW" // SET INTENT ACTION
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// GRANT TEMPORARY READ PERMISSION
intent.data = Uri.parse("content://<AUTHORITY>/<PATH>")
// USE YOUR OWN!
startActivity(intent)
在被调用组件的意图过滤器中,您必须指定一个<data>元素,并且它必须包含一个适当的 URI 和一个 MIME 类型。尽管我们没有明确说明,但是必须指定 MIME 类型的原因是,Android 操作系统使用内容供应器的getType(Uri)方法来自动添加 MIME 类型,同时解析意图。这里有一个例子:
<intent-filter>
<action android:name=
"de.pspaeth.crcons.VIEW"/>
<category android:name=
"android.intent.category.DEFAULT"/>
<data android:mimeType="*/*"
android:scheme="content"
android:host="*"
android:pathPattern=".*"/>
</intent-filter>
然后被调用的组件被授权以指定的方式访问这个 URI。在它完成工作之后,它应该调用revokeUriPermission(String, Uri, Int)来撤销它已经被给予的临时许可。
revokePermission(getPackageName(), uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
and Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
在<provider>元素中,您可以添加几个子元素,如下所示:
-
meta-data:<meta-data android:name="string" android:resource="resource specification" android:value="string" />
这是必须指定resource或value的地方。如果使用resource,一个资源 ID 比如@string/someString会将资源 ID 本身分配给元条目,而使用value和@string/someString会将资源的内容分配给元条目。
-
grant-uri-permission:<grant-uri-permission android:path="string" android:pathPattern="string" android:pathPrefix="string" />
这将授予特定的 URI 权限(使用该子项的零到多个实例)。只有当父节点的属性grantUriPermissions被设置为false时,这个子节点才允许访问特定的 URIs。使用这些属性中的一个:path表示完整的 URI,pathPrefix表示以该值开始的 URIs,pathPattern允许通配符(X*表示任意字符 X 的零到多次重复,.*表示任意字符的零到多次重复)。
-
path-permission:<path-permission android:path="string" android:pathPrefix="string" android:pathPattern="string" android:permission="string" android:readPermission="string" android:writePermission="string" />
要定义内容提供者可以提供的数据子集,可以使用该元素指定路径和所需的权限。path属性指定一个完整的路径,pathPrefix属性允许匹配路径的初始部分,pathPattern是一个完整的路径,但是带有通配符(*匹配前面字符的零到多次出现,.*匹配任何字符的零到多次出现)。属性permission指定了读和写权限,属性readPermission和writePermission区分了读和写权限。如果指定了后两者之一,它优先于permission属性。
设计内容 URIs
URIs 描述了内容请求者感兴趣的数据领域。考虑到 SQL,这应该是表名。然而,URIs 可以做得更多。URI 的官方语法如下:
scheme:[//[user[:password]@]host[:port]]
[/path][?query][#fragment]
你可以看到user、password和port部分是可选的,事实上你通常不会在 Android 环境中指定它们。然而,它们并没有被禁止,而且在某些情况下是有意义的。然而,host部分在最普遍的意义上被解释为提供某种东西,这也正是它在这里的解释方式,“某种东西”就是数据。为了让这个概念更加清晰,Android 的host部分通常被称为权威。例如,在联系人系统应用中,权限将是com.android.contacts。(不要用字符串;请改用类常量字段。有关更多信息,请参见“合同”部分。)按照惯例scheme通常是content。所以,URI 的一般交往是从下面开始的:
content://com.android.contacts
URI 的path部分指定了数据域,即 SQL 中的表。例如,联系人内的用户简档数据通过以下方式来处理:
content://com.android.contacts/profile
在这个例子中,path 只有一个元素,但是 In 可以更复杂,比如pathpart1/pathpart2/pathpart3。
URI 也可以有一个指定选择的查询部分。查看来自类android.content.ContentProvider的查询方法,我们已经有能力在 API 的基础上指定一个选择,但是它是完全可以接受的,尽管不是强制性的,也允许在 URI 中使用查询参数。如果您需要将几个元素放入查询参数,您可以遵循通常的惯例使用&作为分隔符,就像在name=John&age=37中一样。
片段指定了次要资源,并且不经常被内容提供者使用。但是你可以用它,如果它对你有帮助的话。
由于 URI 是如此通用的构造,并且猜测正确的 URIs 来访问某些应用提供的内容几乎是不可能的,所以内容供应器应用通常会提供一个契约类来帮助为手头的任务构建正确的 URIs。
构建内容接口契约
客户端用来访问数据的 URIs 代表了访问内容的接口。因此,有一个中心位置是个好主意,客户可以在这里找到要使用的 URIs。Android 文档建议为此使用内容契约类。这类课程的大纲如下所示:
class MyContentContract {
companion object {
// The authority and the base URI
@JvmField
val AUTHORITY = "com.xyz.whatitis"
@JvmField
val CONTENT_URI = Uri.parse("content://" +
AUTHORITY)
// Selection for ID bases query
@JvmField
val SELECTION_ID_BASED = BaseColumns._ID +
" = ? "
}
// For various tables (or item types) it is
// recommended to use inner classes to specify
// them. This is just an example
class Items {
companion object {
// The name of the item.
@JvmField
val NAME = "item_name"
// The content URI for items
@JvmField
val CONTENT_URI = Uri.withAppendedPath(
MyContentContract.CONTENT_URI, "items")
// The MIME type of a directory of items
@JvmField
val CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE +
"/vnd." + MyContentContract.AUTHORITY +
".items"
// The mime type of a single item.
@JvmField
val CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE +
"/vnd." + MyContentContract.AUTHORITY +
".items"
// You could add database column names or
// projection specifications here, or sort
// order specifications, and more
// ...
}
}
}
当然,接口设计的一部分必须是为类、字段名称和字段值使用有意义的名称。
注意
契约类中描述的接口而不是必须对应于实际的数据库表。从概念上将接口从实际实现中分离出来是完全可行和有益的,并且还提供了表连接或其他项目类型,这些类型是以您可能想到的任何方式派生的。
这里有一些关于这个结构的注释:
-
If you can make sure clients will be using only Kotlin as a platform, this can be written in a much shorter way without any boilerplate code.
object MyContentContract2 { val AUTHORITY = "com.xyz.whatitis" val CONTENT_URI = Uri.parse("content://" + AUTHORITY) val SELECTION_ID_BASED = BaseColumns._ID + " = ? " object Items { val NAME = "item_name" val CONTENT_URI = Uri.withAppendedPath( MyContentContract.CONTENT_URI, "items") val CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd." + MyContentContract.AUTHORITY + ".items" val CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd." + MyContentContract.AUTHORITY + ".items" } }然而,如果我们想让 Java 客户机也使用这个接口,我们必须使用所有那些伴随对象和
@JvmObject声明和修饰符。 -
使用伴随对象和
JvmObject注释允许编写TheClass.THE_FIELD,就像 Java 中的静态字段一样。 -
您可以考虑向您的客户提供一个等价的 Java 构造,这样如果他们只使用 Java,就不必学习 Kotlin 语法。
-
Uri.parse()和Uri.withAppendedPath()方法调用只是使用Uri类的两个例子。这个类包含了更多的方法来帮助管理构造正确的 URIs。 -
您还可以在契约类中提供帮助器方法。如果这样做,请确保接口类不依赖于其他类,并在
fun函数声明中添加一个修饰符JvmStatic,使其可以从 Java 中调用。
然后,您可以向任何可能使用您的内容供应器应用的客户端公开提供这个契约类(或者多个类,如果您希望使用 Kotlin 和 Java 来记录接口的话)。
基于 AbstractCursor 和相关类的游标类
来自ContentProvider的所有query*()方法返回一个android.database.Cursor对象。从包中你可以看到这是一个以数据库为中心的类,这实际上是 Android 的一个小设计缺陷,因为内容接口应该是访问方法不可知的。
此外,Cursor接口是一个随机访问接口,供想要浏览结果集的客户机使用。您可以使用游标类的基本实现android.database.AbstractCursor;它已经实现了几个接口方法。为此,编写class MyCursor : AbstractCursor { ... }或val myCursor = object : AbstractCursor { ... }并实现所有抽象方法,并覆盖该类的一些其他方法来做有意义的事情。
-
override fun getCount(): Int这指定了可用数据集的数量。
-
override fun getColumnNames(): Array<String>这指定了列名的有序数组。
-
override fun getInt(column: Int): Int这会得到一个长值(列索引从零开始)。
-
override fun getLong(column: Int): Long这会得到一个长值(列索引从零开始)。
-
override fun getShort(column: Int): Short这将获得一个短值(列索引从零开始)。
-
override fun getFloat(column: Int): Float这将获得一个浮点值(列索引从零开始)。
-
override fun getDouble(column: Int): Double这会得到一个双精度值(列索引从零开始)。
-
override fun getString(column: Int): String这将获得一个字符串值(列索引从零开始)。
-
override fun isNull(column: Int): Boolean这表明该值是否为
null(列索引从零开始)。 -
override fun getType(column: Int): Int你可以不覆盖这个,但是如果不覆盖,它总会返回
Cursor.FIELD_TYPE_STRING,假设getString()总会返回有意义的东西。对于更细粒度的控制,让它返回FIELD_TYPE_NULL、FIELD_TYPE_INTEGER、FIELD_TYPE_FLOAT、FIELD_TYPE_STRING或FIELD_TYPE_BLOB中的一个。列索引是从零开始的。 -
override fun getBlob(column: Int): ByteArray如果您想要支持 blobs,请覆盖它。否则,将抛出一个
UnsupportedOperationException。 -
override fun onMove(oldPosition: Int, newPosition: Int): Boolean虽然没有标为
abstract,但是你必须覆盖这个。您的实现必须将光标移动到结果集中的相应位置。可能的值范围从-1(第一个位置之前;不是有效位置)到count(在最后一个位置之后;不是有效的位置)。如果移动成功,让它返回true。如果你不覆盖它,什么都不会发生,函数总是返回true。
AbstractCursor还提供了一个名为fillWindow(position: Int, window: CursorWindow?): Unit的方法,可以用来根据查询结果集填充一个android.database.CursorWindow对象。参见CursorWindow的在线 API 文档,继续该方法。
除了AbstractCursor,Cursor接口还有几个(抽象的)实现可以使用,如表 6-2 中所总结的。
表 6-2
更多游标实现
|里面的名字android.database
|
描述
|
| --- | --- |
| AbstractWindowedCursor | 它继承自AbstractCursor并拥有一个保存数据的CursorWindow对象。子类负责在他们的onMove(Int, Int)操作中用数据填充光标窗口,必要时分配一个新的光标窗口。与AbstractCursor相比,它更容易实现,但是你必须给onMove()增加很多功能。 |
| CrossProcessCursor | 这是一个游标实现,允许从远程进程使用它。它只是对android.database.Cursor接口的扩展,包含另外三个方法:fillWindow(Int, CursorWindow)、getWindow(): CursorWindow和onMove(Int, Int): Boolean。它不提供任何自己的实现;你必须覆盖所有在Cursor中定义的方法。 |
| CrossProcessCursorWrapper | 这是一个游标实现,允许从远程进程使用它。它实现了CrossProcessCursor并持有一个Cursor委托,这个委托也可以是一个CrossProcessCursor。 |
| CursorWrapper | 它保存了一个Cursor委托,所有的方法调用都被转发到这个委托。 |
| MatrixCursor | 这是一个完整的Cursor实现,数据在内存中存储为一个Object数组。你必须使用addRow(...)来添加数据。内部类MatrixCursor.RowBuilder可以用来构建供MatrixCursor.addRow(Array<Object>)使用的行。 |
| MergeCursor | 使用它透明地合并或连接光标对象。 |
| sqlite.SQLiteCursor | 这是一个由 SQLite 数据库支持的数据的Cursor实现。使用其中一个构造函数将游标与 SQLite 数据库对象连接起来。 |
基于光标接口的光标类
一种更低级的实现游标的方法不是依赖于AbstractCursor,而是自己实现所有的接口方法。
然后,您可以像在class MyCursor : Cursor { ... }中一样使用子类化,或者像在val myCursor = object : Cursor { ... }中一样使用匿名对象。在线文本指南中的“光标界面”一节描述了所有界面方法。
在提供者代码中调度 URIs
为了简化传入 URIs 的调度,类android.content.UriMatcher派上了用场。如果您有与查询相关的 URIs,例如:
people #list all people from a directory
people/37 #inquire a person with ID 37
people/37/phone #get phone info of person with ID 37
并且想要使用一个简单的switch语句,你可以在你的类或对象中编写如下代码:
val PEOPLE_DIR_AUTHORITY = "directory"
val PEOPLE = 1
val PEOPLE_ID = 2
val PEOPLE_PHONES = 3
val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI(PEOPLE_DIR_AUTHORITY,
"people", PEOPLE)
uriMatcher.addURI(PEOPLE_DIR_AUTHORITY,
"people/#", PEOPLE_ID)
uriMatcher.addURI(PEOPLE_DIR_AUTHORITY,
"people/#/phone", PEOPLE_PHONES)
}
这里,#代表任意数字,*匹配任意字符串。
在您的ContentProvider实现中,您可以使用下面的构造来分派传入的字符串 URL:
when(uriMatcher.match(url)) {
PEOPLE ->
// incoming path = people, do s.th. with that...
PEOPLE_ID ->
// incoming path = people/#, do s.th. with that...
PEOPLE_PHONES ->
// incoming path = people/#/phone, ...
else ->
// do something else
}
提供内容文件
内容提供者不仅可以访问类似数据库的内容,还可以公开检索类似文件的数据的方法,比如图像或声音文件。为此,提供了以下方法:
-
override fun getStreamTypes(uri:Uri, mimeTypeFilter:String) : Array<String>如果您的内容供应器提供文件,请覆盖此方法以允许客户端在给定 URI 的情况下确定支持的 MIME 类型。
mimeTypeFilter不应该是null,可以用它来过滤输出。它支持通配符,所以如果客户端想要检索所有值,它会在这里写*/*,您的提供者代码需要正确处理这一点。输出还必须包含所有那些可能是由提供程序执行的适当类型转换的结果的类型。这可能会返回null来指示空的结果集。例如image/png或audio/mpeg。 -
override fun openFile(uri:Uri, mode:String): ParcelFileDescriptorOverride this to handle requests to open a file blob. The parameter
modemust be one of the following (there is no default):-
r用于只读访问 -
w用于只写访问(如果数据已经存在,则首先擦除) -
wa,类似于w,但可能附加数据 -
rw用于阅读和添加文字 -
rwt,与rw类似,但会截断现有数据
要了解如何处理返回的
ParcelFileDescriptor,请参阅列表后的文本。 -
-
override fun openFile(uri:Uri, mode:String, signal:CancellationSignal): ParcelFileDescriptor这与
openFile(Uri, String)相同,但是客户端可能会在读取文件的过程中发出取消信号。提供者可以保存signal对象,并通过定期调用signal对象上的throwIfCancelled()来捕捉客户端的取消请求。 -
override fun openAssetFile(uri:Uri, mode:String): AssetFileDescriptor这类似于
openFile(Uri, String),但是它可以由需要能够返回文件的子部分的提供者实现,这些子部分通常是他们的 APK 中的素材。为了实现这一点,您可能想要使用android.content.res.AssetManager类。您可以在上下文的asset字段中找到它,所以例如在一个活动中,您可以直接使用asset来寻址AssetManager。 -
override fun openAssetFile(uri:Uri, mode:String, signal:CancellationSignal): AssetFileDescriptor这与
openAssetFile(Uri, String)相同,但允许从客户端取消。提供者可以保存signal对象,并通过定期调用signal对象上的throwIfCancelled()来捕捉客户端的取消请求。 -
override fun : openTypedAssetFile(uri:Uri, mimeTypeFilter:String, opts:Bundle): AssetFileDescriptor如果您想让客户端能够读(而不是写!)按 MIME 类型的素材数据。默认实现将
mimeTypeFilter与它从getType(Uri)获得的任何东西进行比较,如果它们匹配,它就简单地转发给openAssetFile(...)。 -
override fun : openTypedAssetFile(uri:Uri, mimeTypeFilter:String, opts:Bundle, signal:CancellationSignal): AssetFileDescriptor这与
openTypedAssetFile(Uri, String, Bundle)相同,但允许从客户端取消。提供者可以保存signal对象,并通过定期调用signal对象上的throwIfCancelled()来捕捉客户端的取消请求。 -
override fun <T : Any?> openPipeHelper(uri: Uri?, mimeType: String?, opts: Bundle?, args: T, func: PipeDataWriter<T>?): ParcelFileDescriptor这是一个实现
openTypedAssetFile(Uri, String, Bundle)的辅助函数。它创建了一个数据管道和一个后台线程,允许您将生成的数据流回客户端。这个函数返回一个新的ParcelFileDescriptor。工作完成后,调用者必须关闭它。 -
override fun openFileHelper(uri:Uri, mode:String): ParcelFileDescriptor对于子类来说,这是一个方便的方法。默认实现打开一个文件,其路径由使用提供的 URI 的
query()方法的结果给出。对于文件路径,_data成员从查询结果中提取,结果集计数必须是1。
那些返回ParcelFileDescriptor对象的方法可以调用如下适当的构造函数来为文件构建输入和输出流:
val fd = ... // get the ParcelFileDescriptor
val inpStream =
ParcelFileDescriptor.AutoCloseInputStream(fd)
val outpStream =
ParcelFileDescriptor.AutoCloseOutputStream(fd)
一旦工作完成,您必须在流上使用close()方法。Auto表示当您关闭流时,ParcelFileDescriptor会自动为您关闭。
类似地,那些返回AssetFileDescriptor对象的方法可以调用如下适当的构造函数来为文件构建输入和输出流:
val fd = ... // get the AssetFileDescriptor
val inpStream =
AssetFileDescriptor.AutoCloseInputStream(fd)
val outpStream =
AssetFileDescriptor.AutoCloseOutputStream(fd)
同样,一旦工作完成,您必须在流上使用close()方法;当您关闭流时,只有AssetFileDescriptor会自动为您关闭。
通知监听器数据更改
通过其ContentResolver字段(例如Activity.contentResolver)寻址内容提供者的客户端可以通过调用以下内容来注册以获得内容变化的通知:
val uri = ... // a content uri
contentResolver.registerContentObserver(uri, true,
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
// do s.th.
}
override fun onChange(selfChange: Boolean,
uri: Uri?) {
// do s.th.
}
}
)
registerContentObserver()的第二个参数指定子 URIs(URI 加上任何其他路径元素)是否也会导致通知。ContentObserver的构造函数参数也可以是一个Handler对象,用于在不同的线程中接收onChange消息。
要做到这一点,在内容提供者端,您可能需要注意事件是否被正确发出。例如,在任何数据修改方法中,都应该添加以下内容:
context.contentResolver.notifyChange(uri, null)
此外,为了使更改监听防弹,您可能想要通知由query()方法返回的任何Cursor对象。为此,cursor有一个registerContentObserver()方法,可以用来收集基于光标的内容观察者。内容供应器然后也可以向那些内容观察者发送消息。
扩展内容提供者
我们已经看到内容供应器允许访问类似数据库的内容和文件。如果您不太喜欢这种方式,或者对内容供应器应该能够做什么有自己的想法,您可以如下实现call()方法:
override call(method:String, arg:String, extras:Bundle):
Bundle {
super.call(method, arg, extras)
// do your own stuff...
}
这样,您可以设计自己的内容访问框架。当然,您应该告知可能的客户如何使用该接口,例如,在 contract 类中。
警告
没有安全检查适用于调用此方法。您必须自己实现适当的安全检查,例如通过在上下文中使用checkSelfPermission()。
通过 URI 规范化实现客户端访问一致性
查询结果通常包含 id、列表索引号或其他依赖于一些短期数据库上下文的信息。例如,一个查询可能返回 23、67 或 56 这样的商品 ID,如果您需要获得某个商品的详细信息,您可以使用包含该 ID 的另一个 URI 再次查询,例如content://com.xyz/people/23。这种 URIs 的问题是,客户通常不会保存它们供以后检索。同时,ID 可能已经改变,因此 URI 不太可靠。
为了克服这个问题,内容供应器可以实现 URI 规范化。为此,您的内容提供者类必须实现这两个方法:
-
canonicalize(url:Uri): Uri:Let this method return a canonicalized URI, for example, by adding some domain-specific query parameters as follows:
content://com.xyz/people/23 -> content://com.xyz/people? firstName=John& lastName=Bird& Birthday=20010534& SSN=123-99-1624 -
uncanonicalize(url:Uri): Uri:这与
canonicalize()正好相反。如果项目丢失并且无法执行取消定位,则让它返回null。
消费内容
为了消费内容,内容供应器客户端使用一个android.content.ContentResolver对象。任何包含活动、服务等的Context对象都提供了一个名为getContentResolver()的对象,或者用 Kotlin 更简洁地表示,只需编写contentResolver。
使用内容解析器
要访问类似数据库的内容,您可以使用以下ContentProvider方法之一:
-
insert(url: Uri, values: ContentValues): Int这将插入一条记录。
-
delete(url: Uri, where: String, selectionArgs: Array<String>): Int这将删除记录。
-
update(uri: Uri, values: ContentValues, where: String, selectionArgs: Array<String>): Int这将更新记录。
-
query(uri: Uri, projection: Array<String>, queryArgs: Bundle, cancellationSignal: CancellationSignal): Cursor这将根据给定的参数查询内容。
-
query(uri: Uri, projection: Array<String>, selection: String, selectionArgs: Array<String>, sortOrder: String, cancellationSignal: CancellationSignal): Cursor这将根据给定的参数查询内容。
-
query(uri: Uri, projection: Array<String>, selection: String, selectionArgs: Array<String>, sortOrder: String): Cursor这将根据给定的参数查询内容。
如前所述,它们的签名和含义与相应的ContentProvider方法密切相关。另外,看看在线 API 参考。
要改为访问文件内容,您可以使用以下方法之一:
-
openAssetFileDescriptor(uri: Uri, mode: String, cancellationSignal: CancellationSignal): AssetFileDescriptor这将打开内部(素材)文件。
-
openAssetFileDescriptor(uri: Uri, mode: String): AssetFileDescriptor这将打开内部(素材)文件,没有取消信号。
-
openTypedAssetFileDescriptor(uri: Uri, mimeType: String, opts: Bundle, cancellationSignal: CancellationSignal): AssetFileDescriptor这将打开类型化的内部(素材)文件。
-
openTypedAssetFileDescriptor(uri: Uri, mimeType: String, opts: Bundle): AssetFileDescriptor这将打开类型化的内部(素材)文件,没有取消信号。
-
openFileDescriptor(uri: Uri, mode: String, cancellationSignal: CancellationSignal): ParcelFileDescriptor这将打开文件。
-
openFileDescriptor(uri: Uri, mode: String): ParcelFileDescriptor这将打开文件,没有取消信号。
-
openInputStream(uri: Uri): InputStream这将打开一个输入流。
-
openOutputStream(uri: Uri, mode: String): OutputStream这会打开一个输出流。
-
openOutputStream(uri: Uri): OutputStream这将在
w模式下打开一个输出流。
open*Descriptor()方法同样与“提供内容”部分中相应的ContentProvider方法密切相关。另外两个,openInputStream()和openOutputStream(),是更容易访问文件(流)数据的便利方法。
如前所述,要注册内容观察者,以便在内容更改时获得异步信号,请使用以下方法之一:
-
registerContentObserver(uri: Uri, notifyForDescendants: Boolean, observer: ContentObserver) -
unregisterContentObserver(observer: ContentObserver)
要使用通过实现其call()方法来展示扩展的内容提供者,可以使用内容解析器的相应的call()方法
call(uri: Uri, method: String, arg: String, extras: Bundle)
访问系统内容供应器
Android 操作系统及其预装的应用提供了几个内容供应器组件。在在线 API 文档中,您可以在“android.provider/Classes”部分找到内容提供者契约类。以下部分总结了它们是什么以及如何访问它们。
BlockedNumberContract
这将显示一个包含被阻止号码的表。只有系统、默认电话应用、默认短信应用和运营商应用可以访问此表,但canCurrentUserBlockNumbers()除外,它可以由任何应用调用。例如,要使用它,您可以这样写:
val values = ContentValues()
values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
"1234567890")
Uri uri = contentResolver.insert(
BlockedNumbers.CONTENT_URI, values)
日历合同
这是一个相当复杂的内容提供者,有许多表。例如,我们正在访问日历列表并在此添加一个事件:
val havePermissions =
ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_CALENDAR)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CALENDAR)
== PackageManager.PERMISSION_GRANTED
if(!havePermissions) {
// Acquire permissions...
}else{
data class CalEntry(val name: String, val id: String)
val calendars = HashMap<String, CalEntry>()
val uri = CalendarContract.Calendars.CONTENT_URI
val cursor = contentResolver.query(
uri, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val calName = cursor.getString(
cursor.getColumnIndex(
CalendarContract.Calendars.NAME))
val calId = cursor.getString(
cursor.getColumnIndex(
CalendarContract.Calendars._ID))
calendars[calName] = CalEntry(calName, calId)
cursor.moveToNext()
}
Log.e("LOG", calendars.toString())
val calId = "4" // You should instead fetch an
// appropriate entry from the map!
val year = 2018
val month = Calendar.AUGUST
val dayInt = 27
val hour = 8
val minute = 30
val beginTime = Calendar.getInstance()
beginTime.set(year, month, dayInt, hour, minute)
val event = ContentValues()
event.put(CalendarContract.Events.CALENDAR_ID,
calId)
event.put(CalendarContract.Events.TITLE,
"MyEvent")
event.put(CalendarContract.Events.DESCRIPTION,
"This is test event")
event.put(CalendarContract.Events.EVENT_LOCATION,
"School")
event.put(CalendarContract.Events.DTSTART,
beginTime.getTimeInMillis())
event.put(CalendarContract.Events.DTEND,
beginTime.getTimeInMillis())
event.put(CalendarContract.Events.ALL_DAY,0)
event.put(CalendarContract.Events.RRULE,
"FREQ=YEARLY")
event.put(CalendarContract.Events.EVENT_TIMEZONE,
"Germany")
val retUri = contentResolver.insert(
CalendarContract.Events.CONTENT_URI, event)
Log.e("LOG", retUri.toString())
}
我们没有实现权限查询;权限在第七章中有详细描述。
呼叫日志
这是一个列出已拨和已接呼叫的表格。下面是一个列表示例:
val havePermissions =
ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CALL_LOG)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_CALL_LOG)
== PackageManager.PERMISSION_GRANTED
if(!havePermissions) {
// Acquire permissions...
}else {
val uri = CallLog.Calls.CONTENT_URI
val cursor = contentResolver.query(
uri, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
Log.e("LOG", "New entry:")
for(name in cursor.columnNames) {
val v = cursor.getString(
cursor.getColumnIndex(name))
Log.e("LOG"," > " + name + " = " + v)
}
cursor.moveToNext()
}
}
我们没有实现权限查询;权限在第七章中有详细描述。表格 6-3 描述了表格的栏目。
表 6-3
调用日志表列
|名字
|
描述
|
| --- | --- |
| date | 自纪元以来的调用日期,以毫秒为单位。 |
| transcription | 通话或语音邮件条目的转录。 |
| photo_id | 关联照片的缓存照片 ID。 |
| subscription_component_ name | 用于发出或接收呼叫的帐户的组件名称。 |
| type | 呼叫的类型。(在CallLog.Calls中的常量名称)之一:INCOMING_TYPE``OUTGOING_TYPE``MISSED_TYPE``VOICEMAIL_TYPE``REJECTED_TYPE``BLOCKED_TYPE``ANSWERED_EXTERNALLY_TYPE |
| geocoded_location | 与此呼叫关联的号码的地理编码位置。 |
| presentation | 由网络设置的号码表示规则。CallLog.Calls中的常量名称之一:PRESENTATION_ALLOWED``PRESENTATION_RESTRICTED``PRESENTATION_UNKNOWN``PRESENTATION_PAYPHONE |
| duration | 以秒为单位的呼叫持续时间。 |
| subscription_id | 用于拨打或接听电话的帐户的标识符。 |
| is_read | 该项目是否已被用户阅读或消费(0= false,1= true)。 |
| number | 用户输入的电话号码。 |
| features | 描述呼叫特征的位掩码,由(常量名称在CallLog.Calls ): FEATURES_HD_CALL组成:呼叫是 HD。FEATURES_PULLED_EXTERNALLY:呼叫被外部拔出。FEATURES_VIDEO:通话有视频。FEATURES_WIFI:通话是 WIFI 通话。 |
| voicemail_uri | 语音邮件条目的 URI(如果适用)。 |
| normalized_number | 电话号码的缓存规范化(E164)版本(如果存在)。 |
| via_number | 对于来话呼叫,是通过其接收呼叫的辅助线路号码。当 SIM 卡有多个相关的电话号码时,该值表示与 SIM 卡相关的号码被呼叫。 |
| matched_number | 与此项匹配的联系人的缓存电话号码(如果存在)。 |
| last_modified | 上次插入、更新行或将其标记为已删除的日期。以毫秒为单位。只读。 |
| new | 呼叫是否已被确认(0= false,1= true)。 |
| numberlabel | 与电话号码相关联的自定义号码类型的缓存号码标签(如果存在)。 |
| lookup_uri | 缓存的 URI,用于查找与电话号码关联的联系人(如果存在)。 |
| photo_uri | 与电话号码关联的图片的缓存照片 URI(如果存在)。 |
| data_usage | 呼叫的数据使用情况,以字节为单位。 |
| phone_account_address | 无证。 |
| formatted_number | 缓存的电话号码,使用基于用户拨打或接听电话时所在国家的规则进行格式化。 |
| add_for_all_users | 无证。 |
| numbertype | 与电话号码相关联的缓存号码类型(如果适用)。(在CallLog.Calls中的常量名称)之一:INCOMING_TYPE``OUTGOING_TYPE``MISSED_TYPE``VOICEMAIL_TYPE``REJECTED_TYPE``BLOCKED_TYPE ANSWERED_EXTERNALLY_TYPE |
| countryiso | 用户接听或拨打电话的国家/地区的 ISO 3166-1 双字母国家/地区代码。 |
| name | 与电话号码相关联的缓存名称(如果存在)。 |
| post_dial_digits | 已拨号码的后拨部分。 |
| transcription_state_id | 无证。 |
| | 表条目的(技术)ID。 |
联系人合同
这是一份描述电话联系的复杂合同。联系信息存储在三层数据模型中。
-
ContactsContract.Data:任何种类的个人数据。
-
ContactsContract.RawContacts:描述一个人的一组数据。
-
ContactsContract.Contacts:一个人的聚合视图,可能与
RawContacts表中的几行相关。由于它的聚合特性,它只能部分写入。
还有更多合同相关的表被描述为ContactsContract的内部类。为了让您入门,我们没有解释联系人内容提供者的所有可能的用例,而是给出了代码,列出了前面列出的三个主表的内容,显示了单个新联系人在那里写了什么,否则请参考ContactsContract类的在线文档。要列出三个表的内容,请使用以下内容:
fun showTable(tbl:Uri) {
Log.e("LOG", "##################################")
Log.e("LOG", tbl.toString())
val cursor = contentResolver.query(
tbl, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
Log.e("LOG", "New entry:")
for(name in cursor.columnNames) {
val v = cursor.getString(
cursor.getColumnIndex(name))
Log.e("LOG"," > " + name + " = " + v)
}
cursor.moveToNext()
}
}
...
showTable(ContactsContract.Contacts.CONTENT_URI)
showTable(ContactsContract.RawContacts.CONTENT_URI)
showTable(ContactsContract.Data.CONTENT_URI)
如果您使用 Android 预装的联系人应用创建了一个新联系人,在Contacts视图表中,您会发现以下新条目(此处仅显示重要的列):
_id = 1
display_name_alt = Mayer, Hugo
sort_key_alt = Mayer, Hugo
has_phone_number = 1
contact_last_updated_timestamp = 1518451432615
display_name = Hugo Mayer
sort_key = Hugo Mayer
times_contacted = 0
name_raw_contact_id = 1
作为表RawContacts中的一个关联条目,您会发现以下内容:
_id = 1
account_type = com.google
contact_id = 1
display_name_alt = Mayer, Hugo
sort_key_alt = Mayer, Hugo
account_name = pmspaeth1111@gmail.com
display_name = Hugo Mayer
sort_key = Hugo Mayer
times_contacted = 0
account_type_and_data_set = com.google
显然,您会在前面列出的Contacts视图中发现许多条目。相关联的是Data表中的零到多个条目(只显示了最重要的)。
Entry:
_id = 3
mimetype = vnd.android.cursor.item/phone_v2
raw_contact_id = 1
contact_id = 1
data1 = (012) 345-6789
Entry:
_id = 4
mimetype = vnd.android.cursor.item/phone_v2
raw_contact_id = 1
contact_id = 1
data1 = (098) 765-4321
Entry:
_id = 5
mimetype = vnd.android.cursor.item/email_v2
raw_contact_id = 1
contact_id = 1
data1 = null
Entry:
_id = 6
mimetype = vnd.android.cursor.item/name
raw_contact_id = 1
contact_id = 1
data3 = Mayer
data2 = Hugo
data1 = Hugo Mayer
Entry:
_id = 7
mimetype = vnd.android.cursor.item/nickname
raw_contact_id = 1
contact_id = 1
data1 = null
Entry:
_id = 8
mimetype = vnd.android.cursor.item/note
raw_contact_id = 1
contact_id = 1
data1 = null
您可以看到,Data表中的行对应于 GUI 中的编辑字段。您看到两个电话号码,一个名一个姓,没有昵称,也没有电子邮件地址。
文件合同
与我们在这里看到的其他契约不同,这不是一个内容契约。对应的是android.provider.DocumentsProvider,是android.content.ContentProvider的子类。我们将在本章的后面讨论文档提供者。
FontsContract
这是一份处理可下载字体的合同,不对应内容供应器。
媒体库
媒体存储处理内部和外部存储设备上所有媒体相关文件的元数据。这包括音频文件、图像和视频。此外,它以一种与用法无关的方式处理文件。这意味着媒体和非媒体文件与媒体文件相关。根类android.provider.MediaStore本身不包含特定于内容提供者的素材,但是下面的内部类包含:
-
MediaStore.Audio音频文件。包含更多的音乐专辑、艺术家、音频文件本身、流派和播放列表的内部类。
-
MediaStore.Images图像。
-
MediaStore.Videos视频。
-
MediaStore.Files一般文件。
您可以通过浏览在线 API 文档来研究任何媒体存储表。对于您自己的实验,您可以从整个表开始,注意常量EXTERNAL_CONTENT_URI和INTERNAL_CONTENT_URI,或者方法getContentUri(),然后通过我们之前已经使用过的相同代码发送它们。
showTable(MediaStore.Audio.Media.getContentUri(
"internal")) // <- other option: "external"
fun showTable(tbl:Uri) {
Log.e("LOG", "#######################################")
Log.e("LOG", tbl.toString())
val cursor = contentResolver.query(
tbl, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
Log.e("LOG", "New entry:")
for(name in cursor.columnNames) {
val v = cursor.getString(
cursor.getColumnIndex(name))
Log.e("LOG"," > " + name + " = " + v)
}
cursor.moveToNext()
}
}
设置
这是一个处理各种全局和系统级设置的内容提供者。以下是 contract 类中作为常量的主要 URIs:
-
Settings.Global.CONTENT_URI:Global settings. All entries are triples of the following:
-
_id -
android.provider.Settings.NameValueTable.NAME -
android.provider.Settings.NameValueTable.VALUE
-
-
Settings.System.CONTENT_URI:Global system-level settings. All entries are triples of the following:
-
_id -
android.provider.Settings.NameValueTable.NAME -
android.provider.Settings.NameValueTable.VALUE
-
-
Settings.Secure.CONTENT_URI:
这是一个安全的系统设置。应用不允许改变它。所有条目都是以下内容的三元组:
-
_id -
android.provider.Settings.NameValueTable.NAME -
android.provider.Settings.NameValueTable.VALUE
为了研究这些表,请看一下android.provider.Settings的在线 API 文档。它描述了所有可能的设置。要列出完整的设置,您可以使用与前面的ContactsContract契约类相同的函数。
showTable(Settings.Global.CONTENT_URI)
showTable(Settings.System.CONTENT_URI)
showTable(Settings.Secure.CONTENT_URI)
...
fun showTable(tbl:Uri) {
Log.e("LOG", "##################################")
Log.e("LOG", tbl.toString())
val cursor = contentResolver.query(
tbl, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
Log.e("LOG", "New entry:")
for(name in cursor.columnNames) {
val v = cursor.getString(
cursor.getColumnIndex(name))
Log.e("LOG"," > " + name + " = " + v)
}
cursor.moveToNext()
}
}
你的应用不需要特殊权限来读取设置。然而,只可能对Global和System表进行写操作,并且您还需要一个特殊的构造来获得权限。
if(!Settings.System.canWrite(this)) {
val intent = Intent(
Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.data = Uri.parse(
"package:" + getPackageName())
startActivity(intent)
}
通常,您通过调用以下命令来获取权限:
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE\_SETTINGS), 42)
然而,当设置权限时,这个请求会被当前的 Android 版本立即拒绝。因此,您不能使用它,而是需要调用前面所示的 intent。
要访问某个条目,可以再次使用 contract 类中的常量和方法。
val uri = Settings.System.getUriFor(
Settings.System.HAPTIC_FEEDBACK_ENABLED)
Log.e("LOG", uri.toString())
val feedbackEnabled = Settings.System.getInt(
contentResolver,
Settings.System.HAPTIC_FEEDBACK_ENABLED)
Log.e("LOG", Integer.toString(feedbackEnabled))
Settings.System.putInt(contentResolver,
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0)
警告
虽然可以为某个设置获取单独的 URI,但是您不应该使用ContentResolver.update()、ContentResolver.insert()和ContentResolver.delete()方法来改变值。相反,请使用 contract 类提供的方法。
SyncStateContract
浏览器应用、联系人应用和日历应用使用此合约来帮助将用户数据与外部服务器同步。
用户词典
这是指允许您管理和使用基于词典的预测输入的内容供应器。从 API level 23 开始,用户词典只能在输入法编辑器或拼写检查框架中使用。对于现代应用,你不应该试图从另一个地方使用它。因此,该合同仅起信息作用。
语音邮件合同
该合同允许访问涉及语音邮件供应器的信息。它主要由两个由内部类描述的表组成。
VoicemailContract.Status
一个语音邮件源应用使用这个契约来告诉系统它的状态。
-
VoicemailContract.Voicemails这包含实际的语音邮件。
您可以列出这些表格的内容。例如,对于Voicemails表,编写以下内容:
val uri = VoicemailContract.Voicemails.CONTENT_URI.
buildUpon().
appendQueryParameter(
VoicemailContract.PARAM_KEY_SOURCE_PACKAGE,
packageName)
.build()
showTable(uri)
fun showTable(tbl:Uri) {
Log.e("LOG", "####################################")
Log.e("LOG", tbl.toString())
val cursor = contentResolver.query(
tbl, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
Log.e("LOG", "New entry:")
for(name in cursor.columnNames) {
val v = cursor.getString(
cursor.getColumnIndex(name))
Log.e("LOG"," > " + name + " = " + v)
}
cursor.moveToNext()
}
}
添加VoicemailContract.PARAM_KEY_SOURCE_PACKAGE URI 参数很重要;否则,您会得到一个安全异常。
批量访问内容数据
android.content.ContentProvider类允许您的实现使用以下内容:
applyBatch(
operations: ArrayList<ContentProviderOperation>):
Array<ContentProviderResult>
默认实现遍历列表并依次执行每个操作,但是您也可以重写方法以使用自己的逻辑。参数中提供的ContentProviderOperation对象描述了要执行的操作。它可以是更新、删除和插入之一。
为了方便起见,该类提供了一个生成器,您可以按如下方式使用它:
val oper:ContentProviderOperation =
ContentProviderOperation.newInsert(uri)
.withValue("key1", "val1")
.withValue("key2", 42)
.build()
保护内容
从您在AndroidManifest.xml中声明一个内容提供者并通过将其exported属性设置为true来导出它的那一刻起,其他应用就被允许访问该提供者公开的完整内容。
这可能不是您想要的敏感信息。作为一种补救措施,要对内容或部分内容施加限制,可以向<provider>元素或其子元素添加与权限相关的属性。
您基本上有以下选择:
-
通过一个标准保护所有内容
To do so, use the
permissionattribute of<provider>as follows:<provider ... android:permission="PERMISSION-NAME" ... > ... </provider>Here,
PERMISSION-NAMEis a system permission or a permission you defined in the<permission>element of the app. If you do it that way, the complete content of the provider is accessible only to such clients that successfully acquired exactly this permission. More precisely, any read or write access requires clients to have this permission. If you need to distinguish between read permission and write permission, you can instead use thereadPermissionandwritePermissionattributes. If you use a mixture, the more specific attributes win.-
permission = A ® writePermission = A, readPermission = A -
permission = A, readPermission = B ® writePermission = A, readPermission = B -
permission = A, writePermission = B ® writePermission = B, readPermission = A -
permission = A, writePermission = B, readPermission = C ® writePermission = B, readPermission = C
-
-
保护特定的 URI 路径
By using the
<path-permission>subelement of<provider>, you can impose restrictions on specific URI paths.<path-permission android:path="string" android:pathPrefix="string" android:pathPattern="string" android:permission="string" android:readPermission="string" android:writePermission="string" />在
*permission属性中,您指定权限名称和权限范围,就像前面描述的通过一个标准保护所有内容一样。对于路径规范,您可以使用三个可能的属性中的一个:path用于精确的路径匹配,path- Prefix用于匹配路径的开头,pathPattern允许通配符(X*用于任意字符的零到多次出现,.*用于任意字符的零到多次出现)。因为可以使用几个<path-permission>元素,所以可以在内容提供者中构建细粒度的权限结构。 -
许可豁免
By using the
grantUriPermissionattribute of the<provider>element, you can temporarily grant permissions to components called by intent from the app that owns the content provider. If you setgrantUriPermissiontotrueand the intent for calling the other component gets constructed using the help of this:intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION) /*or*/ intent.addFlags( Intent.FLAG_GRANT_WRITE_URI_PERMISSION) /*or*/ intent.addFlags( Intent.FLAG_GRANT_WRITE_URI_PERMISSION and Intent.FLAG_GRANT_READ_URI_PERMISSION)then the called component will have full access to all content of the provider. You can instead set
grantUriPermissiontofalseand add subelements.<grant-uri-permission android:path="string" android:pathPattern="string" android:pathPrefix="string" />
然后,您以更细粒度的方式控制豁免。为了让这两者都有意义,显然必须让由*permission属性设置的限制生效;否则,你没有什么可以豁免的。元素属性的规则如前所述:path用于精确的路径匹配,pathPrefix用于匹配路径的起点,pathPattern允许通配符(X*用于任意字符 X 的零到多次出现,.*用于任意字符的零到多次出现)。
为搜索框架提供内容
Android 搜索框架为用户提供了一个功能,可以通过任何方式和使用任何数据源来搜索任何可用的数据。我们将在第八章讨论搜索框架;目前,重要的是要知道内容供应器在以下方面发挥的作用:
-
最近的查询建议
-
自定义建议
对于这两者,您提供特殊的内容提供者子类,并将它们作为任何其他内容提供者添加到AndroidManifest.xml。
文档供应器
文档提供程序是存储访问框架(SAF)的一部分。它允许以文档为中心的数据访问视图,还展示了文档目录的分层超结构。
注意
SAF 包含在 API 级中。截至 2018 年 2 月,超过 90%的活跃 Android 设备都使用该版本。在此之前,您不能将 SAF 用于设备,但是如果您真的需要涵盖剩余的 10%,您仍然可以将文档作为由内容提供者协调的普通内容来提供,并提取出 SAF 和遗留提供者都可以使用的代码。
文档提供者的主要思想是,你的应用提供对文档的访问,无论相应的数据存储在哪里,并且不关心文档和文档结构如何呈现给用户或其他应用。文档提供者数据模型由一个到多个从根节点开始的树组成,子节点或者是文档,或者是跨越子树的目录,还有其他目录和文档。因此,它类似于文件系统中的数据结构。
从文档提供者开始,创建一个实现android.provider.DocumentsProvider的类,它本身是android.content.ContentProvider的一个专门化子类。至少,您必须实现这些方法:
override fun onCreate(): Boolean:
使用它来初始化文档提供程序。因为这是在应用的主线程上运行的,所以你不能在这里执行冗长的操作。但是您可以准备对提供者的数据访问。如果提供者加载成功,则返回true,否则返回false。
-
override fun queryRoots(projection: Array<out String>?): Cursor:This is supposed to query the roots of the provider’s data structure. In many cases, the data will fit into one tree, and you thus need to provide just one root, but you can have as many roots as makes sense for your requirements. The
projectionargument may present a list of columns to be included in the result set. The names are the same as theCOLUMN_*constants insideDocumentsContract.Root. It may benull, which means return all columns. The method must return cursors with at a maximum the following fields (shown are the constant names fromDocumentsContract.Root):-
COLUMN_AVAILABLE_BYTES(long):根下可用字节。可选,可以是null表示unknown。 -
COLUMN_CAPACITY_BYTES(long):该根处的树的容量,以字节为单位。想想文件系统的容量。可选,可以是null表示unknown。 -
COLUMN_DOCUMENT_ID:该根对应的目录的 ID(字符串)。必需的。 -
COLUMN_FLAGS:应用于根的标志(int)。(在DocumentsContract.Root中的常数)的组合:-
FLAG_LOCAL_ONLY(设备本地,无网络接入), -
FLAG_SUPPORTS_CREATE(根下至少有一个文档支持创建内容) -
FLAG_SUPPORTS_RECENTS(可以查询 root 来显示最近更改的文档) -
FLAG_SUPPORTS_SEARCH(该树允许搜索文档)
-
-
COLUMN_ICON(int):根的图标资源 ID。必选。 -
COLUMN_MIME_TYPES(字符串):支持的 MIME 类型。如果不止一个,使用换行符\n作为分隔符。可选的,可能是null来表示支持所有的 MIME 类型。 -
COLUMN_ROOT_ID(字符串):根的唯一 ID。必选。 -
COLUMN_SUMMARY(字符串):该根的摘要;可能会显示给用户。可选,可以是null表示“未知” -
COLUMN_TITLE(字符串):根的标题,可能会显示给用户。必选。
-
如果这组根改变了,你必须用DocumentsContract.buildRootsUri调用ContentResolver.notifyChange来通知系统。
-
override fun queryChildDocuments(parentDocumentId: String?, projection: Array<out String>?, sortOrder: String?): Cursor:Return the immediate children documents and subdirectories contained in the requested directory. Apps targeting at API level 26 or higher should instead implement
fun queryChildDocuments(parentDocumentId: String?, projection: Array<out String>?, queryArgs: Bundle?): Cursorand in this method use the following:override fun queryChildDocuments( parentDocumentId: String?, projection: Array<out String>?, sortOrder: String?): Cursor { val bndl = Bundle() bndl.putString( ContentResolver.QUERY_ARG_SQL_SORT_ORDER, sortOrder) return queryChildDocuments( parentDocumentId, projection, bndl) } -
override fun queryChildDocuments(parentDocumentId: String?, projection: Array<out String>?, queryArgs: Bundle?): Cursor:Return the immediate children documents and subdirectories contained in the requested directory. The bundle argument contains query parameters as keys.
ContentResolver.QUERY_ARG_SQL_SELECTION ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS ContentResolver.QUERY_ARG_SQL_SORT_ORDER -or- ContentResolver.QUERY_ARG_SORT_COLUMNS (this being a String array)
parentDocumentId是我们想要列出的目录的 ID,在projection中,您可以指定应该返回的列。使用来自DocumentsContract.Document的常量COLUMN_*列表。或者写null返回所有栏目。结果Cursor最大返回以下字段(关键字是DocumentsContract.Document中的常量):
-
COLUMN_DISPLAY_NAME(字符串):文档的显示名称,用作向用户显示的主标题。必选。 -
COLUMN_DOCUMENT_ID(字符串):单据的唯一标识。必选。 -
COLUMN_FLAGS:文档的标志。(来自DocumentsContract.Document的常量名称)的组合:-
FLAG_SUPPORTS_WRITE(支持写作) -
FLAG_SUPPORTS_DELETE(支持删除) -
FLAG_SUPPORTS_THUMBNAIL(支持缩略图表示) -
FLAG_DIR_PREFERS_GRID(对于目录,如果它们应该显示为网格) -
FLAG_DIR_PREFERS_LAST_MODIFIED(对于目录,优先按“最后修改时间”排序) -
FLAG_VIRTUAL_DOCUMENT(没有 MIME 类型的虚拟文档) -
FLAG_SUPPORTS_COPY(支持复制) -
FLAG_SUPPORTS_MOVE(在树内移动,被支撑) -
FLAG_SUPPORTS_REMOVE(从层级结构中移除,不删除,支持)
-
-
COLUMN_ICON(int):文档的特定图标资源 ID。可能是null使用系统默认。 -
COLUMN_LAST_MODIFIED(long):上次修改文档的时间戳,从 UTC 1970 年 1 月 1 日 00:00:00.0 开始,以毫秒为单位。必需,但如果未定义,可能是null。 -
COLUMN_MIME_TYPE(字符串):文档的 MIME 类型。必选。 -
COLUMN_SIZE(long):文档的大小,以字节为单位,如果未知,则为null。必选。 -
COLUMN_SUMMARY(字符串):一个文档的摘要;可以显示给用户。可选,可能是null。
对于与网络相关的操作,您可以部分返回数据,并在Cursor上设置DocumentsContract.EXTRA_LOADING,以表明您仍在获取额外的数据。然后,当网络数据可用时,您可以发送更改通知来触发重新查询并返回完整的内容。为了支持变更通知,您必须用一个相关的 URI 来触发Cursor.setNotificationUri(),可能是从DocumentsContract.buildChildDocumentsUri()开始。然后你可以用那个 URI 打电话给ContentResolver.notifyChange()发送变更通知。
fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor:
打开并返回请求的文档。这应该会返回一个可靠的ParcelFileDescriptor来检测远程调用者何时读或写完了文档。如果您在下载内容时阻止,您应该定期检查CancellationSignal.isCanceled()以中止放弃的打开请求。对于要返回的文档,参数为documentId。mode指定“打开”模式,如r、w或rw。应始终支持模式r。如果不支持传递模式,提供者应该抛出UnsupportedOperationException。如果模式是排他的r或w,您可以返回一个管道或套接字对,但是像rw这样的复杂模式意味着磁盘上有一个支持查找的普通文件。如果请求被取消,调用方可以使用signal。可能是null。
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor:
返回单个请求文档的元数据。参数是用于返回文档 ID 的documentId和用于放入光标的列列表的projection。使用来自DocumentsContract.Document的常量。列表见queryChildDocuments()方法描述。如果在这里使用null,所有的列都将被返回。
在文件AndroidManifest.xml中,您可以像注册任何其他提供者一样注册文档提供者。
<provider
android:name="com.example.YourDocumentProvider"
android:authorities="com.example.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission=
"android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name=
"android.content.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
在前面的查询中,我们已经看到,Cursor对象返回标志来表示最近的文档和在树中的搜索应该得到支持。为此,您必须在您的DocumentsProvider实现中再实现一两个方法。
-
override fun queryRecentDocuments(rootId: String, projection: Array<String>): Cursor:这应该会返回请求的根目录下最近修改的文档。返回的文档要按
COLUMN_LAST_MODIFIED降序排列,最多显示 64 个条目。最近的文档不支持更改通知。 -
querySearchDocuments(rootId: String, query: String, projection: Array<String>): Cursor:这应该会返回与所请求的根下的给定查询相匹配的文档。返回的文档应该按照相关性降序排列。对于慢速查询,您可以返回部分数据,并在游标上设置
EXTRA_LOADING以指示您正在获取额外的数据。然后,当数据可用时,您可以发送更改通知来触发重新查询并返回完整的内容。为了支持变更通知,您必须将setNotificationUri(ContentResolver, Uri)与相关的Uri一起使用,可能来自buildSearchDocumentsUri(String, String, String)。然后您可以用那个Uri调用方法notifyChange(Uri, android.database.ContentObserver, boolean)来发送变更通知。
一旦您的文档提供者被配置并运行,客户端组件就可以使用ACTION_OPEN_DOCUMENT或ACTION_CREATE_DOCUMENT意图来打开或创建文档。Android 系统选择器将负责向用户呈现适当的文档;您不必为您的文档提供者提供自己的 GUI。
下面是这种客户端访问的一个示例:
// An integer you can use to identify that call when the
// called Intent returns
val READ_REQUEST_CODE = 42
// ACTION_OPEN_DOCUMENT used in this example is the
// intent to choose a document like for example a file
// file via the system's file browser.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
// Filter to only show results that can be "opened", such
// as a file (as opposed to a list of informational items
)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// You can use a filter to for example show only images.
// To search for all documents instead, you can use "*/*"
// here.
intent.type = "image/*"
// The actual Intent call - the system will provide the
// GUI
startActivityForResult(intent, READ_REQUEST_CODE)
一旦从系统选择器中选择了一个项目,为了捕捉意图返回,您应该编写如下内容:
override fun onActivityResult(requestCode:Int,
resultCode:Int,
resultData:Intent) {
// The ACTION_OPEN_DOCUMENT intent was sent with the
// request code READ_REQUEST_CODE. If the request
// code seen here doesn't match, it's the
// response to some other intent, and the code below
// shouldn't run at all.
if (requestCode == READ_REQUEST_CODE
&& resultCode == Activity.RESULT_OK) {
// The document selected shows up in
// intent.getData()
val uri = resultData.data
Log.i("LOG", "Uri: " + uri.toString())
showImage(uri) // Do s.th. with it
}
}
除了打开示例中所示的文件,您还可以对 intent 返回中收到的 URI 做其他事情。例如,您还可以发出一个查询来获取元数据,如前面的查询方法所示。由于DocumentsProvider继承自ContentProvider,您可以使用前面描述的方法为文档的字节打开一个流。