Intent
Intent 用于解决 Android 应用的各项组件之间的通讯,例如:启动组件,传递数据。分为显式 Intent 和隐式 Intent
显式Intent
Intent 有多个构造函数的重载,最常用的显式 Intent 是
//第一个参数Context要求提供一个启动Activity的上下文;
//第二个参数Class用于指定想要启动的目标Activity
public Intent(Context packageContext, Class> cls) {
mComponent = new ComponentName(packageContext, cls);
}
布局文件 activity_main.xml 如下,其中 android:textAllCaps="false" 是控制按钮中的因为字母不要自动转为大写字母
在点击按钮时启动自己应用中的另一个 Activity :
button.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
效果
隐式Intent
隐式 Intent 不会明确指出想要启动哪一个Activity,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动
通过在 AndroidManifest.xml 文件中的
与之匹配的 Intent :
val intent = Intent("com.example.androidstudy.ACTION_START_SECOND_ACTIVITY")
intent.setDataAndType(Uri.parse("http://www.baidu.com"), "image/*")
intent.addCategory("com.example.androidstudy.category.TEST_CATEGORY")
startActivity(intent)
注意
- 需要同时匹配过滤列表中的 action、category、data 信息,否则匹配失败
- 一个 Activity 中可以有多个 intent-filter ,一个 Intent 只要能匹配任何一组 intent-filter 即可
action的匹配规则
要求Intent中的action存在且必须和过滤规则中的其中一个action相同
category的匹配规则
要求Intent中如果含有category,那么所有的category都必须和过滤规则中的其中一个category相同
注意
系统在调用startActivity或者startActivityForResult的时候会默认为Intent加上“android.intent. category.DEFAULT”这个category,因此被调用的Activity必须有
data的匹配规则
如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data
data的语法
android:scheme=""
android:host=""
android:port=""
android:path=""
android:pathPattern=""
android:pathPrefix=""
android:mimeType="" />
data由两部分组成,mimeType和URI
mimeType
mimeType指媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*等
URI
://:/[||]
scheme:协议。例如:http、file、content等
host:主机名。例如:www.baidu.com
port:端口。例如:80
path、pathPrefix 和 pathPattern:这三个参数都表示路径信息,可以三者选其一
path表示完整的路径信息
可以包含通配符 * ,表示0个或多个任意字符
pathPrefix表示路径的前缀信息
URI 的例子:
content://com.example.androidstudy:200/floder/subfloder/etc
http://www.baidu.com:80/search/info
注意
如果要为Intent指定完整的data,必须要调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法彼此会清除对方的值
隐式Intent启动Activity前应判断
当通过隐式方式启动一个 Activity 之前,应该判断下是否有 Activity 能够匹配我们的隐式 Intent,有两种方法,找不到匹配的Activity就会返回null
- PackageManager 的 resolveActivity 方法
- Intent 的 resolveActivity 方法
PackageManager还提供了queryIntentActivities方法:
public abstract List queryIntentActivities(@NonNull Intent intent, @ResolveInfoFlags int flags);
它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息,第二个参数要使用MATCH_DEFAULT_ONLY这个标记位,含义是仅匹配那些在intent-filter中声明了
使用Android系统内置的动作
调用系统的浏览器
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("http://www.baidu.com")
startActivity(intent)
调拨打电话界面
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
地图
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("geo:38.899533,-77.036476")
startActivity(intent)
直接拨打电话
在 AndroidManifest.xml 中还需要声明权限
代码
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.CALL_PHONE
) == PackageManager.PERMISSION_DENIED
) {
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
卸载应用,会弹出系统卸载的确认框
在 AndroidManifest.xml 中还需要声明权限
但代码里不需要动态申请权限
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:com.example.kotlin")
startActivity(intent)
安装应用
Android 7.0 以前
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setDataAndType(
Uri.fromFile(File(cacheDir, "app-debug.apk")),
"application/vnd.android.package-archive"
)
startActivity(intent)
Android 7.0 开始
分享私有文件的推荐方法是使用 FileProvider
定义一个FileProvider
android:name 是FileProvider组件的完整类名
android:authorities 是域名,为了保证唯一性,通常是你的应用包名+fileprovider
android:exported 设置false,不需要暴露它
android:grantUriPermissions 设置true,表示允许你可以对文件授予临时权限
添加可用权限的文件目录
为了将实际的文件路径(file://)映射成content URI(content://),需要一个配置文件xml来提前定义文件存放的目录路径path与Content URI的对应关系。文件放置在res/xml/下
上面的
标签 | 对应方法 | 路径示例 |
---|---|---|
getFilesDir() | /data/data/com.example.androidstudy/files/ | |
getCacheDir() | /data/data/com.example.androidstudy/cache/ | |
Environment.getExternalStorageDirectory() | /storage/emulated/0/ | |
getExternalFilesDir() | /storage/emulated/0/Android/data/com.chen.gradle/files/ | |
getExternalCacheDir() | /storage/emulated/0/Android/data/com.chen.gradle/cache/ | |
表示根目录,“/” |
授予临时的读写权限
在 AndroidManifest.xml 配置 provider 标签的时候,属性 android:grantUriPermissions="true" 表示允许它授予 Uri 临时的权限,当我们生成出一个 content:// 的 Uri 对象之后,还需要对这个 Uri 接收的 App 赋予对应的权限才可以使用,权限是定义于 Intent 中的常量,如下:
public static final int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
有两种为其他应用授予 Uri 对象的访问权限的方式:
-
Context.grantUriPermission(String toPackage, Uri uri, int modeFlags)
此方法从授权开始,一直到设备重启或者手动调用 Context.revokeUriPermission() 方法,才会收回对此 Uri 的授权
toPackage:授予权限的 App 的包名
uri:授予权限的 content:// 的 Uri
modeFlags:定义在 Intent 中的读写权限
-
Intent.addFlags(@Flags int flags)
flags:定义于 Intent 中的常量,例如:FLAG_GRANT_READ_URI_PERMISSION
此方法从授权开始,一直到应用完全退出
Android 7 安装应用代码
/**
* Android 7 开始, apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApkN(context: Context, apkFilePath: String) {
val file = File(apkFilePath)
// 使用FileProvider.getUriForFile()方法将file:// 转为 content://
// authority 参数需要与前面在 AndroidManifest.xml 中 配置的 android:authorities保持一致
val apkUri = FileProvider.getUriForFile(context, "com.example.androidstudy.fileProvider", file)
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//赋予临时权限给Uri
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
context.startActivity(intent)
}
Android 8.0 开始
Android 8.0 开始,安装应用需要打开未知来源权限,需要去设置中为需要安装应用的APP开启权限
在 AndroidManifest.xml 中还需要声明权限
Android 8 安装应用代码
const val UNKNOWN_APP_SOURCES_REQUEST_CODE = 0X0008
/**
* Android 8 开始, apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApkO(context: Context, apkFilePath: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val isGranted = context.packageManager.canRequestPackageInstalls()
if (!isGranted) {
AlertDialog.Builder(context)
.setCancelable(false)
.setTitle("安装应用需要打开未知来源权限,请去设置中开启权限")
.setPositiveButton("确定") { dialog, witch ->
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
(context as Activity).startActivityForResult(
intent,
UNKNOWN_APP_SOURCES_REQUEST_CODE
)
}
.show()
} else {
installApkN(context, apkFilePath)
}
}
}
......
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& requestCode == UNKNOWN_APP_SOURCES_REQUEST_CODE
&& packageManager.canRequestPackageInstalls()
) {
val apkPath = cacheDir.canonicalPath + "/app-debug.apk"
installApk(this, apkPath)
}
}
问题一
先在/data/data/com.example.androidstudy/cache目录下放入app-debug.apk,卸载原来的应用,重新运行在模拟器上
There was a problem while parsing the package
截图:
这是因为卸载原来的应用会删除目录 /data/data/com.example.androidstudy/ 下的所有东西,apk文件自然也被删掉了,因此在点击安装前,需要再次确认apk是否在指定目录下
问题二
INSTSLL_PARSE_FAILED_NO_CERTIFICATES:测试的 apk 没有签名导致的,此错误在拖拽 apk 到低版本(我使用的是Nexus 4 API 22)模拟器上才会显示错误信息,如果是高版本(Pixel API 29)直接拖拽进模拟器,可以直接安装,但使用代码安装时,只显示 “App not installed” ,没有其他信息反馈
因此要注意,测试的 apk 一定要使用签名过的,可以使用压缩软件打开 apk ,如果 META-INF 目录下存在 MANIFEST.MF 文件,就是签过名的,否则没有签名
直接使用启动按钮在目录 app\build\outputs\apk\debug 下生成的apk是未签名的,如下:
使用 Build -> Build Bundle(s)/APK(s) -> Build APK(s) 是签过名的
总结使用 Intent 安装
要注意在 AndroidManifest.xml 中添加权限
下面是完整的使用 Intent 安装 apk 代码
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
const val UNKNOWN_APP_SOURCES_REQUEST_CODE = 0X0008
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val apkPath = cacheDir.canonicalPath + "/app-debug.apk"
Log.d(TAG, "onCreate: apkPath=$apkPath")
installApk(this, apkPath)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& requestCode == UNKNOWN_APP_SOURCES_REQUEST_CODE
&& packageManager.canRequestPackageInstalls()
) {
val apkPath = cacheDir.canonicalPath + "/app-debug.apk"
installApk(this, apkPath)
}
}
}
/**
* apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApk(context: Context, apkFilePath: String) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
installApkO(context, apkFilePath)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
installApkN(context, apkFilePath)
}
else -> {
installApkBeforeN(context, apkFilePath)
}
}
}
/**
* Android 7 之前, apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApkBeforeN(context: Context, apkFilePath: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setDataAndType(
Uri.fromFile(File(apkFilePath)),
"application/vnd.android.package-archive"
)
context.startActivity(intent)
}
/**
* Android 7 开始, apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApkN(context: Context, apkFilePath: String) {
val file = File(apkFilePath)
// 使用FileProvider.getUriForFile()方法将file:// 转为 content://
// authority 参数需要与前面在 AndroidManifest.xml 中 配置的 android:authorities保持一致
val apkUri = FileProvider.getUriForFile(
context,
"com.example.androidstudy.fileProvider",
file
)
Log.d("TAG", "installApkN: apkUri=$apkUri")
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//赋予临时权限给Uri
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(
apkUri,
"application/vnd.android.package-archive"
)
context.startActivity(intent)
}
/**
* Android 8 开始, apk 的安装
* @param context Context
* @param apkFilePath String
*/
fun installApkO(context: Context, apkFilePath: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val isGranted = context.packageManager.canRequestPackageInstalls()
if (!isGranted) {
AlertDialog.Builder(context)
.setCancelable(false)
.setTitle("安装应用需要打开未知来源权限,请去设置中开启权限")
.setPositiveButton("确定") { dialog, witch ->
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
(context as Activity).startActivityForResult(
intent,
UNKNOWN_APP_SOURCES_REQUEST_CODE
)
}
.show()
} else {
installApkN(context, apkFilePath)
}
}
}
静默安装
静默安装 aosp 源码
在源码 frameworks/base/core/java/android/content/pm/PackageInstaller.java 文件里,类注释中有以下两行:
The ApiDemos project contains examples of using this API:
ApiDemos/src/com/example/android/apis/content/InstallApk*.java
.
这告诉我们安装应用的 API 使用方法在 ApiDemos/src/com/example/android/apis/content/InstallApk*.java 中,所在目录全路径是aosp源码的:aosp/development/samples/ApiDemos/src/com/example/android/apis/content,也就是下图中的两个文件
InstallApkSessionApi.java 里是静默安装
InstallApk.java 里是普通安装,也就是前面的调用 intent 进行安装
静默安装注意点
- 静默安装需要进行系统签名
- 需要在 AndroidManifest.xml 需要添加权限 REQUEST_INSTALL_PACKAGES
- 设置启动模式 launchMode 为 singleTask
在 AndroidManifest.xml 需要添加权限和设置启动模式,如下:
权限:
启动模式 launchMode:
问题一
同样,测试的 apk 不能直接使用启动按钮在目录 app\build\outputs\apk\debug 下生成的apk,要使用 Build -> Build Bundle(s)/APK(s) -> Build APK(s) 生成的 apk,否则会报以下错误:
Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]
错误产生原因:
Android Studio 3.0会在 debug apk 的 manifest 文件 application 标签里自动添加 android:testOnly="true"
属性
也可以在被测试 apk 所在项目的 gradle.properties (项目根目录或者 gradle 全局配置目录 ~/.gradle/) 文件中添加:
android.injected.testOnly=false
添加后就可以直接使用启动按钮在目录 app\build\outputs\apk\debug 下生成的apk
静默安装代码
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
private const val PACKAGE_INSTALLED_ACTION =
"com.example.android.apis.content.SESSION_API_PACKAGE_INSTALLED"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
var session: PackageInstaller.Session? = null
try {
// 创建一个 session
val packageInstaller = packageManager.packageInstaller
val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
// 将 apk 文件写入 session
addApkToInstallSession("app-debug.apk", session)
// 创建一个安装状态接收者,当 session 状态变化时,会回调这个接收者
val context: Context = this@MainActivity
val intent = Intent(context, MainActivity::class.java)
intent.action = PACKAGE_INSTALLED_ACTION
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val statusReceiver = pendingIntent.intentSender
// 提交会话,开始安装流程
session.commit(statusReceiver)
} catch (e: IOException) {
throw RuntimeException("Couldn't install package", e)
} catch (e: RuntimeException) {
session?.abandon()
throw e
}
}
}
@Throws(IOException::class)
private fun addApkToInstallSession(assetName: String, session: PackageInstaller.Session) {
// It's recommended to pass the file size to openWrite(). Otherwise installation may fail
// if the disk is almost full.
session.openWrite("package", 0, -1).use { outputStream ->
assets.open(assetName).use { inputStream ->
val buffer = ByteArray(16384)
var length = 0
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
}
//如果 apk 实在某个路径下,就用下面的方法
/*val apkPath = cacheDir.canonicalPath + "/app-debug.apk"
FileInputStream(apkPath).use { inputStream ->
val buffer = ByteArray(16384)
var length = 0
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
}*/
}
}
// 这个 Activity 的启动模式 launchMode 必须为 singleTop,否则不会回调到这个 onNewIntent() 方法
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val extras = intent.extras
if (PACKAGE_INSTALLED_ACTION == intent.action) {
val status = extras.getInt(PackageInstaller.EXTRA_STATUS)
val message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// 需要用户操作确认
val confirmIntent = extras[Intent.EXTRA_INTENT] as Intent
startActivity(confirmIntent)
}
PackageInstaller.STATUS_SUCCESS -> {
Toast.makeText(this, "Install succeeded!", Toast.LENGTH_SHORT).show()
}
PackageInstaller.STATUS_FAILURE,
PackageInstaller.STATUS_FAILURE_ABORTED,
PackageInstaller.STATUS_FAILURE_BLOCKED,
PackageInstaller.STATUS_FAILURE_CONFLICT,
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
PackageInstaller.STATUS_FAILURE_INVALID,
PackageInstaller.STATUS_FAILURE_STORAGE -> {
Toast.makeText(this, "Install failed! $status, $message", Toast.LENGTH_SHORT)
.show()
}
else -> {
Toast.makeText(
this,
"Unrecognized status received from installer: $status",
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
apk 中存放资源
代码中的测试 apk 放在 main/assets 目录下,这里就说一下安卓 apk 中存放资源的方式,有两种路径 res/raw 目录和 main/assets 目录,两者目录下的文件在打包后会原封不动的保存在 apk 包中,不会被编译成二进制,有以下区别
区别 | res/raw 目录 | main/assets 目录 |
---|---|---|
是否映射到 R.java | 是,可使用 R.id.filename | 否 |
是否可以再建立文件夹 | 否 | 是 |
读取操作(都返回 InputStream) | resources.openRawResource(R.raw.test) | assets.open("testAsset.txt") |
文件名大小写 | 只能小写 | 大小写都可以 |
存放资源的读取代码示例
读取操作出来上面表格中的方法,还可以使用下面的方法
//读取 res/raw 目录文件
resources.openRawResourceFd(R.raw.test_raw).createInputStream()
//读取 main/assets 目录文件
assets.openFd("testAsset.txt").createInputStream()
注意:对于 assets 中的文件,aapt 会选择性的对其进行压缩,以减少apk的大小。
在 aosp 源码 aosp/frameworks/base/tools/aapt/Package.cpp 文件中记录了不会被压缩的文件
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif", ".opus",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
因此使用 assets.openFd("testAsset.txt").createInputStream() 方法无法直接读取 .txt 文件内容,直接读取会报以下错误:
Caused by: java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed
而使用 assets.open("testAsset.txt") 方式读取则不会报错,因此尽量使用这个方法
也可以让 aapt 不对 txt 文件进行压缩,需要在 app/build.gradle 中的 android{} 块中内添加以下内容,这样 assets.openFd("testAsset.txt").createInputStream() 方法就可以直接读取 .txt 文件内容了
android {
aaptOptions {
noCompress "txt" //表示不让aapt压缩的文件后缀
}
......
}
代码
//读取 raw 内容目录下 test_raw.txt 文件内容
var rawInputStream = resources.openRawResource(R.raw.test_raw)
val rawContent = rawInputStream.bufferedReader().readLine()
Log.d(TAG, "onCreate: rawContent=$rawContent")
//读取 assets 目录下 testAsset.txt 文件内容
var assetInputStream = assets.open("testAsset.txt")
val assetContent = assetInputStream.bufferedReader().readLine()
Log.d(TAG, "onCreate: assetContent=$assetContent")