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. 对快捷方式的理解
    安卓碎片化真的很严重,严重到你都不相信自己写的代码逻辑,作为一个移动开发者我们能做的就是协调好用户和厂商。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在开发 Android 应用程序时,需要考虑 WebSocket 兼容性问题,主要有以下几个方面: 1. WebSocket API 兼容性:Android 平台在不同版本中可能存在 WebSocket API 的差异,需要根据目标用户的 Android 系统版本选择适当的 API。 2. 网络环境兼容性:在某些网络环境下,WebSocket 可能会受到限制或阻塞,需要通过其他方式实现实时通信,例如使用长轮询或 SSE。 3. 安全性兼容性:WebSocket 是一种明文协议,可能会存在安全漏洞,需要注意避免使用不安全的 WebSocket 实现,或者加密 WebSocket 数据以确保数据安全。 为了解决这些兼容性问题,可以采取以下几个适配方法: 1. 选择适当的 WebSocket 实现库:根据目标用户的 Android 系统版本选择适当的 WebSocket 实现库,例如在 Android 5.0 及以上版本中可以使用 android.net.http.WebSocket 类来实现 WebSocket 功能,而在 Android 4.4 及以下版本,则需要使用第三方库来实现 WebSocket。 2. 处理网络环境问题:在某些网络环境下,WebSocket 可能会受到限制或阻塞,需要通过其他方式实现实时通信。例如,可以使用长轮询或 SSE,或者使用反向Ajax等技术来实现实时通信。 3. 处理安全问题:可以使用 SSL/TLS 等方式加密 WebSocket 数据,确保数据安全。 4. 处理 WebSocket 连接管理问题:需要合理管理 WebSocket 连接,例如在应用程序进入后台或网络状态发生变化时,需要关闭 WebSocket 连接以避免网络带宽占用过多,或者重新建立 WebSocket 连接以确保通信正常。 综上所述,为了确保 Android 应用程序的 WebSocket 功能兼容性,需要根据不同版本的 Android 平台选择适当的 WebSocket 实现库,并合理处理网络环境、安全性和连接管理等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不会画板子的物联网工程师

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

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

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

打赏作者

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

抵扣说明:

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

余额充值