前言
最近做的项目里有一个需求,就是在我们的app里可以选择打开微信或qq,然后从里面选取excel文件导入到我们的app,并且上传到服务器。 我细化了下这个需求,我们的app需要做到以下几点:
- 用户可以通过按钮点击跳转到微信和qq,如果系统里未安装对应app给出提示。
- 用户可以在微信或qq聊天窗口中,选择excel文件,用其他应用打开,然后选择用我们的app打开。
- 我们的app获取这些导入的excel文件,并上传到服务器。
- excel文件格式支持xls、xlsx、csv
既然知道了具体的需求,那我们就来看看怎么实现。
第一步
让我们的app可以打开微信或qq
实现代码如下:
/**
* 打开微信
*/
private fun openWX() {
try {
val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.component = ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI")
startActivity(intent)
} catch (e: ActivityNotFoundException) {
showToast(mActivity, getString(R.string.not_found_wechat))
}
}
/**
* 打开qq
*/
private fun openQQ() {
try {
val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.component = ComponentName(
"com.tencent.mobileqq",
"com.tencent.mobileqq.activity.SplashActivity"
)
startActivity(intent)
} catch (e: ActivityNotFoundException) {
showToast(mActivity, getString(R.string.not_found_qq))
}
}
有两点需要注意:
- startActivity 代码需要加上 try-catch 捕获 ActivityNotFoundException 这个异常并处理。因为用户的手机里是有可能没有安装qq或者微信这两个app的,这个时候 startActivity 就会抛出这个异常。
- intent 需要加上 Intent.FLAG_ACTIVITY_NEW_TASK 这个标记位,给qq或者微信开一个新的任务栈,这时在手机任务列表里,qq或者微信也会独立显示,不会跟我们的app显示到一起,这样更方便操作。
第二步
让我们的app可以打开Excel文件
怎么做呢?需要 在 AndroidManifest.xml 配置文件找到我们需要处理excel文件的 Activity 的标签,做出如下修改:
<activity
android:name=".ui.activity.upload.GoodsImportActivity"
android:screenOrientation="portrait"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<data android:mimeType="application/vnd.ms-excel" />
<data android:mimeType="text/comma-separated-values" />
</intent-filter>
</activity>
这样我们的app的这个activity就可以支持打开excel文件。在微信和qq里点击excel文件并选择用其他应用打开的话,我们的app就会出现在应用列表中。
需要注意的是,这个activity的启动模式需要设置为singleTask。试想一下,如果不设置为singleTask,那么可以通过微信或qq多次启动我们的这个activity,造成回退栈里存在多个此activity的实例,这显然是不允许的。所以我们希望这个activity实例在回退栈里只存在一个,并且启动这个activity的时候,如果这个activity实例之上还有其他activity实例,则需要其他activity实例出栈,让这个activity到栈顶。这里涉及到activity启动模式方面的知识,简单梳理一下: 启动模式分为四种,standard、singleTop、singleTask、singleInstance。
- standard 标准模式。在AndroidManifest.xml文件里未定义launchMode 的话,默认使用这个。每次启动activity都会创建一个实例。
- singleTop 栈顶复用模式。如果一个activity存在任务栈的栈顶,则再次启动时不重新创建实例,否则会重新创建实例加入到栈中。当它在栈顶并再次启动时,不会调用activity的onCreate和onStart方法,会调用onNewIntent方法。
- singleTask 栈内复用模式。如果一个activity存在任务栈中,则再次启动这个activity时,则不会重新创建实例,并且如果这个activity之上还存在其他activity实例,则会让其他activity全部出栈,让其到栈顶。这时也不会调用onCreate和onStart方法会调用onNewIntent方法。
- singleInstance 单实例模式。拥有singleTask的特性,并且在启动activity时,系统会单独创建一个任务栈,将这个activity加入进去,这个任务栈只能存在这一个activity实例,即使在这个activity再启动一个其他的activity也会安排到新的任务栈当中去。
这里就涉及到Intent的隐式启动的相关知识也梳理一下:
我们知道Intent是分为显式调用和隐式调用。如果一个Intent显示调用和隐式调用同时存在时,是以显示调用为主。显式调用是需要指定启动组件的包名和类名。而隐式调用是通过IntentFilter定义的过滤规则包括action、category、data的过滤规则来匹配寻找手机里可以响应这个intent的activity。所以这里我们的app需要在为这个activity定义一组intent-filter,让我们的activity可以响应这个隐式调用。
既然涉及到IntentFilter匹配,那就来梳理一下IntentFilter的匹配规则及注意事项:
-
IntentFilter过滤信息有action、category、data。一个过滤列表中的action、category、data可以存在多个。
-
Intent需要同时匹配aciton类别、category类别、data类别,才能启动这个activity。
-
action是一个字符串,action匹配就是Intent里的action和过滤规则中的action字符串的值相同。所以上面的例子里我们要定义一条action规则
<action android:name="android.intent.action.VIEW"/>
,这样就能和qq或微信的intent中的action匹配上。 -
一个
<intent-filter>
里可以有多个action,Intent里的只要有一个action能和<intent-filter>
中的一个action匹配成功即可。 -
category匹配要求如果Intent里有category则 每一个category 都需要和
<intent-filter>
中的一个category相同。需要注意的是Intent中可以不设置category,但系统会在startActivity时默认加入android.intent.category.DEFAULT
这个category。所以我们的app里的这个activity如果想要响应隐式调用Intent的话,则必须在<intent-filter>
中加上<category android:name="android.intent.category.DEFAULT" />
。 -
data由mimeType 和 URI 组成。完整的URI结构是:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern]
。URI如果不指定scheme和host,那么其他参数无效,整个URI也无效。mimeType表示媒体类型,结构是:type/subType。我们可以通过这个定义文件的格式。我们这里接收的是xls、xlsx和csv的excel文件,所以需要在里定义三个data,分别指定mimeType如下:<!--.xls文件-->
<data android:mimeType="application/vnd.ms-excel" />
<!--.xlsx文件-->
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<!--.csv文件-->
<data android:mimeType="text/comma-separated-values" />
这里再列举一些其他常用的mimeType:
mimeType类型 文件类型 text/plain 纯文本 text/html html文档 image/png png图片 video/* 视频 -
data匹配规则是如果含有data规则,则Intent必须含有一个data数据和里的一个data完全匹配。这里完全匹配的意思是 中出现的mimeType 和 URI分别至少有一条需要在Intent里包含。不太好理解,举一个例子:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
</intent-filter>
这里要匹配这个<intent-filter>
需要如下代码:
val intent = Intent()
intent.action = Intent.ACTION_VIEW
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.type = "text/plain" //或者intent.type = "image/*"
startActivity(intent)
但如果把
<data android:mimeType="image/*" />
改为
<data android:mimeType="image/*" android:scheme="test" android:host="www.test.com" />
那么上述Intent则无法匹配到这个data,因为第二条中包含URI信息,所以Intent里也得加入对应的URI信息才能匹配。如下把这行代码
intent.type = "text/plain"
改成
intent.setDataAndType(Uri.parse("test://www.test.com"),"text/plain")
即可匹配到。这里将第一条加入URI数据改为<data android:mimeType="text/plain" android:scheme="hehe" android:host="www.hehe.com"/>
,上述这个Intent一样能够匹配成功。因为里的mimeType 和 URI 需要分开来看。
<data>
写成如下这样
<data android:mimeType="text/plain" android:scheme="hehe" android:host="www.hehe.com"/>
<data android:mimeType="image/*" android:scheme="test" android:host="www.test.com"/>
和
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
<data android:scheme="hehe" android:host="www.hehe.com" />
<data android:scheme="test" android:host="www.test.com" />
这两者的效果是一样的。
第三步
让我们这个activity找到微信或qq分享过来的文件
首先我们需要在activity里的onCreate()方法和onNewIntent这两个方法里通过Intent.getData()方法获取文件的URI,然后通过这个Uri.getPath获得文件的路径。 我们先来看一下获取的这个路径是什么样子的


有两个办法
- 通过uri推断出具体的文件路径。例如上面这个路径应该是外置存储路径下的tencent文件夹下面的具体目录。qq的是
/storage/emulated/0/Tencent/QQfile_recv/
,微信的是/storage/emulated/0/tencent/MicroMsg/Download
,可以截取uri的path得到真实路径。例如:
intent?.let {
it.data?.let { uri ->
val index = uri.path.toLowerCase().indexOf("tencent")
val filePath = Environment.getExternalStorageDirectory().absolutePath + "/" + uri.path.substring(index)
//do something else
...
}
}
但是我并不推荐这么做,因为路径有可能发生改变,如果有一天不存放在tencent
目录下,那这段代码就是错的。
- 通过ContentResolver 获得文件的输入流,copy一份文件到我们自己的目录下,然后再对其做操作。
intent?.let {
it.data?.let { uri ->
val tempFilePath = Environment.getExternalStorageDirectory().absolutePath + "/myappdir/temp.xlsx"
val tempFile = File(tempFilePath)
val fileInputStream = contentResolver.openInputStream(uri)
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(tempFile)
val buffer = ByteArray(1024)
var len = -1
while (run{len = fileInputStream.read(buffer);len != -1}) {
outputStream.write(buffer,0,len)
}
} catch (e: java.lang.Exception){
e.printStackTrace()
} finally {
try {
outputStream?.close()
fileInputStream.close()
} catch (e: IOException){
e.printStackTrace()
}
}
//do something else
...
}
}
推荐使用这种做法。
总结
至此我们已经完成了从微信或qq导入excel文件到己方app的需求。这个看似简单的需求,一步一步做下来,发现涉及的知识点还是挺多的。包括:
- Intent隐式启动
- IntentFilter匹配规则
- activity启动模式
- FileProvider、Uri
这些可以深挖的地方。以前我可能只是完成这项功能就好了,这次细细去探究才发现,这里面蕴含的知识点还真不少,最后有一种恍然大悟的感觉,看来以后要多多思考和总结。