Android中的SPI实现

news2024/11/17 18:26:02

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python入门,数据容器:字典dict

字典作用就和它的名字一样&#xff0c;我们可以通过某个关键字找到它对应的信息&#xff0c;或者讲的高级一点&#xff0c;就是key与value的对应关系 举例&#xff1a; 一场考试小明考了80分&#xff0c;小红考了90分&#xff0c;小东考了95分&#xff0c;在字典里&#xff0…

使用JDK自带的jvisualvm工具查看堆dump文件【回顾】

JDK自带的jvisualvm的使用 打开方式&#xff1a; 直接命令行输入&#xff1a;jvisualvm ,然后回车​​​​​​​ ​​ 或者去jdk的bin目录下找到打开 安装visual GC插件 检测死锁 再点击“死锁 dump”就可以看到死锁的线程信息了&#xff1b;

C++每日一练(16):数组逆序

题目描述 给你m个整数&#xff0c;将其逆序输出 输入 第一行一个整数m&#xff08;3 < m < 100 )&#xff1a;数的个数 第二行m个整数&#xff08;空格隔开&#xff09;&#xff08;这些数在0-9999999之间) 输出 m个整数&#xff08;空格隔开&#xff09; 输入样例 3 1 7…

SV-7041T 30W网络有源音箱校园教室广播音箱,商场广播音箱,会议广播音箱,酒店广播音箱,工厂办公室广播音箱

SV-7041T 30W网络有源音箱 校园教室广播音箱&#xff0c;商场广播音箱&#xff0c;会议广播音箱&#xff0c;酒店广播音箱&#xff0c;工厂办公室广播音箱 SV-7041T是深圳锐科达电子有限公司的一款2.0声道壁挂式网络有源音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将…

GZ075 云计算应用赛题第9套

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷9 某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenSt…

【已解决】fatal: Authentication failed for ‘https://github.com/.../‘

文章目录 异常原因解决方法 异常原因 在 Linux 服务器上使用git push命令&#xff0c;输入用户名和密码之后&#xff0c;总会显示一个报错&#xff1a; fatal: Authentication failed for https://github.com/TianJiaQi-Code/Linux.git/ # 致命&#xff1a;无法通过验证访问起…

CAN总线记录仪在车企服务站的应用

CAN总线记录仪在车企服务站的应用 CAN总线记录仪在车企服务站中有着广泛的应用。这种设备可以记录车上的CAN总线数据&#xff0c;方便工程师进行分析&#xff0c;以找出可能存在的问题。CAN记录仪一般采用TF卡来存储数据&#xff0c;实现离线脱机实时存储。数据存储完毕后&…

Shiro框架:Shiro登录认证流程源码解析

目录 1.用户登录认证流程 1.1 生成认证Token 1.2 用户登录认证 1.2.1 SecurityManager login流程解析 1.2.1.1 authenticate方法进行登录认证 1.2.1.1.1 单Realm认证 1.2.1.2 认证通过后创建登录用户对象 1.2.1.2.1 复制SubjectContext 1.2.1.2.2 对subjectContext设…

《向量数据库指南》RAG 应用中的指代消解——解决方案初探

随着 ChatGPT 等大语言模型(LLM)的不断发展&#xff0c;越来越多的研究人员开始关注语言模型的应用。 其中&#xff0c;检索增强生成&#xff08;Retrieval-augmented generation&#xff0c;RAG&#xff09;是一种针对知识密集型 NLP 任务的生成方法&#xff0c;它通过在生成过…

嵌入式学习-网络编程-Day1

Day1 思维导图 作业 实现一下套接字通信 代码 #include<myhead.h>int main(int argc, const char *argv[]) {//1、创建套接字int sfd socket(AF_INET, SOCK_STREAM, 0);//参数1&#xff1a;通信域&#xff1a;使用的是ipv4通信//参数2&#xff1a;表示使用tcp通信//参…

Python轴承故障诊断 (11)基于VMD+CNN-BiGRU-Attenion的故障分类

目录 往期精彩内容&#xff1a; 前言 模型整体结构 1 变分模态分解VMD的Python示例 2 轴承故障数据的预处理 2.1 导入数据 2.2 故障VMD分解可视化 2.3 故障数据的VMD分解预处理 3 基于VMD-CNN-BiGRU-Attenion的轴承故障诊断分类 3.1 定义VMD-CNN-BiGRU-Attenion分类网…

批评与自我批评组织生活会发言材料2024年六个方面

生活就像一场马拉松&#xff0c;成功需要坚持不懈的奔跑。每一步都可能会遇到挫折和困难&#xff0c;但只要你努力向前&#xff0c;坚持不放弃&#xff0c;你就一定能够迎接胜利的喜悦。不要害怕失败&#xff0c;因为失败是成功的垫脚石。相信自己的能力&#xff0c;追求自己的…

机器学习 | 卷积神经网络

机器学习 | 卷积神经网络 实验目的 采用任意一种课程中介绍过的或者其它卷积神经网络模型&#xff08;例如LeNet-5、AlexNet等&#xff09;用于解决某种媒体类型的模式识别问题。 实验内容 卷积神经网络可以基于现有框架如TensorFlow、Pytorch或者Mindspore等构建&#xff…

青阳龙野网络文件传输系统Docker版

青阳龙野网络文件传输系统Docker版 基于底包debian:bookworm-slim制作 一键拉取命令如下&#xff1a; docker run -idt \ -p 8080:8080 \ -v /data:/kiftd-1.1.1-release/filesystem \ -v /kiftd/conf:/kiftd-1.1.1-release/conf \ -e TZAsia/Shanghai \ --privilegedtrue \…

PyTorch深度学习实战(30)——Deepfakes

PyTorch深度学习实战&#xff08;30&#xff09;——Deepfakes 0. 前言1. Deepfakes 原理2. 数据集分析3. 使用 PyTorch 实现 Deepfakes3.1 random_warp.py3.2 Deepfakes.py 小结系列链接 0. 前言 Deepfakes 是一种利用深度学习技术生成伪造视频和图像的技术。它通过将一个人的…

css 怎么绘制一个带圆角的渐变色的边框

1&#xff0c;可以写两个样式最外面的div设置一个渐变的背景色。里面的元素使用纯色。但是宽高要比外面元素的小。可以利用里面的元素设置padding这样挡住部分渐变色。漏出来的渐变色就像边框一样。 <div class"cover-wrapper"> <div class"item-cover…

Spark高级特性 (难)

Spark高级特性 (难) 闭包 /** 编写一个高阶函数&#xff0c;在这个函数要有一个变量&#xff0c;返回一个函数&#xff0c;通过这个变量完成一个计算* */Testdef test(): Unit { // val f: Int > Double closure() // val area f(5) // println(area)// 在这能否…

[易语言]易语言调用C++ DLL回调函数

易语言适合用于数据展示&#xff0c;数据的获取还是VC来的快、方便哈。 因此我一般使用VC编写DLL&#xff0c;使用易语言编写界面&#xff0c;同一个程序&#xff0c;DLL和EXE通讯最方便的就是使用接口回调了。 废话少说&#xff0c;进入主题。 1. VC编写DLL 为了DLL能够调…

精品量化公式——“风险指数”,适用于短线操作的交易系统,股票期货都适用!不漂移

不多说&#xff0c;直接上效果如图&#xff1a; ► 日线表现 代码评估 技术指标代码评估&#xff1a; 用于通过各种技术指标来分析股市走势。它使用了多个自定义变量&#xff08;VAR1, VAR2, VAR3, 等等&#xff09;&#xff0c;并且基于这些变量构建了复杂的条件和计算。以下…

PostgreSQL之SEMI-JOIN半连接

什么是Semi-Join半连接 Semi-Join半连接&#xff0c;当外表在内表中找到匹配的记录之后&#xff0c;Semi-Join会返回外表中的记录。但即使在内表中找到多条匹配的记录&#xff0c;外表也只会返回已经存在于外表中的记录。而对于子查询&#xff0c;外表的每个符合条件的元组都要…