Android~快捷方式兼容适配


背景

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."
        )
    }
}

总结

安卓适配坑很多,会感觉越测越没底,但我们还是要尝试一下,解决问题的思路比较重要。
该方案中比较核心的点:

  1. 给用户比较好的体验(产品角度,用户点击有回应)
  2. 各厂商Launcher获取快捷方式以及相关权限适配
  3. 对快捷方式的理解
    安卓碎片化真的很严重,严重到你都不相信自己写的代码逻辑,作为一个移动开发者我们能做的就是协调好用户和厂商。

附上源码,上面就是安卓适配的全部内容,如果写的不错欢迎点赞收藏,点个关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是嗨森啦

如果文章还不错,欢迎点赞收藏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值