背景
2022年了,你们的APP创建快捷方式适配得怎么样了?最近收到用户反馈公司APP创建快捷方式无效点击无任何反应,然后我去看了下相关代码,发现代码最后一次提交是19年的了。想想19年到现在安卓都推出几个版本了,比较怀疑代码兼容性适配做的不好!找了几部手机对比了微信是如何做适配的,发现微信这块做得不怎么好,只要用户点击添加到桌面
,总会提示已尝试添加到桌面的弹框,如果确实添加无效跳转到各个厂商的打开相关权限的指导页面,个人感觉用户体验糟糕透了。快捷方式这个功能对于用户还是蛮重要的,它能很方便打开我们APP中一些常用功能(虽然大多数人总是先打开APP再点下一步下一步…),于是自己花了点时间调研了安卓快捷方式的适配,方便大家参考。
一、适配思路
首先快捷方式的创建,Jetpack中有
ShortcutManagerCompat ,它有静态、动态、固定几种快捷方式,具体如何使用官方文档比较详细,这里不再重复,本文针对的动态快捷方式的创建。
通过查看ShortcutManagerCompat 提供的几个API源码,它内部已经做了一些适配。经过苦苦测试,发现使用它这套API不同厂商机子上结果难以把控。存在几个问题:
- 低版本<25安卓有的可以重复创建,有的不可以
- 同样的程序,有的手机功能正常,有的手机无任何反应(权限隐藏的比较深)
- 不同系统版本和手机,权限管理、交互设计不一样
看到这里,相信你大概知道微信为啥点击就弹框提醒了吧,它已经躺平,但我们不能放弃。
掘金这篇文章不错Android 创建桌面快捷方式,从中我们可以看到各个厂商魔改安卓的思路,但我们真的对付不过来厂商,假如A厂商的高管又跑路去创办了C厂,那我们是不是又要花点时间分析它是如何实现的。为了用户体验稍微再好一点,我们的适配方案采取下面的流程:
二、实现细节
1.判断快捷方式是否存在
上述流程图中,可以看到我们调用到了两次这个api。先读取是否支持请求PinShortcut,读不到则遍历读取contentResolver提供的,都读不到就返回快捷方式不存在。
fun hasShortCut(
context: Context,
id: String,
title: String,
result: ((Boolean, Exception?) -> (Unit))
) {
val isReqPinShortcut =
ShortcutManagerCompat.isRequestPinShortcutSupported(context) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
val shortCuts =
ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_PINNED)
if (shortCuts.isNotEmpty() || isReqPinShortcut) {
var hasShortcut = false
shortCuts.find {
it.id == id
}?.let {
hasShortcut = true
}
result.invoke(hasShortcut, null)
} else {
val resolver = context.contentResolver
for (s in getCheckPackages(context)) {
try {
val url = Uri.parse("content://${s}.settings/favorites?notify=true")
val cursor = resolver.query(
url,
arrayOf("title", "iconResource"),
"title=?",
arrayOf(title.trim()),
null
)
cursor?.run {
result.invoke((count > 0), null)
return
}
} catch (e: Exception) {
e.printStackTrace()
result.invoke(false, e)
}
}
result.invoke(false, null)
}
}
private fun getCheckPackages(context: Context): Array<String> {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
val res = context.packageManager.resolveActivity(intent, 0)
val pkgName = res?.activityInfo?.packageName
return if (pkgName.isNullOrEmpty()) {
arrayOf(Const.LAUNCHER_SETTINGS)
} else {
arrayOf(Const.LAUNCHER_SETTINGS, pkgName)
}
}
这个函数依赖于权限配置,我们还需要在清单文件中配置各个厂商的Launcher读取权限,这里说明一下,我们不考虑第三方Launcher了。原因是厂商Launcher不会让你随便卸载,除非你有root权限 爱搞机。
<!-- 各厂商快捷方式 权限 -->
<!-- 系统原生 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher2.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher2.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
<!-- LG -->
<uses-permission android:name="com.lge.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.lge.launcher.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.lge.launcher2.permission.READ_SETTINGS" />
<uses-permission android:name="com.lge.launcher2.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.lge.launcher3.permission.READ_SETTINGS" />
<uses-permission android:name="com.lge.launcher3.permission.WRITE_SETTINGS" />
<!-- htc -->
<uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.htc.launcher.permission.WRITE_SETTINGS" />
<!-- 华为 -->
<uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.huawei.launcher2.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.huawei.launcher2.permission.READ_SETTINGS" />
<uses-permission android:name="com.huawei.launcher2.permission.WRITE_SETTINGS" />
<uses-permission android:name="com.huawei.launcher3.permission.READ_SETTINGS" />
<uses-permission android:name="com.huawei.launcher3.permission.WRITE_SETTINGS" />
<!-- 三星 -->
<uses-permission android:name="com.sec.android.app.twlauncher.settings.READ_SETTINGS" />
<uses-permission android:name="com.sec.android.app.twlauncher.settings.WRITE_SETTINGS" />
<!-- Oppo -->
<uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS" />
<!-- ViVo -->
<uses-permission android:name="com.bbk.launcher2.permission.READ_SETTINGS" />
<uses-permission android:name="com.bbk.launcher2.permission.WRITE_SETTINGS" />
<!-- 魅族 -->
<uses-permission android:name="com.meizu.flyme.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.meizu.flyme.launcher.permission.WRITE_SETTINGS" />
2.创建快捷方式
主要是ShortcutManagerCompat的使用,然后配合BroadcastReceiver,附上关联的两个类这里不细说了。
fun addShortcut(
context: Context,
id: String,
icon: Int,
title: String,
intent: Intent
): Boolean {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(title)
.setIcon(IconCompat.createWithResource(context, icon))
.setIntent(intent)
.build()
val bundle = Bundle().apply {
putString(Const.EXTRA_SHORTCUT_ID, id)
putString(Const.EXTRA_SHORTCUT_LABEL, title)
}
val sender = IntentSenderHelper.getDefaultIntentSender(
context,
ShortcutBroadcastReceiver.ACTION, ShortcutBroadcastReceiver::class.java, bundle
)
val isReqPinShortcut = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
return if(isReqPinShortcut) {
ShortcutManagerCompat.requestPinShortcut(context, shortcut, sender)
} else false
}
class ShortcutBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ACTION == intent.action) {
val id = intent.getStringExtra(Const.EXTRA_SHORTCUT_ID)
val label = intent.getStringExtra(Const.EXTRA_SHORTCUT_LABEL)
Log.w(TAG, "onReceive: id = $id, label = $label")
if(label.isNullOrEmpty() || id.isNullOrEmpty()){
return
}
ShortcutUtil.hasShortCut(context,id,label) { ret, err ->
Toast.makeText(context,"create result:$ret",Toast.LENGTH_SHORT).show()
}
}
}
companion object {
const val ACTION = "com.vesync.shortcut.create"
}
}
object IntentSenderHelper {
fun getDefaultIntentSender(context: Context, action: String): IntentSender {
return PendingIntent.getBroadcast(
context, 0, Intent(action),
PendingIntent.FLAG_ONE_SHOT
).intentSender
}
fun getDefaultIntentSender(
context: Context,
action: String,
clz: Class<*>,
bundle: Bundle?
): IntentSender {
val intent = Intent(action)
intent.component = ComponentName(context, clz)
if (bundle != null) {
intent.putExtras(bundle)
}
return PendingIntent.getBroadcast(context, 0, intent,PendingIntent.FLAG_ONE_SHOT).intentSender
}
}
3.修改删除快捷方式
其实删除快捷方式这个api不叫删除,叫disable不可用,调用后图标会置灰用户点击后会提示你设置的提示语。删除的权限应该还是Launcher把控着的。修改函数在低版本上测试是不可用的,简单封装。
fun updateShortCut(
context: Context,
id: String,
icon: Int,
title: String,
intent: Intent
): Boolean {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
/*val addIntent = Intent(Const.ACTION_INSTALL_SHORTCUT).apply {
putExtra(Intent.EXTRA_SHORTCUT_NAME, title)
putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(context, icon))
putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent)
}
context.sendBroadcast(addIntent)*/
false
} else {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(title)
.setIcon(IconCompat.createWithResource(context, icon))
.setIntent(intent)
.build()
ShortcutManagerCompat.enableShortcuts(context, arrayListOf(shortcut))
ShortcutManagerCompat.updateShortcuts(context, arrayListOf(shortcut))
}
}
/**
* 删除快捷方式
*/
fun delShortCut(context: Context, clz: Class<*>, id: String, title: String) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
val mainIntent = Intent(Intent.ACTION_MAIN).apply {
setClass(context.applicationContext, clz)
addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
addCategory(Intent.CATEGORY_LAUNCHER)
}
val delIntent = Intent(Const.ACTION_UNINSTALL_SHORTCUT).apply {
putExtra(Intent.EXTRA_SHORTCUT_NAME, title)
putExtra(Intent.EXTRA_SHORTCUT_INTENT, mainIntent)
}
context.sendBroadcast(delIntent)
} else {
ShortcutManagerCompat.disableShortcuts(
context,
arrayListOf(id),
"$title has been removed."
)
}
}
总结
安卓适配坑很多,会感觉越测越没底,但我们还是要尝试一下,解决问题的思路比较重要。
该方案中比较核心的点:
- 给用户比较好的体验(产品角度,用户点击有回应)
- 各厂商Launcher获取快捷方式以及相关权限适配
- 对快捷方式的理解
安卓碎片化真的很严重,严重到你都不相信自己写的代码逻辑,作为一个移动开发者我们能做的就是协调好用户和厂商。
附上
源码
,上面就是安卓适配的全部内容,如果写的不错欢迎点赞收藏,点个关注。