Android中的SPI实现

Android中的SPI实现

SPI是JVM世界中的标准API,但在Android应用程序中并不常用。然而,它可以非常有用地实现插件架构。让我们探讨一下如何在Android中利用SPI。

问题

在Android中,不同的提供者为推送功能提供服务,而在大型项目中,使用单一实现是不可行的。以下是一些可用的提供者:

  • FCM(Firebase Cloud Messaging):主要的推送服务实现,但需要Google服务,可能无法在所有设备上使用。
  • ADM(Amazon Device Messaging):Amazon设备(Kindle设备)上的实现,仅在Amazon设备上运行。
  • HCM(Huawei Cloud Messaging):华为设备上的实现。
  • Baidu(Baidu Push SDK):主要用于中国的推送服务实现。

由于有如此多的服务,管理和初始化它们变得具有挑战性。

当我们需要为不同的应用程序构建提供不同的服务集时,问题变得更加困难。以下是一些示例:

  • Google Play控制台不允许发布包含百度服务的应用程序。因此,百度服务应仅包含在面向中国的构建中。
  • Amazon设备消息传递仅适用于Amazon设备,因此在仅针对Amazon应用商店的构建中包含它是有意义的。
  • 华为实现在面向华为商店的构建中是有意义的。

解决方案

为了解决这个问题,我们可以从创建推送服务实现的抽象层开始。这个抽象层应该放在一个单独的Gradle模块中,以便它可以轻松地作为其他实现模块的依赖项添加。

抽象层

我们可以通过创建以下通用接口来为推送服务定义抽象层:

package com.kurantsov.pushservice

import android.content.Context

/**
 * Interface used to provide push service implementation via SPI
 */
interface PushService {
    /**
     * Type of the push service implementation
     */
    val type: PushServiceType

    /**
     * Priority of the push service implementation
     */
    val priority: PushServicePriority

    /**
     * Returns if the push service implementation is available on the device
     */
    fun isAvailable(context: Context): Boolean

    /**
     * Initializes push service
     */
    fun initialize(context: Context)
}

/**
 * Describes type of the push service implementation
 */
interface PushServiceType {
    val name: String
    val description: String
}

sealed class PushServicePriority(val value: Int) {
    object High : PushServicePriority(0)
    object Medium : PushServicePriority(1)
    object Low : PushServicePriority(2)
}

实现

然后,我们可以基于推送服务提供者实现一个通用接口。

为此,我们可以为每个实现创建一个Gradle模块。

Firebase Cloud Messaging实现示例:

package com.kurantsov.pushservice.firebase

import android.content.Context
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.ktx.messaging
import com.kurantsov.pushservice.PushService
import com.kurantsov.pushservice.PushServiceManager
import com.kurantsov.pushservice.PushServicePriority
import com.kurantsov.pushservice.PushServiceType

class FirebasePushService : PushService {
    override val type: PushServiceType = FirebasePushServiceType
    override val priority: PushServicePriority = PushServicePriority.High

    override fun isAvailable(context: Context): Boolean {
        val availability =
            GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
        return availability == ConnectionResult.SUCCESS
    }

    override fun initialize(context: Context) {
        Firebase.messaging.token.addOnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.w(TAG, "Fetching FCM registration token failed", task.exception)
            }

            val token = task.result

            PushServiceManager.setPushToken(token, FirebasePushServiceType)
        }
    }

    private companion object {
        const val TAG = "FirebasePushService"
    }
}

object FirebasePushServiceType : PushServiceType {
    override val name: String = "FCM"
    override val description: String = "Firebase"
}

Amazon Device Messaging实现示例:

package com.kurantsov.pushservice.amazon

import android.content.Context
import com.amazon.device.messaging.ADM
import com.kurantsov.pushservice.PushService
import com.kurantsov.pushservice.PushServicePriority
import com.kurantsov.pushservice.PushServiceType

/**
 * Amazon device messaging implementation of the push service
 */
class AmazonPushService : PushService {
    override val type: PushServiceType = AmazonPushServiceType
    override val priority: PushServicePriority = PushServicePriority.High

    override fun isAvailable(context: Context): Boolean {
        return isAmazonServicesAvailable
    }

    override fun initialize(context: Context) {
        val adm = ADM(context)
        adm.registrationId?.let { token ->
            handleRegistrationSuccess(token)
        } ?: run {
            adm.startRegister()
        }
    }
}

object AmazonPushServiceType : PushServiceType {
    override val name: String = "ADM"
    override val description: String = "Amazon"
}

/**
 * Returns if amazon device messaging is available on the device
 */
val isAmazonServicesAvailable: Boolean by lazy {
    try {
        Class.forName("com.amazon.device.messaging.ADM")
        true
    } catch (e: ClassNotFoundException) {
        false
    }
}

实现注册

为了使实现通过SPI“可发现”,我们需要进行注册。这可以通过在META-INF/services/{接口的全限定名}中添加实现的完全限定名称来完成。这需要在提供接口实现的每个模块中完成。

Firebase实现文件示例内容:

com.kurantsov.pushservice.firebase.FirebasePushService
请注意,要将服务文件夹的完整路径包含在模块的结果AAR中,路径是:{模块路径}/src/main/resources/META-INF/services

Android Studio项目视图中的SPI注册示例

用法

最后一步是使用接口实现。以下是SPI使用示例:

import java.util.ServiceLoader

private fun listImplementations(context: Context) {
    //Loading push service implementations
    val serviceLoader = ServiceLoader.load(PushService::class.java)
    //Logging implementations
    serviceLoader
        .sortedBy { pusService -> pusService.priority.value }
        .forEach { pushService ->
            val isAvailable = pushService.isAvailable(context)
            Log.d(
                TAG, "Push service implementation - ${pushService.type.description}, " +
                        "available - $isAvailable"
            )
        }
}

示例输出如下:

Push service implementation - Firebase, available - true
Push service implementation - Amazon, available - false
Push service implementation - Huawei, available - true
Push service implementation - Baidu, available - true

完整代码请参考

https://github.com/ArtsemKurantsou/SPI4Android

额外内容

PushServiceManager

以下是一个更“真实”的示例,展示了PushServiceManager的用法:

package com.kurantsov.pushservice

import android.content.Context
import android.util.Log
import java.util.ServiceLoader
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.atomic.AtomicBoolean

object PushServiceManager {
    private const val TAG = "PushServiceManager"
    var pushToken: PushToken = PushToken.NotInitialized
        private set

    private val isInitialized: AtomicBoolean = AtomicBoolean(false)
    private val tokenChangedListeners: MutableSet<OnPushTokenChangedListener> =
        CopyOnWriteArraySet()
    private var selectedPushServiceType: PushServiceType? = null

    fun initialize(context: Context) {
        if (isInitialized.get()) {
            Log.d(TAG, "Push service is initialized already")
            return
        }
        synchronized(this) {
            if (isInitialized.get()) {
                Log.d(TAG, "Push service is initialized already")
                return
            }
            performServiceInitialization(context)
        }
    }

    private fun performServiceInitialization(context: Context) {
        //Loading push service implementations
        val serviceLoader = ServiceLoader.load(PushService::class.java)
        val selectedImplementation = serviceLoader
            .sortedBy { pusService -> pusService.priority.value }
            .firstOrNull { pushService ->
                val isAvailable = pushService.isAvailable(context)
                Log.d(
                    TAG, "Checking push service - ${pushService.type.description}, " +
                            "available - $isAvailable"
                )
                isAvailable
            }
        if (selectedImplementation != null) {
            selectedImplementation.initialize(context)
            selectedPushServiceType = selectedImplementation.type
            isInitialized.set(true)
            Log.i(TAG, "Push service initialized with ${selectedImplementation.type.description}")
        } else {
            Log.e(TAG, "Push service implementation failed. No implementations found!")
            throw IllegalStateException("No push service implementations found!")
        }
    }

    /**
     * Adds listener for the push token updates. Called immediately if token is available
     * already.
     */
    fun addOnPushTokenChangedListener(listener: OnPushTokenChangedListener) {
        tokenChangedListeners.add(listener)
        val currentToken = pushToken
        if (currentToken is PushToken.Initialized) {
            listener.onPushTokenChanged(currentToken)
        }
    }

    /**
     * Removes listener for the push token updates.
     */
    fun removeOnPushTokenChangedListener(listener: OnPushTokenChangedListener) {
        tokenChangedListeners.remove(listener)
    }

    /**
     * Called by push service implementation to notify about push token change.
     */
    fun setPushToken(token: String, serviceType: PushServiceType) {
        if (selectedPushServiceType != serviceType) {
            Log.w(TAG, "setPushToken called from unexpected implementation. " +
                    "Selected implementation - ${selectedPushServiceType?.description}, " +
                    "Called by - ${serviceType.description}")
            return
        }
        val initializedToken = PushToken.Initialized(token, serviceType)
        this.pushToken = initializedToken
        tokenChangedListeners.forEach { listener ->
            listener.onPushTokenChanged(initializedToken)
        }
    }

    /**
     * Called by push service implementation to notify about push message.
     */
    fun processMessage(message: Map<String, String>, sender: String) {
        Log.d(TAG, "processMessage: sender - $sender, message - $message")
    }

}

PushServiceInitializer

为了简化推送服务的最终集成,我们可以使用App启动库,这样“app”模块就不需要添加其他内容。

Initializer:

package com.kurantsov.pushservice

import android.content.Context
import android.util.Log
import androidx.startup.Initializer

class PushServiceInitializer : Initializer<PushServiceManager> {
    override fun create(context: Context): PushServiceManager {
        runCatching {
            PushServiceManager.initialize(context)
        }.onFailure { e ->
            Log.e(TAG, "create: failed to initialize push service", e)
        }.onSuccess {
            Log.d(TAG, "create: Push service initialized successfully")
        }
        return PushServiceManager
    }

    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()

    private companion object {
        const val TAG = "PushServiceInitializer"
    }
}

AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="com.kurantsov.pushservice.PushServiceInitializer"
                android:value="androidx.startup" />
        </provider>

    </application>
</manifest>

编译时实现选择

由于使用了推送服务实现的SPI,我们有几个模块提供了实现。要将其添加到最终的apk中,我们只需要在实现模块上添加依赖关系。

有几种方法可以在编译时添加/删除依赖项。例如:

我们可以创建几个应用程序的构建变体,并使用基于变体的依赖关系(例如,如果我们有华为变体,我们可以使用huaweiImplementation而不是implementation;这样只会为中国变体添加依赖项)。
基于编译标志进行依赖项的添加。
以下是基于标志的方法示例( app/build.gradle.kts):

dependencies {
    implementation(project(":push-service:core"))
    implementation(project(":push-service:firebase"))
    if (getBooleanProperty("amazon")) {
        implementation(project(":push-service:amazon"))
    }
    if (getBooleanProperty("huawei")) {
        implementation(project(":push-service:huawei"))
    }
    if (getBooleanProperty("baidu")) {
        implementation(project(":push-service:baidu"))
    }
}

fun getBooleanProperty(propertyName: String): Boolean {
    return properties[propertyName]?.toString()?.toBoolean() == true
}

然后,我们可以在编译过程中使用命令行中的-P{标志名称}={值}来添加这些标志。以下是添加所有实现的命令示例:

gradle :app:assemble -Pamazon=true -Phuawei=true -Pbaidu=true

aar/apk中的SPI实现

您可以使用Android Studio内置的apk资源管理器验证aar/apk文件中的SPI实现。

在aar文件中,META-INF/services文件夹位于classes.jar内部。Firebase实现aar示例:
Firebase 实现AAR 示例
在apk文件中,META-INF/services文件夹位于apk根目录中。以下是最终apk示例:
APK 示例

参考链接

https://github.com/ArtsemKurantsou/SPI4Android
https://en.wikipedia.org/wiki/Service_provider_interface
https://developer.android.com/topic/libraries/app-startup

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/328369.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

『C++成长记』内存管理

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;C &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、C/C内存分布 二、内存管理方式 &#x1f4d2;2.1C语言内存管理方式 &#x…

vue2使用 element表格展开功能渲染子表格

默认样式 修改后 样式2 <el-table :data"needDataFollow" border style"width: 100%"><el-table-column align"center" label"序号" type"index" width"80" /><el-table-column align"cent…

CTF CRYPTO 密码学-3

题目名称&#xff1a;反编译 题目描述&#xff1a; 分析 题目给出一个pyc后缀的文件&#xff0c;需要使用uncompyle6模块去还原成py文件 uncompyle6简介 uncompyle6 是一个 Python 反编译器&#xff0c;它能够将 Python 字节码&#xff08;.pyc 文件&#xff09;转换回源代码&…

定制键盘设计

方案1 stm32方案 参考 智辉君的键盘 方案2 沁恒方案 CH9328与ch9329区别&#xff1a;一个是单向&#xff0c;一个是双向。 ch9329是ch9328的升级款。 原理篇4、CH9328使用-CSDN博客https://blog.csdn.net/qq_44817843/article/details/112124822

容器部署的nextcloud配置onlyoffice时开启密钥

容器部署的nextcloud配置onlyoffice时开启密钥 配置 进入onlyoffice容器 docker exec -it 容器id bash编辑配置vi /etc/onlyoffice/documentserver/local.json enable设置为true&#xff0c;并配置secret 重启容器&#xff0c;并将配置的密钥填入nextcloud密钥页面 docker r…

力扣hot100 零钱兑换 背包 滚动数组

Problem: 322. 零钱兑换 文章目录 &#x1f388; 思路&#x1f496; Code &#x1f388; 思路 &#x1f468;‍&#x1f3eb; 大佬题解 &#x1f496; Code ⏰ 时间复杂度: O ( n ) O(n) O(n) class Solution {public int coinChange(int[] coins, int amount){int INF …

「爱下馆子」真的会寿命更短吗?

​“爱下馆子”真的会寿命更短吗&#xff1f; 随着现代生活节奏的加快&#xff0c;越来越多的人选择在外就餐&#xff0c;也就是我们常说的“下馆子”。然而&#xff0c;最近的一项研究发现&#xff0c;每天至少两顿在外就餐的人&#xff0c;全因死亡率增加了49%。这一发现引发…

Python图像处理【18】边缘检测详解

边缘检测详解 0. 前言1. 图像导数2. LoG/zero-crossing2.1 Marr-Hildteth 算法 3. Canny 与 holistically-nested 算法3.1 Canny 边缘检测3.2 holistically-nested 边缘检测 小结系列链接 0. 前言 边缘是图像中两个区域之间具有相对不同灰级特性的边界&#xff0c;或者说是亮度…

Configure Virtual Serial Port Driver串口模拟器VSPD

背景 串口通讯想必做硬件开发和软件的人来说都相当了解&#xff0c;以前的电脑&#xff0c;基本标配都包含一个串口。但现在的电脑&#xff0c;基本都没有配置串口了&#xff0c;如果要使用串口的功能&#xff0c;基本就要用一个USB转串口的硬件模块。 虚拟串口&#xff08;虚…

不同打包工具下的环境变量配置方式对比

本文作者为 360 奇舞团前端开发工程师 天明 前言 在现代的JavaScript应用程序开发中&#xff0c;环境变量的配置是至关重要的。不同的应用场景和部署环境可能需要不同的配置&#xff0c;例如开发、测试和生产环境。最常见的需求是根据不同的环境&#xff0c;配置如是否开启sour…

Transformer从菜鸟到新手(七)

引言 上篇文章加速推理的KV缓存技术&#xff0c;本文介绍让我们可以得到更好的BLEU分数的解码技术——束搜索。 束搜索 我们之前生成翻译结果的时候&#xff0c;使用的是最简单的贪心搜索&#xff0c;即每次选择概率最大的&#xff0c;但是每次生成都选择概率最大的并不一定…

C++实战Opencv第一天——win11下配置vs,opencv环境和运行第一个c++代码(从零开始,保姆教学)

OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习软件库&#xff0c;它提供了大量的通用算法和功能&#xff0c;用于处理图像和视频数据。C 通常提供比 Python 更高的执行速度&#xff0c;对于需要高性能处理的任务&#x…

【XR806开发板试用】留言板功能开发

开发板简介 XR806开源鸿蒙开发板是一款基于XR806芯片设计&#xff0c;高度集成WiFi/BLE/常用外设&#xff0c;可供开发者进行方案评估、DIY或小规模产品研发&#xff0c;可广泛应用于智能家居、智能楼宇、智能城市和工业互联等领域。 搭载OpenHarmony系统&#xff08;已通过O…

Linux中的yum源仓库和NFS文件共享服务

一.yum简介 1.1 yum简介 yum&#xff0c;全称“Yellow dog Updater, Modified”&#xff0c;是一个专门为了解决包的依赖关系而存在的软件包管理器。类似于windows系统的中电脑软件关键&#xff0c;可以一键下载&#xff0c;一键安装和卸载。yum 是改进型的 RPM 软件管理器&am…

使用CloudFlare-Woker搭建简易网站

使用CloudFlare-Woker搭建简易网站 1、首先到CloudFlare官网登录或注册自己的账号&#xff1a;Cloudflare 中国官网 | 智能化云服务平台 | 免费CDN安全防护 | Cloudflare (cloudflare-cn.com) 注册一个账号 2、登录账号后进入仪表盘网页&#xff0c;选择Workers & Pages页…

2019年认证杯SPSSPRO杯数学建模C题(第二阶段)保险业的数字化变革全过程文档及程序

2019年认证杯SPSSPRO杯数学建模 基于统计建模的车险业数字变革研究 C题 保险业的数字化变革 原题再现&#xff1a; 车险&#xff0c;即机动车辆保险。保险自身是一种分散风险、消化损失的经济补偿制度&#xff0c;车险即为分散机动车辆在行驶过程中可能发作的未知风险和损失…

uni-app引用矢量库图标

矢量库引用 导入黑色图标 1.生成连接&#xff0c;下载样式 2.导入项目&#xff08;字体样式&#xff09; 3.引入css样式 4.替换font-face 5.使用图标&#xff08;字体图标&#xff0c;只有黑色&#xff09; 导入彩色图标 1.安装插件 npm install -g iconfont-tools2.…

修复系统和修复常见安卓问题的 10 个应用

我们都喜欢我们的 Android 智能手机&#xff0c;对吧&#xff1f;有很多值得喜欢的地方。 Android 手机易于使用且通常无故障&#xff0c;但毕竟它只是一台机器&#xff0c;偶尔也会出现问题。面对现实吧&#xff0c;我们的智能手机并不完美。用户经常遇到的一些常见 Android …

使用scipy处理图片——滤镜处理

大纲 black_tophatwhite_tophatconvolvecorrelategaussian_filtergaussian_laplacemaximum_filtermedian_filterminimum_filterpercentile_filterprewittrank_filtersobelspline_filteruniform_filter基础代码代码仓库 在《使用numpy处理图片——模糊处理》一文中&#xff0c;我…

【python】py-spy 实时显示python进程内的线程堆栈CPU消耗 python CPU消耗分析

安装 pip install py-spy AI调用源码&#xff0c;红色调用时&#xff0c;python进程CPU 100% 启动程序&#xff0c;输入问题&#xff0c;观察CPU top sudo .local/bin/py-spy top --pid 7150 可以看到&#xff0c;此时与显卡交互占用了绝大部分CPU&#xff0c;有点死循环检测…