├── .gitignore ├── README.md ├── SharedLib ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── softtanck │ │ └── sharedlib │ │ └── ExampleInstrumentedTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── softtanck │ │ └── ramessage │ │ └── IRaMessenger.aidl │ └── java │ └── com │ └── softtanck │ ├── IRaMessageInterface.kt │ ├── RaConstant.kt │ ├── RaNotification.kt │ ├── model │ ├── RaClient.kt │ ├── RaCustomMessenger.java │ ├── RaRequestTypeArg.kt │ └── RaRequestTypeParameter.kt │ ├── ramessageclient │ ├── RaClientApi.kt │ └── core │ │ ├── BaseServiceConnection.kt │ │ ├── RaServiceConnector.kt │ │ ├── engine │ │ ├── BaseClientHandler.kt │ │ ├── RaClientHandler.kt │ │ └── retrofit │ │ │ ├── KotlinExtensions.kt │ │ │ ├── Platform.java │ │ │ ├── RaRetrofit.kt │ │ │ ├── RemoteServiceMethod.java │ │ │ ├── RequestFactory.java │ │ │ └── ServiceMethod.java │ │ ├── listener │ │ ├── BindStatusChangedListener.kt │ │ ├── ClientListenerManager.kt │ │ ├── DisconnectedReason.kt │ │ └── RaRemoteMessageListener.kt │ │ ├── model │ │ └── RaClientBindStatus.kt │ │ └── util │ │ ├── LockHelper.kt │ │ └── ResponseHandler.kt │ ├── ramessageservice │ ├── BaseConnectionService.kt │ ├── RaServerApi.kt │ ├── engine │ │ ├── BaseServerSyncHandler.kt │ │ ├── RaClientManager.kt │ │ └── RaServerHandler.kt │ ├── intercept │ │ ├── IRaResponseIntercept.kt │ │ ├── RaDefaultInterceptImpl.kt │ │ ├── RaUnbindInterceptImpl.kt │ │ └── RealInterceptorChain.kt │ ├── model │ │ ├── RaChain.kt │ │ └── RaRemoteMethod.kt │ └── util │ │ └── ServerUtil.kt │ └── util │ └── Utils.java ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── softtanck │ │ └── ramessage │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ ├── shared │ │ │ └── model │ │ │ │ └── Food.kt │ │ │ └── softtanck │ │ │ └── ramessage │ │ │ ├── MainActivity.kt │ │ │ └── ipc │ │ │ └── RaTestInterface.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── softtanck │ └── ramessage │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── ramessageservice ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── softtanck │ │ └── ramessageservice │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ ├── shared │ │ │ └── model │ │ │ │ └── Food.kt │ │ │ └── softtanck │ │ │ └── ramessageservice │ │ │ ├── MainActivity.kt │ │ │ ├── MyServerTestFunImpl.kt │ │ │ ├── MyServerTestFunImplV2.kt │ │ │ ├── RaConnectionService.kt │ │ │ ├── RaConnectionServiceV2.kt │ │ │ └── ipc │ │ │ └── RaTestInterface.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── softtanck │ └── ramessageservice │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea/compiler.xml 17 | /.idea/deploymentTargetDropDown.xml 18 | /.idea/gradle.xml 19 | /.idea/kotlinc.xml 20 | /.idea/migrations.xml 21 | /.idea/misc.xml 22 | /.idea/vcs.xml 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥🔥🔥A lightweight cross-process communication component on Android, Kotlin and Coroutines both supported. 2 | 3 | A lightweight cross-process communication component on Android。(All versions are supported,only 98kb)![RUNOOB 图标](https://jitpack.io/v/Softtanck/RAMessage.svg) 4 | 5 | | Type | Supported | 6 | | :----------------------: | :-------: | 7 | | All versions for Android | ✅ | 8 | | Kotlin | ✅ | 9 | | Sync call | ✅ | 10 | | Async Call | ✅ | 11 | | Coroutines | ✅ | 12 | | Thread-Safe | ✅ | 13 | | Many(Client)-To-Many(Service) | ✅ | 14 | | Method's Parameters:1、Basic type;2、Object which is implemented Parcelable;3、`List`;4、`List`;5、`List`;6、`List` | ✅ | 15 | | Broadcast message | ✅ | 16 | | Proguard | ✅ | 17 | 18 | ## How to use 19 | 20 | ```kotlin 21 | implementation 'com.github.Softtanck:RAMessage:2.0.0' 22 | ``` 23 | 24 | ### Client 25 | 26 | 1. Defined the interface at client side; 27 | 28 | ```kotlin 29 | interface RaTestInterface : IRaMessageInterface { 30 | fun getAFood(): Food? 31 | fun getAFoodWithParameter(foodName: String): Food? 32 | fun getAllFoods(): List? 33 | fun eatFood() 34 | fun buyFood(): Boolean 35 | fun getFoodName(): String 36 | fun setFoodName(foodName: String): String 37 | 38 | suspend fun suspendBuyFood(): Boolean? 39 | suspend fun suspendGetFood(): Food? 40 | } 41 | ``` 42 | 43 | 2. Use method of `RaClientApi.INSTANCE.create(RaTestInterface::class.java)` after bind is successful; 44 | 45 | #### Client simple 46 | 47 | ```kotlin 48 | RaClientApi.INSTANCE.bindRaConnectionService(this, componentName, object : BindStatusChangedListener { 49 | override fun onConnectedToRaServices(componentName: ComponentName) { 50 | Log.d(TAG, "connectedToRaServices: $this-$componentName") 51 | val testInterface = RaClientApi.INSTANCE.create(componentName = componentName, service = RaTestInterface::class.java) 52 | // 1. Get a food from other process 53 | var remoteFood: Food? = testInterface.getAFood() 54 | Log.d(TAG, "getAFood result: $remoteFood") 55 | if (remoteFood?.name != "Apple") { 56 | throw IllegalStateException("Get a food from other process failed") 57 | } 58 | 59 | // 2. Get a food with parameter 60 | remoteFood = testInterface.getAFoodWithParameter("Banana") 61 | Log.d(TAG, "getAFoodWithParameter: $remoteFood") 62 | 63 | // 3. Get all foods 64 | val allFoods = testInterface.getAllFoods() 65 | Log.d(TAG, "getAllFoods: $allFoods, ${allFoods?.size}") 66 | 67 | // 4. Eat food 68 | testInterface.eatFood() 69 | 70 | // 5. Buy a food 71 | val buyFoodResult = testInterface.buyFood() 72 | Log.d(TAG, "buyFood: $buyFoodResult") 73 | 74 | // 6. Get a food name 75 | val foodName = testInterface.getFoodName() 76 | Log.d(TAG, "getFoodName: $foodName") 77 | 78 | // 7. Set food name 79 | val changedFoodName = testInterface.setFoodName("Pear") 80 | Log.d(TAG, "setFoodName: $changedFoodName") 81 | 82 | // 8. Suspend 83 | lifecycleScope.launch(Dispatchers.IO) { 84 | 85 | // 8.1 buy food 86 | val suspendBuyFoodResult = testInterface.suspendBuyFood() 87 | Log.d(TAG, "suspendBuyFood: $suspendBuyFoodResult") 88 | 89 | // 8.2 get food 90 | val suspendGetFood = testInterface.suspendGetFood() 91 | Log.d(TAG, "suspendGetFood: $suspendGetFood") 92 | } 93 | } 94 | 95 | override fun onConnectRaServicesFailed(componentName: ComponentName) { 96 | Log.d(TAG, "onConnectRaServicesFailed: $componentName") 97 | } 98 | 99 | override fun onDisconnectedFromRaServices(componentName: ComponentName, @DisconnectedReason disconnectedReason: Int) { 100 | Log.d(TAG, "disconnectedFromRaServices: $disconnectedReason-$componentName") 101 | } 102 | }) 103 | ``` 104 | 105 | ### Service 106 | 107 | 1. extend `BaseConnectionService` 108 | 2. Implement `RaTestInterface` 109 | 110 | #### Service sample 111 | 112 | ```kotlin 113 | interface MyServerTestFunImpl : RaTestInterface { 114 | 115 | override fun getAFood(): Food? { 116 | Log.d(TAG, "[SERVER] getAFood: Service is invoked") 117 | return testFood 118 | } 119 | 120 | override fun getAFoodWithParameter(foodName: String): Food? { 121 | Log.d(TAG, "[SERVER] getAFoodWithParameter: Service is invoked, foodName:$foodName") 122 | return testFood.apply { 123 | name = foodName 124 | } 125 | } 126 | 127 | override fun getAllFoods(): List? { 128 | Log.d(TAG, "[SERVER] getAllFoods") 129 | return mutableListOf().apply { 130 | repeat(10) { 131 | add(testFood) 132 | } 133 | } 134 | } 135 | 136 | override fun eatFood() { 137 | Log.d(TAG, "[SERVER] eatFood") 138 | } 139 | 140 | override fun buyFood(): Boolean { 141 | Log.d(TAG, "[SERVER] buyFood") 142 | return true 143 | } 144 | 145 | override fun getFoodName(): String { 146 | Log.d(TAG, "[SERVER] getFoodName") 147 | return testFood.name 148 | } 149 | 150 | override fun setFoodName(foodName: String): String { 151 | Log.d(TAG, "[SERVER] setFoodName: $foodName") 152 | return testFood.name 153 | } 154 | 155 | override suspend fun suspendBuyFood(): Boolean { 156 | Log.d(TAG, "[SERVER] suspendBuyFood") 157 | return true 158 | } 159 | 160 | override suspend fun suspendGetFood(): Food { 161 | Log.d(TAG, "[SERVER] suspendGetFood") 162 | return testFood 163 | } 164 | } 165 | ``` 166 | 167 | # Proguard 168 | 169 | -keep class * extends com.softtanck.IRaMessageInterface { *;} 170 | -keep interface * extends com.softtanck.IRaMessageInterface { *;} 171 | -keep class com.softtanck.ramessageclient.core.engine.retrofit.RemoteServiceMethod { *; } 172 | -keep class com.softtanck.ramessageservice.** { *; } 173 | 174 | # 🔥🔥🔥一个高性能且线程安全的IPC通信框架,支持Java、Kotlin以及同步调用、异步调用、协程 175 | 176 | 一个高性能且线程安全的IPC通信框架。(Android全平台支持,仅98kb)![RUNOOB 图标](https://jitpack.io/v/Softtanck/RAMessage.svg) 177 | 178 | | Type | Supported | 179 | | :----------------------: | :-------: | 180 | | Android所有版本 | ✅ | 181 | | Kotlin | ✅ | 182 | | 同步调用 | ✅ | 183 | | 异步调用 | ✅ | 184 | | 协程 | ✅ | 185 | | 线程安全 | ✅ | 186 | | 多个客户端<->多个服务端 | ✅ | 187 | |支持接口参数、返回参数为:1、基本类型;2、实现了Parcelable的对象;3、```List```;4、```List```;5、```List```;6、```List```| ✅ | 188 | | 广播消息 | ✅ | 189 | | 混淆 | ✅ | 190 | 191 | ## 如何使用 192 | 193 | ```kotlin 194 | implementation 'com.github.Softtanck:RAMessage:2.0.0' 195 | ``` 196 | 197 | ### 客户端 198 | 199 | 1. 先在客户端定义想要IPC的接口; 200 | 201 | ```kotlin 202 | interface RaTestInterface : IRaMessageInterface { 203 | fun getAFood(): Food? 204 | fun getAFoodWithParameter(foodName: String): Food? 205 | fun getAllFoods(): List? 206 | fun eatFood() 207 | fun buyFood(): Boolean 208 | fun getFoodName(): String 209 | fun setFoodName(foodName: String): String 210 | 211 | suspend fun suspendBuyFood(): Boolean? 212 | suspend fun suspendGetFood(): Food? 213 | } 214 | ``` 215 | 216 | 2. 在客户端绑定远程服务成功后,通过 `RaClientApi.INSTANCE.create(ComponentName, RaTestInterface::class.java)`方法即可获得对应服务,然后调用对应接口即可; 217 | 218 | #### 客户端示例 219 | 220 | ```kotlin 221 | // 1. 提供被绑定的远程服务器名字;2. 在绑定成功后,调用远程服务即可; 222 | RaClientApi.INSTANCE.bindRaConnectionService(this, componentName, object : BindStatusChangedListener { 223 | override fun onConnectedToRaServices(componentName: ComponentName) { 224 | Log.d(TAG, "connectedToRaServices: $this-$componentName") 225 | val testInterface = RaClientApi.INSTANCE.create(componentName = componentName, service = RaTestInterface::class.java) 226 | // 1. Get a food from other process 227 | var remoteFood: Food? = testInterface.getAFood() 228 | Log.d(TAG, "getAFood result: $remoteFood") 229 | if (remoteFood?.name != "Apple") { 230 | throw IllegalStateException("Get a food from other process failed") 231 | } 232 | 233 | // 2. Get a food with parameter 234 | remoteFood = testInterface.getAFoodWithParameter("Banana") 235 | Log.d(TAG, "getAFoodWithParameter: $remoteFood") 236 | 237 | // 3. Get all foods 238 | val allFoods = testInterface.getAllFoods() 239 | Log.d(TAG, "getAllFoods: $allFoods, ${allFoods?.size}") 240 | 241 | // 4. Eat food 242 | testInterface.eatFood() 243 | 244 | // 5. Buy a food 245 | val buyFoodResult = testInterface.buyFood() 246 | Log.d(TAG, "buyFood: $buyFoodResult") 247 | 248 | // 6. Get a food name 249 | val foodName = testInterface.getFoodName() 250 | Log.d(TAG, "getFoodName: $foodName") 251 | 252 | // 7. Set food name 253 | val changedFoodName = testInterface.setFoodName("Pear") 254 | Log.d(TAG, "setFoodName: $changedFoodName") 255 | 256 | // 8. Suspend 257 | lifecycleScope.launch(Dispatchers.IO) { 258 | 259 | // 8.1 buy food 260 | val suspendBuyFoodResult = testInterface.suspendBuyFood() 261 | Log.d(TAG, "suspendBuyFood: $suspendBuyFoodResult") 262 | 263 | // 8.2 get food 264 | val suspendGetFood = testInterface.suspendGetFood() 265 | Log.d(TAG, "suspendGetFood: $suspendGetFood") 266 | } 267 | } 268 | 269 | override fun onConnectRaServicesFailed(componentName: ComponentName) { 270 | Log.d(TAG, "onConnectRaServicesFailed: $componentName") 271 | } 272 | 273 | override fun onDisconnectedFromRaServices(componentName: ComponentName, @DisconnectedReason disconnectedReason: Int) { 274 | Log.d(TAG, "disconnectedFromRaServices: $disconnectedReason-$componentName") 275 | } 276 | }) 277 | ``` 278 | ### 服务端 279 | 1. 继承```BaseConnectionService``` 280 | 2. 实现```RaTestInterface```接口 281 | #### 服务端示例 282 | ```kotlin 283 | interface MyServerTestFunImpl : RaTestInterface { 284 | 285 | override fun getAFood(): Food? { 286 | Log.d(TAG, "[SERVER] getAFood: Service is invoked") 287 | return testFood 288 | } 289 | 290 | override fun getAFoodWithParameter(foodName: String): Food? { 291 | Log.d(TAG, "[SERVER] getAFoodWithParameter: Service is invoked, foodName:$foodName") 292 | return testFood.apply { 293 | name = foodName 294 | } 295 | } 296 | 297 | override fun getAllFoods(): List? { 298 | Log.d(TAG, "[SERVER] getAllFoods") 299 | return mutableListOf().apply { 300 | repeat(10) { 301 | add(testFood) 302 | } 303 | } 304 | } 305 | 306 | override fun eatFood() { 307 | Log.d(TAG, "[SERVER] eatFood") 308 | } 309 | 310 | override fun buyFood(): Boolean { 311 | Log.d(TAG, "[SERVER] buyFood") 312 | return true 313 | } 314 | 315 | override fun getFoodName(): String { 316 | Log.d(TAG, "[SERVER] getFoodName") 317 | return testFood.name 318 | } 319 | 320 | override fun setFoodName(foodName: String): String { 321 | Log.d(TAG, "[SERVER] setFoodName: $foodName") 322 | return testFood.name 323 | } 324 | 325 | override suspend fun suspendBuyFood(): Boolean { 326 | Log.d(TAG, "[SERVER] suspendBuyFood") 327 | return true 328 | } 329 | 330 | override suspend fun suspendGetFood(): Food { 331 | Log.d(TAG, "[SERVER] suspendGetFood") 332 | return testFood 333 | } 334 | } 335 | ``` 336 | ## 一些说明 337 | - 推荐使用协程的方式调用; 338 | - 自定义的参数中的对象或函数返回值对象不能被混淆; 339 | - 自定义的参数是对象时,该对象必须实现Parcelable接口; 340 | - 接口带有返回值是「同步」调用,不带返回值是「异步」调用; 341 | - 如果项目支持 协程,无论是否带返回值的接口都支持「异步」调用; 342 | - 当接口带有返回值时,调用方需要考虑调用同步方法的时候的线程防止出现ANR(协程不需要考虑); 343 | - 接口如果有返回值,但是如果远程调用失败,返回值为空,请注意「**空指针**」异常; 344 | 345 | # 混淆 346 | ``` 347 | -keep class * extends com.softtanck.IRaMessageInterface { *;} 348 | -keep interface * extends com.softtanck.IRaMessageInterface { *;} 349 | -keep class com.softtanck.ramessageclient.core.engine.retrofit.RemoteServiceMethod { *; } 350 | -keep class com.softtanck.ramessageservice.** { *; } 351 | ``` 352 | # Licence 353 | ``` 354 | Copyright 2023 Softtanck. 355 | Licensed under the Apache License, Version 2.0 (the "License"); 356 | you may not use this file except in compliance with the License. 357 | You may obtain a copy of the License at 358 | 359 | http://www.apache.org/licenses/LICENSE-2.0 360 | 361 | Unless required by applicable law or agreed to in writing, software 362 | distributed under the License is distributed on an "AS IS" BASIS, 363 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 364 | See the License for the specific language governing permissions and 365 | limitations under the License. 366 | ``` 367 | -------------------------------------------------------------------------------- /SharedLib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /SharedLib/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-parcelize' 4 | id 'kotlin-android' 5 | id 'maven-publish' 6 | } 7 | 8 | android { 9 | defaultConfig { 10 | minSdkVersion 23 11 | compileSdk 34 12 | buildToolsVersion = "34.0.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | buildFeatures { 25 | aidl true 26 | } 27 | namespace 'com.softtanck.sharedlib' 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_17 31 | targetCompatibility JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = '17' 35 | } 36 | } 37 | 38 | afterEvaluate { 39 | publishing { 40 | publications { 41 | // Creates a Maven publication called "release". 42 | release(MavenPublication) { 43 | from components.release 44 | groupId = 'com.softtanck.ramessage' 45 | artifactId = 'RaMessage' 46 | version = '2.0.0' 47 | } 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | compileOnly 'androidx.core:core-ktx:1.12.0' 54 | compileOnly "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" 55 | // androidx.test 56 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 57 | } -------------------------------------------------------------------------------- /SharedLib/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes Signature, InnerClasses, EnclosingMethod 2 | -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations 3 | -keepattributes AnnotationDefault 4 | 5 | # Ignore JSR 305 annotations for embedding nullability information. 6 | -dontwarn javax.annotation.** 7 | 8 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 9 | -dontwarn kotlin.Unit 10 | 11 | # With R8 full mode generic signatures are stripped for classes that are not 12 | # kept. Suspend functions are wrapped in continuations where the type argument 13 | # is used. 14 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 15 | -keep class * extends com.softtanck.IRaMessageInterface { *;} 16 | -keep interface * extends com.softtanck.IRaMessageInterface { *;} 17 | -keep class com.softtanck.ramessageclient.core.engine.retrofit.RemoteServiceMethod { *; } 18 | -keep class com.softtanck.ramessageservice.** { *; } -------------------------------------------------------------------------------- /SharedLib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /SharedLib/src/androidTest/java/com/softtanck/sharedlib/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.sharedlib 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.softtanck.sharedlib.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /SharedLib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SharedLib/src/main/aidl/com/softtanck/ramessage/IRaMessenger.aidl: -------------------------------------------------------------------------------- 1 | // IRaMessenger.aidl 2 | package com.softtanck.ramessage; 3 | // Once named, the function name cannot be changed, but the function can be added. 4 | // This file is a typical AIDL. 5 | interface IRaMessenger { 6 | // The methods of the original Handler. Note: this is oneway!!! 7 | oneway void send(in Message msg); 8 | // Sync call from handler 9 | Message sendSync(in Message msg); 10 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/IRaMessageInterface.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck 2 | 3 | /** 4 | * A common interface for all the classes that need to be notified when the 5 | * @author Softtanck 6 | * @date 2022/7/15 7 | * Description: TODO 8 | */ 9 | abstract interface IRaMessageInterface -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/RaConstant.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck 2 | 3 | /** 4 | * @author Softtanck 5 | * @date 2022/3/12 6 | * Description: TODO 7 | */ 8 | internal const val MESSAGE_BASE_KEY = 1 9 | 10 | internal const val MESSAGE_REGISTER_CLIENT_REQ = MESSAGE_BASE_KEY + 1 11 | internal const val MESSAGE_REGISTER_CLIENT_RSP = MESSAGE_BASE_KEY + 2 12 | internal const val MESSAGE_CLIENT_DISCONNECT_REQ = MESSAGE_BASE_KEY + 3 13 | 14 | internal const val MESSAGE_CLIENT_SINGLE_REQ = MESSAGE_BASE_KEY + 4 15 | internal const val MESSAGE_CLIENT_SINGLE_RSP = MESSAGE_BASE_KEY + 5 16 | 17 | /** 18 | * Sent from the server to the client to indicate the something changed. 19 | */ 20 | internal const val MESSAGE_CLIENT_BROADCAST_RSP = MESSAGE_BASE_KEY + 6 21 | 22 | internal const val MESSAGE_BUNDLE_REPLY_TO_KEY = "message_bundle_reply_to_key" 23 | internal const val MESSAGE_BUNDLE_METHOD_NAME_KEY = "message_bundle_method_name_key" 24 | internal const val MESSAGE_BUNDLE_NORMAL_RSP_KEY = "message_bundle_rsp_key" 25 | internal const val MESSAGE_BUNDLE_RSP_TYPE_KEY = "message_bundle_rsp_type_key" 26 | internal const val MESSAGE_BUNDLE_TYPE_ARG_KEY = "message_bundle_type_arg_key" 27 | internal const val MESSAGE_BUNDLE_TYPE_PARAMETER_KEY = "message_bundle_type_parameter_key" 28 | 29 | 30 | internal const val DEFAULT_BUNDLE_TYPE = 0 31 | internal const val MESSAGE_BUNDLE_PARCELABLE_TYPE = DEFAULT_BUNDLE_TYPE 32 | internal const val MESSAGE_BUNDLE_ARRAYLIST_STRING_TYPE = DEFAULT_BUNDLE_TYPE + 1 33 | internal const val MESSAGE_BUNDLE_ARRAYLIST_CHAR_SEQUENCE_TYPE = DEFAULT_BUNDLE_TYPE + 2 34 | internal const val MESSAGE_BUNDLE_ARRAYLIST_INTEGER_TYPE = DEFAULT_BUNDLE_TYPE + 3 35 | internal const val MESSAGE_BUNDLE_ARRAYLIST_PARCELABLE_TYPE = DEFAULT_BUNDLE_TYPE + 4 36 | internal const val MESSAGE_BUNDLE_BOOLEAN_TYPE = DEFAULT_BUNDLE_TYPE + 5 37 | internal const val MESSAGE_BUNDLE_CHAR_TYPE = DEFAULT_BUNDLE_TYPE + 6 38 | internal const val MESSAGE_BUNDLE_STRING_TYPE = DEFAULT_BUNDLE_TYPE + 7 39 | internal const val MESSAGE_BUNDLE_BYTE_TYPE = DEFAULT_BUNDLE_TYPE + 8 40 | 41 | 42 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/RaNotification.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck 2 | 3 | import android.R 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.content.Context 8 | import android.graphics.Color 9 | import android.os.Build 10 | import androidx.core.app.NotificationCompat 11 | 12 | /** 13 | * @author Softtanck 14 | * @date 2022/3/12 15 | * Description: TODO 16 | */ 17 | internal object RaNotification { 18 | 19 | private const val NOTIFICATION_CHANNEL_ID = "com.softtanck.ramessageservice.foreground.service.id" 20 | private const val NOTIFICATION_CHANNEL_NAME = "com.softtanck.ramessageservice.foreground.service" 21 | 22 | const val BASE_CONNECTION_SERVICE_NOTIFICATION_ID = 1 23 | 24 | fun getNotificationForInitSetup(context: Context): Notification { 25 | val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 26 | val notificationChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE) 27 | notificationChannel.lightColor = Color.BLUE 28 | notificationChannel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE 29 | val service = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 30 | service.createNotificationChannel(notificationChannel) 31 | NOTIFICATION_CHANNEL_ID 32 | } else { 33 | NOTIFICATION_CHANNEL_ID 34 | } 35 | val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, channelId) 36 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 37 | builder.priority = NotificationManager.IMPORTANCE_HIGH 38 | } 39 | return builder.setSmallIcon(R.drawable.ic_dialog_info) 40 | .setColor(-0xf05a35) 41 | .setContentTitle("RA Connection Service is working...") 42 | .build() 43 | } 44 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/model/RaClient.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.model 2 | 3 | import android.os.Message 4 | import android.os.Messenger 5 | import android.os.Parcelable 6 | import android.util.Log 7 | 8 | /** 9 | * Created by Softtanck on 2022/3/12 10 | * a client of RaMessageService 11 | * @param clientUID client's uid 12 | * @param clientMessenger client's messenger 13 | * @param serviceKey service's key, used to identify the service 14 | */ 15 | internal data class RaClient(val clientUID: Int, val clientMessenger: T, val serviceKey: String) { 16 | 17 | private val TAG = this.javaClass.name 18 | 19 | fun getClientBinder() = 20 | if (clientMessenger is RaCustomMessenger) { 21 | clientMessenger.binder 22 | } else { 23 | (clientMessenger as Messenger).binder 24 | } 25 | 26 | fun sendAsyncMessageToClient(message: Message) { 27 | when (clientMessenger) { 28 | is RaCustomMessenger -> { 29 | clientMessenger.send(message) 30 | } 31 | 32 | is Messenger -> { 33 | clientMessenger.send(message) 34 | } 35 | 36 | else -> { 37 | Log.e(TAG, "[SERVER] sendConnectionRegisterStateToClient: Unknown client messenger type") 38 | throw IllegalArgumentException("Unknown client messenger type") 39 | } 40 | } 41 | } 42 | 43 | fun sendSyncMessageToClient(message: Message): Message? = 44 | when (clientMessenger) { 45 | is RaCustomMessenger -> { 46 | clientMessenger.sendSync(message) 47 | } 48 | 49 | else -> { 50 | Log.e(TAG, "[SERVER] sendConnectionRegisterStateToClient: Unknown client messenger type, Dropped message:$message") 51 | null 52 | } 53 | } 54 | 55 | override fun toString(): String { 56 | return "RaClient(clientPID=$clientUID, clientMessenger=$clientMessenger)" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/model/RaCustomMessenger.java: -------------------------------------------------------------------------------- 1 | package com.softtanck.model; 2 | 3 | import android.os.IBinder; 4 | import android.os.Message; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | import android.os.RemoteException; 8 | import android.util.Pair; 9 | 10 | import androidx.annotation.Keep; 11 | import androidx.annotation.Nullable; 12 | 13 | import com.softtanck.ramessage.IRaMessenger; 14 | import com.softtanck.ramessageclient.core.engine.RaClientHandler; 15 | 16 | /** 17 | * Created by Softtanck on 2022/3/12 18 | * Copied from Android's SDK. ref: Messenger.java 19 | */ 20 | @Keep 21 | public class RaCustomMessenger implements Parcelable { 22 | /** 23 | * For upgrading logic. And the default version is 1. 24 | * NOTE: This code needs to updated if the version of client changed. 25 | */ 26 | public static final int RA_CLIENT_MESSENGER_VERSION = 1; 27 | public static final String RA_CLIENT_MESSENGER_KEY = "ra_client_messenger_key"; 28 | public static final Pair raMsgVersion = new Pair<>(RA_CLIENT_MESSENGER_KEY, RA_CLIENT_MESSENGER_VERSION); 29 | private final IRaMessenger mTarget; 30 | 31 | public RaCustomMessenger(RaClientHandler target) { 32 | mTarget = target.getInnerMessenger(); 33 | } 34 | 35 | /** 36 | * Send a Message to this Messenger's Handler. 37 | * 38 | * @param message The Message to send. Usually retrieved through 39 | * {@link Message#obtain() Message.obtain()}. 40 | * @throws RemoteException Throws DeadObjectException if the target 41 | * Handler no longer exists. 42 | */ 43 | public void send(Message message) throws RemoteException { 44 | mTarget.send(message); 45 | } 46 | 47 | /** 48 | * A method which can be used to send a message in sync. 49 | * 50 | * @param message The Message to send. Usually retrieved through 51 | * {@link Message#obtain() Message.obtain()}. 52 | * @return the result from service 53 | * @throws RemoteException Throws DeadObjectException if the target 54 | * Handler no longer exists. 55 | */ 56 | public Message sendSync(Message message) throws RemoteException { 57 | return mTarget.sendSync(message); 58 | } 59 | 60 | /** 61 | * Retrieve the IBinder that this Messenger is using to communicate with 62 | * its associated Handler. 63 | * 64 | * @return Returns the IBinder backing this Messenger. 65 | */ 66 | @Nullable 67 | public IBinder getBinder() { 68 | return mTarget == null ? null : mTarget.asBinder(); 69 | } 70 | 71 | 72 | /** 73 | * Comparison operator on two Messenger objects, such that true 74 | * is returned then they both point to the same Handler. 75 | */ 76 | public boolean equals(Object otherObj) { 77 | if (otherObj == null) { 78 | return false; 79 | } 80 | try { 81 | return mTarget.asBinder().equals(((RaCustomMessenger) otherObj) 82 | .mTarget.asBinder()); 83 | } catch (ClassCastException e) { 84 | } 85 | return false; 86 | } 87 | 88 | public int hashCode() { 89 | return mTarget.asBinder().hashCode(); 90 | } 91 | 92 | public int describeContents() { 93 | return 0; 94 | } 95 | 96 | public void writeToParcel(Parcel out, int flags) { 97 | out.writeStrongBinder(mTarget.asBinder()); 98 | } 99 | 100 | public static final Creator CREATOR = new Creator() { 101 | public RaCustomMessenger createFromParcel(Parcel in) { 102 | IBinder target = in.readStrongBinder(); 103 | return target != null ? new RaCustomMessenger(target) : null; 104 | } 105 | 106 | public RaCustomMessenger[] newArray(int size) { 107 | return new RaCustomMessenger[size]; 108 | } 109 | }; 110 | 111 | /** 112 | * Convenience function for writing either a Messenger or null pointer to 113 | * a Parcel. You must use this with {@link #readMessengerOrNullFromParcel} 114 | * for later reading it. 115 | * 116 | * @param messenger The Messenger to write, or null. 117 | * @param out Where to write the Messenger. 118 | */ 119 | public static void writeMessengerOrNullToParcel(RaCustomMessenger messenger, 120 | Parcel out) { 121 | out.writeStrongBinder(messenger != null ? messenger.mTarget.asBinder() 122 | : null); 123 | } 124 | 125 | /** 126 | * Convenience function for reading either a Messenger or null pointer from 127 | * a Parcel. You must have previously written the Messenger with 128 | * {@link #writeMessengerOrNullToParcel}. 129 | * 130 | * @param in The Parcel containing the written Messenger. 131 | * @return Returns the Messenger read from the Parcel, or null if null had 132 | * been written. 133 | */ 134 | public static RaCustomMessenger readMessengerOrNullFromParcel(Parcel in) { 135 | IBinder b = in.readStrongBinder(); 136 | return b != null ? new RaCustomMessenger(b) : null; 137 | } 138 | 139 | /** 140 | * Create a Messenger from a raw IBinder, which had previously been 141 | * retrieved with {@link #getBinder}. 142 | * 143 | * @param target The IBinder this Messenger should communicate with. 144 | */ 145 | public RaCustomMessenger(@Nullable IBinder target) { 146 | mTarget = target != null ? IRaMessenger.Stub.asInterface(target) : null; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/model/RaRequestTypeArg.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Softtanck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.model 17 | 18 | import android.os.Parcelable 19 | import androidx.annotation.Keep 20 | import kotlinx.parcelize.Parcelize 21 | import kotlinx.parcelize.RawValue 22 | 23 | /** 24 | * Created by Softtanck on 2022/3/12 25 | * A standard parameter builder. 26 | * Will be used in [RemoteServiceMethod] 27 | */ 28 | @Keep 29 | @Parcelize 30 | data class RaRequestTypeArg(val arg: @RawValue Any) : Parcelable -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/model/RaRequestTypeParameter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Softtanck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.model 17 | 18 | import android.os.Parcelable 19 | import androidx.annotation.Keep 20 | import kotlinx.parcelize.Parcelize 21 | 22 | /** 23 | * Created by Softtanck on 2022/3/12 24 | * A standard parameter builder. 25 | * Will be used in [RemoteServiceMethod] 26 | */ 27 | @Keep 28 | @Parcelize 29 | data class RaRequestTypeParameter(val parameterTypeClasses: Class<*>) : Parcelable -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/RaClientApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Softtanck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.ramessageclient 17 | 18 | import android.content.ComponentName 19 | import android.content.Context 20 | import android.os.Build 21 | import android.os.Bundle 22 | import android.os.Message 23 | import android.os.Parcelable 24 | import com.softtanck.IRaMessageInterface 25 | import com.softtanck.MESSAGE_BUNDLE_METHOD_NAME_KEY 26 | import com.softtanck.MESSAGE_BUNDLE_TYPE_ARG_KEY 27 | import com.softtanck.MESSAGE_BUNDLE_TYPE_PARAMETER_KEY 28 | import com.softtanck.model.RaRequestTypeParameter 29 | import com.softtanck.ramessageclient.core.BaseServiceConnection 30 | import com.softtanck.ramessageclient.core.RaServiceConnector 31 | import com.softtanck.ramessageclient.core.engine.retrofit.RaRetrofit 32 | import com.softtanck.ramessageclient.core.listener.BindStatusChangedListener 33 | import com.softtanck.ramessageclient.core.listener.ClientListenerManager 34 | import com.softtanck.ramessageclient.core.listener.RaRemoteMessageListener 35 | import com.softtanck.ramessageclient.core.model.RaClientBindStatus 36 | import com.softtanck.ramessageclient.core.util.ResponseHandler 37 | import kotlinx.coroutines.flow.MutableStateFlow 38 | 39 | /** 40 | * @author Softtanck 41 | * @date 2022/3/12 42 | * Description: [getDefaultComponentName]、[addBindStatusListener]、[removeBindStatusListener]、[clearAllBindStatusListener]、[removeRemoteBroadcastMessageListener] 43 | */ 44 | class RaClientApi private constructor() { 45 | 46 | private val raRetrofit by lazy { RaRetrofit(false) } 47 | 48 | private val _innerDefaultComponentName by lazy { ComponentName(BaseServiceConnection::class.java.`package`?.name ?: "", BaseServiceConnection::class.java.name) } 49 | 50 | private val remoteConnections by lazy { mutableListOf() } 51 | 52 | companion object { 53 | @JvmStatic 54 | val INSTANCE: RaClientApi by lazy { RaClientApi() } 55 | } 56 | 57 | /** 58 | * Bind a remote service. 59 | * @param context the context 60 | * @param componentName the componentName of remote 61 | * @param bindStatusChangedListener the [BindStatusChangedListener] 62 | */ 63 | @JvmOverloads 64 | fun bindRaConnectionService(context: Context, componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener? = null) { 65 | val localRaRemoteConnection = remoteConnections.find { it.serviceConnector.raClientBindStatus.componentName == componentName } 66 | val localRaServiceConnector = localRaRemoteConnection?.serviceConnector 67 | if (localRaServiceConnector == null) { 68 | val newLocalRaServiceConnector: RaServiceConnector 69 | val newLocalRaRemoteConnection: RaServiceConnector.RaRemoteConnection 70 | synchronized(remoteConnections) { 71 | newLocalRaServiceConnector = RaServiceConnector(context, RaClientBindStatus(componentName = componentName, bindStatus = MutableStateFlow(false), bindInProgress = MutableStateFlow(false))) 72 | newLocalRaRemoteConnection = RaServiceConnector.RaRemoteConnection(serviceConnector = newLocalRaServiceConnector) 73 | remoteConnections.add(newLocalRaRemoteConnection) 74 | } 75 | newLocalRaServiceConnector.bindRaConnectionService(componentName = componentName, bindStatusChangedListener = bindStatusChangedListener) 76 | } else { 77 | localRaServiceConnector.bindRaConnectionService(componentName = componentName, bindStatusChangedListener = bindStatusChangedListener) 78 | } 79 | } 80 | 81 | /** 82 | * Unbind the remote service, should be called on background thread. 83 | * @param componentName the componentName of remote 84 | */ 85 | fun unbindRaConnectionService(componentName: ComponentName) { 86 | remoteConnections.find { it.serviceConnector.raClientBindStatus.componentName == componentName }?.run { 87 | serviceConnector.unbindRaConnectionService() 88 | synchronized(remoteConnections) { 89 | remoteConnections.remove(this) 90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 91 | remoteConnections.removeIf { !it.serviceConnector.raClientBindStatus.bindStatus.value } 92 | } else { 93 | remoteConnections.removeAll { !it.serviceConnector.raClientBindStatus.bindStatus.value } 94 | } 95 | } 96 | ClientListenerManager.INSTANCE.clearAllBindStatusChangedListener(componentName = componentName) 97 | } 98 | } 99 | 100 | fun getDefaultComponentName(): ComponentName = _innerDefaultComponentName 101 | 102 | /** 103 | * Added a bind listener 104 | * @param bindStatusChangedListener the [BindStatusChangedListener] 105 | */ 106 | fun addBindStatusListener(componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener) { 107 | ClientListenerManager.INSTANCE.addBindStatusChangedListener(componentName, bindStatusChangedListener) 108 | } 109 | 110 | /** 111 | * Remove a bind listener 112 | * @param bindStatusChangedListener the [BindStatusChangedListener] 113 | */ 114 | fun removeBindStatusListener(componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener) { 115 | ClientListenerManager.INSTANCE.removeBindStatusChangedListener(componentName, bindStatusChangedListener) 116 | } 117 | 118 | /** 119 | * Clear all bind listeners 120 | */ 121 | fun clearAllBindStatusListener() { 122 | ClientListenerManager.INSTANCE.clearAllBindStatusChangedListener() 123 | } 124 | 125 | /** 126 | * Add a remote broadcast message listener 127 | * @param remoteMessageListener the [RaRemoteMessageListener] 128 | */ 129 | fun addRemoteBroadcastMessageListener(componentName: ComponentName, remoteMessageListener: RaRemoteMessageListener) { 130 | ClientListenerManager.INSTANCE.addRemoteBroadCastMessageCallback(componentName, remoteMessageListener) 131 | } 132 | 133 | /** 134 | * Remove a remote broadcast message listener 135 | */ 136 | fun removeRemoteBroadcastMessageListener(componentName: ComponentName, remoteMessageListener: RaRemoteMessageListener) { 137 | ClientListenerManager.INSTANCE.removeRemoteBroadCastMessageCallback(componentName, remoteMessageListener) 138 | } 139 | 140 | /** 141 | * Clear all remote broadcast message listeners 142 | */ 143 | fun clearAllRemoteBroadcastMessageListener(componentName: ComponentName) { 144 | ClientListenerManager.INSTANCE.clearAllRemoteBroadCastMessageCallbacks(componentName) 145 | } 146 | 147 | /** 148 | * Current the client is bound to server. 149 | * ture bound, otherwise not. 150 | */ 151 | fun isBoundToService(componentName: ComponentName) = remoteConnections.find { it.serviceConnector.raClientBindStatus.componentName == componentName }?.serviceConnector?.raClientBindStatus?.bindStatus ?: false 152 | 153 | /** 154 | * Disconnect all connections. 155 | * NOTE: All remote broadcast message listeners will not be removed. 156 | * If you want to remove all listeners, please call [clearAllRemoteBroadcastMessageListener] 157 | */ 158 | fun disconnectAll() { 159 | synchronized(remoteConnections) { 160 | remoteConnections.forEach { it.serviceConnector.unbindRaConnectionService() } 161 | remoteConnections.clear() 162 | } 163 | ClientListenerManager.INSTANCE.clearAllBindStatusChangedListener() 164 | } 165 | 166 | /** 167 | * Destroy all resources. include connections, callbacks(like: [RaRemoteMessageListener]) 168 | */ 169 | fun destroyAllResources() { 170 | disconnectAll() 171 | ClientListenerManager.INSTANCE.clearAllRemoteBroadCastMessageCallbacks() 172 | } 173 | 174 | /** 175 | * Call a remote method with sync. 176 | * @param remoteMethodName the remote method name. 177 | * @param requestParameters the requestParameters. 178 | * @param requestArgs the requestArgs. 179 | */ 180 | fun remoteMethodCallSync(componentName: ComponentName, remoteMethodName: String, requestParameters: ArrayList, requestArgs: ArrayList): T? { 181 | val msg = remoteConnections.find { it.serviceConnector.raClientBindStatus.componentName == componentName }?.serviceConnector?.raClientHandler?.sendMsgToServerSync(Message.obtain().apply { 182 | val bundle = Bundle() 183 | bundle.putString(MESSAGE_BUNDLE_METHOD_NAME_KEY, remoteMethodName) 184 | bundle.putParcelableArrayList(MESSAGE_BUNDLE_TYPE_PARAMETER_KEY, requestParameters) 185 | bundle.putParcelableArrayList(MESSAGE_BUNDLE_TYPE_ARG_KEY, requestArgs) 186 | data = bundle 187 | }) 188 | return ResponseHandler.makeupMessageForRsp(msg) 189 | } 190 | 191 | /** 192 | * Call a remote method with async. 193 | * @param componentName the componentName of remote 194 | * @param remoteMethodName the remote method name. 195 | * @param remoteMethodParameterTypes the requestParameters for find the remote method. 196 | * @param raRemoteMessageListener the [RaRemoteMessageListener], can be NULL. 197 | * @param args the requestArgs. 198 | */ 199 | @JvmOverloads 200 | fun remoteMethodCallAsync(componentName: ComponentName, remoteMethodName: String, remoteMethodParameterTypes: ArrayList, args: ArrayList, raRemoteMessageListener: RaRemoteMessageListener? = null) { 201 | remoteConnections.find { it.serviceConnector.raClientBindStatus.componentName == componentName }?.serviceConnector?.raClientHandler?.sendMsgToServerAsync(Message.obtain().apply { 202 | val bundle = Bundle() 203 | bundle.putString(MESSAGE_BUNDLE_METHOD_NAME_KEY, remoteMethodName) 204 | bundle.putParcelableArrayList(MESSAGE_BUNDLE_TYPE_PARAMETER_KEY, remoteMethodParameterTypes) 205 | bundle.putParcelableArrayList(MESSAGE_BUNDLE_TYPE_ARG_KEY, args) 206 | data = bundle 207 | }, raRemoteMessageListener) 208 | } 209 | 210 | /** 211 | * Create an implementation of the API endpoints defined by the {@code service} interface. 212 | * Like retrofit. 213 | */ 214 | fun create(componentName: ComponentName, service: Class): T = raRetrofit.create(componentName = componentName, service = service) 215 | 216 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/BaseServiceConnection.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import android.os.HandlerThread 8 | import android.util.Log 9 | import com.softtanck.model.RaCustomMessenger 10 | import com.softtanck.ramessageclient.core.engine.RaClientHandler 11 | import com.softtanck.ramessageclient.core.listener.BindStatusChangedListener 12 | import com.softtanck.ramessageclient.core.listener.ClientListenerManager 13 | import com.softtanck.ramessageclient.core.model.RaClientBindStatus 14 | import com.softtanck.ramessageclient.core.util.LockHelper 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | 18 | /** 19 | * @author Softtanck 20 | * @date 2022/3/12 21 | * Description: TODO 22 | */ 23 | abstract class BaseServiceConnection(val context: Context, val raClientBindStatus: RaClientBindStatus) : ServiceConnection { 24 | private val TAG: String = this.javaClass.simpleName 25 | 26 | private val workThreadHandler = HandlerThread(TAG) 27 | internal val raClientHandler by lazy { 28 | // Let the background thread started 29 | if (!workThreadHandler.isAlive) { 30 | workThreadHandler.start() 31 | } 32 | RaClientHandler(looper = workThreadHandler.looper, raClientBindStatus = raClientBindStatus) 33 | } 34 | 35 | // The unbind is triggered by manual, Like [unbindRaConnectionService] 36 | // This flag is for reconnect the service if the IBinder died from server. 37 | private val _isUnbindTriggeredByManualStateFlow = MutableStateFlow(false) 38 | val isUnbindTriggeredByManualStateFlow = _isUnbindTriggeredByManualStateFlow.asStateFlow() 39 | 40 | /** 41 | * Bind a remote service. 42 | * @param componentName the componentName 43 | * @param bindStatusChangedListener the [BindStatusChangedListener] 44 | */ 45 | fun bindRaConnectionService(componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener?) { 46 | if (raClientBindStatus.bindInProgress.value) { 47 | Log.w(TAG, "[CLIENT] Binding is in progress, Ignore this request. Thread:${Thread.currentThread()}") 48 | // Add the listener if it is not NULL 49 | bindStatusChangedListener?.let { ClientListenerManager.INSTANCE.addBindStatusChangedListener(componentName = componentName, bindStatusChangedListener = bindStatusChangedListener) } 50 | } else { 51 | synchronized(LockHelper.BIND_IN_PROGRESS_OBJ_LOCK) { 52 | Log.d(TAG, "[CLIENT] Before CAS bindInProgress:${raClientBindStatus.bindInProgress.value}, Thread:${Thread.currentThread()}") 53 | if (raClientBindStatus.bindInProgress.value) { 54 | Log.w(TAG, "[CLIENT] Binding is in progress, Ignore this request. Thread:${Thread.currentThread()}") 55 | // Add the listener if it is not NULL 56 | bindStatusChangedListener?.let { ClientListenerManager.INSTANCE.addBindStatusChangedListener(componentName = componentName, bindStatusChangedListener = bindStatusChangedListener) } 57 | return@synchronized 58 | } 59 | Log.d(TAG, "[CLIENT] Binding to RaConnectionService. Thread:${Thread.currentThread()}") 60 | // Make sure the bind is changed success!!! 61 | raClientBindStatus.bindInProgress.value = true 62 | Log.d(TAG, "[CLIENT] After CAS bindInProgress:${raClientBindStatus.bindInProgress.value}, Thread:${Thread.currentThread()}") 63 | bindStatusChangedListener?.let { ClientListenerManager.INSTANCE.addBindStatusChangedListener(componentName = componentName, bindStatusChangedListener = bindStatusChangedListener) } 64 | val serviceIntent = Intent() 65 | serviceIntent.component = componentName 66 | serviceIntent.putExtra(RaCustomMessenger.raMsgVersion.first, RaCustomMessenger.raMsgVersion.second) 67 | /* 68 | * These code from AOSP. :) 69 | * public boolean filterEquals(Intent other) { 70 | * if (other == null) { 71 | * return false; 72 | * } 73 | * // Action,Uri,MIME type,PackageName,Component,Category 74 | * if (!Objects.equals(this.mAction, other.mAction)) return false; 75 | * if (!Objects.equals(this.mData, other.mData)) return false; 76 | * if (!Objects.equals(this.mType, other.mType)) return false; 77 | * if (!Objects.equals(this.mPackage, other.mPackage)) return false; 78 | * if (!Objects.equals(this.mComponent, other.mComponent)) return false; 79 | * if (!Objects.equals(this.mCategories, other.mCategories)) return false; 80 | * 81 | * return true; 82 | * } 83 | */ 84 | // If every action is different it makes the Service's onBind call every time, which is compatible with the old code. PLEASE DO NOT REMOVE THIS LINE!!! 85 | serviceIntent.action = RaCustomMessenger.raMsgVersion.toString() 86 | var bindServiceResult = false 87 | try { 88 | bindServiceResult = context.bindService(serviceIntent, this, Context.BIND_AUTO_CREATE) 89 | } catch (e: SecurityException) { 90 | Log.w(TAG, "[CLIENT] Looks you are missing the permission, ${e.message}") 91 | } finally { 92 | Log.d(TAG, "[CLIENT] Binding Api results:$bindServiceResult, Thread:${Thread.currentThread()}") 93 | raClientBindStatus.bindInProgress.value = false 94 | _isUnbindTriggeredByManualStateFlow.value = false 95 | if (!bindServiceResult) { // Callback the result to user if the action is failed to execute 96 | // Use the iterator to avoid CME!!! 97 | val iterator = ClientListenerManager.INSTANCE.getAllBindStatusChangedListener(componentName = componentName).iterator() 98 | while (iterator.hasNext()) { 99 | iterator.next().onConnectRaServicesFailed(componentName) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Unbind the remove service 109 | */ 110 | fun unbindRaConnectionService() { 111 | // Check current status with bound. 112 | if (raClientBindStatus.bindStatus.value) { 113 | synchronized(LockHelper.BIND_RESULT_OBJ_LOCK) { 114 | if (raClientBindStatus.bindStatus.value) { 115 | Log.d(TAG, "[CLIENT] Unbinding from RaMessageService, Thread: ${Thread.currentThread()}") 116 | // 1. Trying to send the message of disconnected to server if the connection is good. 117 | raClientHandler.trySendDisconnectedToService() 118 | raClientHandler.setOutBoundMessenger(null) 119 | // 3. Last, unbind the service 120 | context.unbindService(this) 121 | // 4. mark the flag as true 122 | _isUnbindTriggeredByManualStateFlow.value = true 123 | Log.d(TAG, "[CLIENT] unbindRaConnectionService: done") 124 | } else { 125 | Log.w(TAG, "[CLIENT] Already in unbinding, Ignore this request, Thread: ${Thread.currentThread()}") 126 | } 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/RaServiceConnector.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.os.IBinder 6 | import android.util.Log 7 | import com.softtanck.model.RaCustomMessenger 8 | import com.softtanck.ramessage.IRaMessenger 9 | import com.softtanck.ramessageclient.RaClientApi 10 | import com.softtanck.ramessageclient.core.model.RaClientBindStatus 11 | 12 | /** 13 | * @author Softtanck 14 | * @date 2022/3/12 15 | * Description: TODO 16 | */ 17 | internal class RaServiceConnector(context: Context, clientBindStatus: RaClientBindStatus) : BaseServiceConnection(context, clientBindStatus) { 18 | 19 | data class RaRemoteConnection(val serviceConnector: RaServiceConnector) 20 | 21 | companion object { 22 | private val TAG: String = RaServiceConnector::class.java.simpleName 23 | } 24 | 25 | // NOTE: This method works on Main thread. 26 | override fun onServiceConnected(componentName: ComponentName, service: IBinder) { 27 | Log.d(TAG, "[CLIENT] onServiceConnected : $componentName, serviceDesc:${service.interfaceDescriptor}, thread:${Thread.currentThread()}") 28 | raClientHandler.setOutBoundMessenger(IRaMessenger.Stub.asInterface(service)) 29 | if (!raClientHandler.sendRegisterMsgToServer(RaCustomMessenger(raClientHandler))) { 30 | Log.e(TAG, "[CLIENT] Failed to send msg to server, Since outBoundMessenger type is null") 31 | } 32 | } 33 | 34 | override fun onServiceDisconnected(componentName: ComponentName) { 35 | Log.d(TAG, "[CLIENT] onServiceDisconnected : $componentName, thread:${Thread.currentThread()}") 36 | raClientHandler.onBindStatusChanged(false) 37 | } 38 | 39 | override fun onBindingDied(componentName: ComponentName) { 40 | val unbindTriggeredByManual = isUnbindTriggeredByManualStateFlow.value 41 | Log.w(TAG, "[CLIENT] onBindingDied : $componentName, unbindTriggeredByManual:$unbindTriggeredByManual") 42 | if (!unbindTriggeredByManual) { // Retry logic is performed only the connection disconnected from the system. 43 | unbindRaConnectionService() 44 | // Looks like the server is dead, so we should try to reconnect. 45 | // TODO : Delay should be added? 46 | RaClientApi.INSTANCE.bindRaConnectionService(context = context, componentName = componentName) 47 | } 48 | } 49 | 50 | override fun onNullBinding(componentName: ComponentName) { 51 | Log.w(TAG, "[CLIENT] onNullBinding : $componentName") 52 | } 53 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/BaseClientHandler.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.engine 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.os.Message 6 | import android.os.RemoteException 7 | import android.util.Log 8 | import android.util.SparseArray 9 | import com.softtanck.ramessage.IRaMessenger 10 | import com.softtanck.ramessageclient.core.listener.ClientListenerManager 11 | import com.softtanck.ramessageclient.core.listener.RaRemoteMessageListener 12 | import com.softtanck.ramessageclient.core.model.RaClientBindStatus 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import java.util.concurrent.atomic.AtomicInteger 16 | 17 | /** 18 | * @author Softtanck 19 | * @date 2022/3/12 20 | * Description: The base handler for client. 21 | * @param looper the looper for handler 22 | * @param raClientBindStatus the [RaClientBindStatus] 23 | */ 24 | internal abstract class BaseClientHandler(looper: Looper, private val raClientBindStatus: RaClientBindStatus) : Handler(looper) { 25 | 26 | companion object { 27 | private const val TAG: String = "BaseClientHandler" 28 | } 29 | 30 | val innerMessenger: IRaMessenger.Stub by lazy { RaCustomClientMessengerImpl() } 31 | 32 | /** 33 | * The transaction ID of a message and generated by the client. 34 | * Also, It is a thread-safe. see [safelyIncrement] 35 | */ 36 | private val msgTransactionNumber = AtomicInteger(0) 37 | 38 | /** 39 | * a output messenger for server. client will send message to server by this messenger. 40 | */ 41 | private val _outputMessengerStateFlow = MutableStateFlow(null) 42 | 43 | /** 44 | * The output messenger for server. client will send message to server by this messenger. 45 | */ 46 | val outputMessengerStateFlow = _outputMessengerStateFlow.asStateFlow() 47 | 48 | /** 49 | * Remember all callbacks from the client. And WeakReference is used as value. 50 | * That can be void memory leaks here. 51 | */ 52 | protected val singleCallbacks: SparseArray = SparseArray() 53 | 54 | /** 55 | * will be called when a message arrived from server. 56 | * @param msg the message 57 | * @param isSync is sync message 58 | * @return null or message from client, if null, the message will be ignored. 59 | */ 60 | abstract fun onRemoteMessageArrived(msg: Message, isSync: Boolean): Message? 61 | 62 | /** 63 | * Send a message to server with sync 64 | * @param message the message 65 | * @return null or message from server 66 | */ 67 | protected fun sendSyncMessageToServer(message: Message): Message? = if (raClientBindStatus.bindStatus.value) { 68 | try { 69 | outputMessengerStateFlow.value?.sendSync(message.apply { 70 | arg1 = safelyIncrement() 71 | }) 72 | } catch (e: RemoteException) { 73 | Log.e(TAG, "[CLIENT] Failed to send sync msg to server, msg: $message, reason: ${e.message}") 74 | null 75 | } 76 | } else { 77 | Log.w(TAG, "[CLIENT] You are disconnected with server, Ignore this the message. msg:$message-sync") 78 | null 79 | } 80 | 81 | /** 82 | * Send a message to server with async 83 | * @param message the message 84 | * @param raRemoteMessageListener remote callback 85 | * @return send success or not 86 | */ 87 | protected fun sendAsyncMessageToServer(message: Message, raRemoteMessageListener: RaRemoteMessageListener? = null): Boolean { 88 | if (raClientBindStatus.bindStatus.value) { 89 | return runCatching { 90 | outputMessengerStateFlow.value?.let { messenger -> 91 | val tempTrxId = safelyIncrement() 92 | synchronized(singleCallbacks) { 93 | singleCallbacks.put(tempTrxId, raRemoteMessageListener) 94 | } 95 | messenger.send(message.apply { arg1 = tempTrxId }) 96 | true 97 | } ?: run { 98 | Log.e(TAG, "[CLIENT] Failed to send async msg to server, msg: $message") 99 | false 100 | } 101 | }.onFailure { exception -> 102 | Log.e(TAG, "[CLIENT] Failed to send sync msg to server, msg: $message, reason: ${exception.message}") 103 | safelyNULLCallbackRemoteMessage(raRemoteMessageListener) 104 | }.getOrDefault(false) 105 | } else { 106 | Log.w(TAG, "[CLIENT] You are disconnected with server, Ignore this the message. msg:$message-async") 107 | safelyNULLCallbackRemoteMessage(raRemoteMessageListener) 108 | return false 109 | } 110 | } 111 | 112 | /** 113 | * Send a message to server with async without safety check 114 | * @param message the message 115 | * @param raRemoteMessageListener remote callback 116 | * @return send success or not 117 | */ 118 | protected fun notSafetySendAsyncMessageToServer(message: Message, raRemoteMessageListener: RaRemoteMessageListener? = null): Boolean { 119 | return runCatching { 120 | outputMessengerStateFlow.value?.let { messenger -> 121 | val tempTrxId = safelyIncrement() 122 | synchronized(singleCallbacks) { 123 | singleCallbacks.put(tempTrxId, raRemoteMessageListener) 124 | } 125 | messenger.send(message.apply { arg1 = tempTrxId }) 126 | true 127 | } ?: run { 128 | Log.e(TAG, "[CLIENT] Failed to send async msg to server, msg: $message") 129 | false 130 | } 131 | }.onFailure { exception -> 132 | Log.e(TAG, "[CLIENT] Failed to send sync msg to server, msg: $message, reason: ${exception.message}") 133 | safelyNULLCallbackRemoteMessage(raRemoteMessageListener) 134 | }.getOrDefault(false) 135 | } 136 | 137 | /** 138 | * This method is auto increment the number of transaction. 139 | * Will be reset to 0 if it more than [Int.MAX_VALUE] 140 | */ 141 | private fun safelyIncrement(): Int { 142 | if (msgTransactionNumber.incrementAndGet() >= Int.MAX_VALUE) { 143 | @Suppress("ControlFlowWithEmptyBody") 144 | while (!msgTransactionNumber.compareAndSet(msgTransactionNumber.get(), 0)); 145 | } 146 | return msgTransactionNumber.get() 147 | } 148 | 149 | private fun safelyNULLCallbackRemoteMessage(raRemoteMessageListener: RaRemoteMessageListener?) { 150 | runCatching { 151 | raRemoteMessageListener?.onMessageArrived(message = null) 152 | }.onFailure { exception -> 153 | Log.e(TAG, "[CLIENT] Failed to call onMessageArrived, reason: ${exception.message}") 154 | } 155 | } 156 | 157 | /** 158 | * Core messenger for client. AIDL implementation. 159 | */ 160 | private inner class RaCustomClientMessengerImpl : IRaMessenger.Stub() { 161 | override fun send(msg: Message) { 162 | msg.sendingUid = getCallingUid() 163 | this@BaseClientHandler.sendMessage(msg) 164 | } 165 | 166 | override fun sendSync(msg: Message): Message { 167 | msg.sendingUid = getCallingUid() 168 | return this@BaseClientHandler.onRemoteMessageArrived(msg, true) ?: return Message.obtain(msg) 169 | } 170 | } 171 | 172 | /** 173 | * Set an outbound messengers from outside 174 | * @param messenger the outBoundMessenger from server 175 | */ 176 | open fun setOutBoundMessenger(messenger: IRaMessenger?) { 177 | _outputMessengerStateFlow.value = messenger 178 | } 179 | 180 | /** 181 | * clear all callbacks, like: [RaRemoteMessageListener] 182 | */ 183 | fun clearAllCallbacks() { 184 | synchronized(singleCallbacks) { 185 | singleCallbacks.clear() 186 | } 187 | // Generally speaking, the broadcast should be removed by the developers themselves. So I have commented out this code. 188 | // ClientListenerManager.INSTANCE.clearAllRemoteBroadCastMessageCallbacks(raClientBindStatus.componentName) 189 | } 190 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/RaClientHandler.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.engine 2 | 3 | import android.os.Bundle 4 | import android.os.Looper 5 | import android.os.Message 6 | import android.util.Log 7 | import com.softtanck.MESSAGE_BUNDLE_REPLY_TO_KEY 8 | import com.softtanck.MESSAGE_CLIENT_BROADCAST_RSP 9 | import com.softtanck.MESSAGE_CLIENT_DISCONNECT_REQ 10 | import com.softtanck.MESSAGE_CLIENT_SINGLE_REQ 11 | import com.softtanck.MESSAGE_CLIENT_SINGLE_RSP 12 | import com.softtanck.MESSAGE_REGISTER_CLIENT_REQ 13 | import com.softtanck.MESSAGE_REGISTER_CLIENT_RSP 14 | import com.softtanck.model.RaCustomMessenger 15 | import com.softtanck.ramessageclient.core.listener.ClientListenerManager 16 | import com.softtanck.ramessageclient.core.listener.DisconnectedReason 17 | import com.softtanck.ramessageclient.core.listener.RA_DISCONNECTED_ABNORMAL 18 | import com.softtanck.ramessageclient.core.listener.RA_DISCONNECTED_MANUAL 19 | import com.softtanck.ramessageclient.core.listener.RaRemoteMessageListener 20 | import com.softtanck.ramessageclient.core.model.RaClientBindStatus 21 | import com.softtanck.ramessageclient.core.util.LockHelper 22 | 23 | /** 24 | * @author Softtanck 25 | * @date 2022/3/12 26 | * Description: TODO 27 | */ 28 | internal class RaClientHandler(looper: Looper, private val raClientBindStatus: RaClientBindStatus) : BaseClientHandler(looper, raClientBindStatus) { 29 | 30 | companion object { 31 | private val TAG: String = RaClientHandler::class.java.simpleName 32 | } 33 | 34 | override fun handleMessage(msg: Message) { 35 | Log.d(TAG, "[CLIENT] RaClientHandler handleMessage: ${msg.what}") 36 | when (msg.what) { 37 | MESSAGE_REGISTER_CLIENT_RSP -> { 38 | onBindStatusChanged(true) 39 | } 40 | 41 | MESSAGE_CLIENT_SINGLE_RSP -> { 42 | Log.d(TAG, "[CLIENT] Received a new msg from server: $msg, trxID: ${msg.arg1}") 43 | singleCallbacks.get(msg.arg1)?.run { 44 | // 1. first is the callback needs to be called 45 | onMessageArrived(message = msg) 46 | // 2. finally, remove the trxID from the map 47 | synchronized(singleCallbacks) { 48 | singleCallbacks.remove(msg.arg1) 49 | } 50 | } 51 | } 52 | 53 | MESSAGE_CLIENT_BROADCAST_RSP -> { 54 | Log.d(TAG, "[CLIENT] Received a new msg(broadcast) from server: $msg") 55 | ClientListenerManager.INSTANCE.getAllRemoteBroadCastMessageCallbacks(raClientBindStatus.componentName).forEach { callback -> 56 | callback.onMessageArrived(message = msg) 57 | } 58 | } 59 | } 60 | } 61 | 62 | fun onBindStatusChanged(isConnected: Boolean, @DisconnectedReason disconnectedReason: Int = RA_DISCONNECTED_ABNORMAL) { 63 | Log.d(TAG, "[CLIENT] onBindStatusChanged: $isConnected, $disconnectedReason") 64 | if (isConnected) { 65 | if (!raClientBindStatus.bindStatus.value) { 66 | synchronized(LockHelper.BIND_RESULT_OBJ_LOCK) { 67 | if (!raClientBindStatus.bindStatus.value) { // Double check here 68 | raClientBindStatus.bindStatus.value = true 69 | val iterator = ClientListenerManager.INSTANCE.getAllBindStatusChangedListener(raClientBindStatus.componentName).iterator() 70 | while (iterator.hasNext()) { 71 | val bindStateListener = iterator.next() 72 | bindStateListener.onConnectedToRaServices(raClientBindStatus.componentName) 73 | } 74 | } else { 75 | Log.w(TAG, "[CLIENT] Already scheduled, Ignore this request, isConnected: $isConnected, Thread:${Thread.currentThread()}") 76 | } 77 | } 78 | } else { 79 | // Just log here 80 | Log.w(TAG, "[CLIENT] It looks like you should not going here. Thread:${Thread.currentThread()}") 81 | } 82 | } else { 83 | if (raClientBindStatus.bindStatus.value) { 84 | synchronized(LockHelper.BIND_RESULT_OBJ_LOCK) { 85 | if (raClientBindStatus.bindStatus.value) { // Double check here 86 | raClientBindStatus.bindStatus.value = false 87 | val iterator = ClientListenerManager.INSTANCE.getAllBindStatusChangedListener(raClientBindStatus.componentName).iterator() 88 | while (iterator.hasNext()) { 89 | val bindStatusListener = iterator.next() 90 | bindStatusListener.onDisconnectedFromRaServices(componentName = raClientBindStatus.componentName, disconnectedReason = disconnectedReason) 91 | } 92 | } else { 93 | Log.w(TAG, "[CLIENT] Already scheduled, Ignore this request, isConnected: true, Thread:${Thread.currentThread()}") 94 | } 95 | // finally, clear the singleCallbacks and broadcastCallbacks 96 | clearAllCallbacks() 97 | } 98 | } else { 99 | raClientBindStatus.bindStatus.value = false 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Send a message to service with compat. 106 | * Since the client maybe not have the permission to access the hidden API, we need to use the compat. 107 | * if the client has the permission, we can use the normal way to send message. 108 | * Note: Currently, This method only for disconnection. 109 | * @param message the message to send 110 | */ 111 | private fun sendMsgToServiceCompat(message: Message) { 112 | if (outputMessengerStateFlow.value != null && outputMessengerStateFlow.value is RaCustomMessenger) { 113 | sendSyncMessageToServer(message) 114 | } else { 115 | sendMsgToServerAsync(message) 116 | } 117 | } 118 | 119 | private fun sendMsgWithoutResetMsgWhatCompat(message: Message) { 120 | if (outputMessengerStateFlow.value != null && outputMessengerStateFlow.value is RaCustomMessenger) { 121 | sendMsgToServerAsyncWithoutResetMsgWhat(message) 122 | } else { 123 | sendMsgToServerSyncWithoutResetMsgWhat(message) 124 | } 125 | } 126 | 127 | fun trySendDisconnectedToService() { 128 | sendMsgWithoutResetMsgWhatCompat(Message.obtain().apply { what = MESSAGE_CLIENT_DISCONNECT_REQ }) 129 | // I think you are disconnected with service, So hardcode here. 130 | onBindStatusChanged(false, RA_DISCONNECTED_MANUAL) 131 | } 132 | 133 | private fun sendMsgToServerAsyncWithoutResetMsgWhat(message: Message) { 134 | sendAsyncMessageToServer(message) 135 | } 136 | 137 | private fun sendMsgToServerSyncWithoutResetMsgWhat(message: Message): Message? = sendSyncMessageToServer(message) 138 | 139 | fun sendMsgToServerAsync(message: Message, raRemoteMessageListener: RaRemoteMessageListener? = null) { 140 | sendAsyncMessageToServer(message.apply { what = MESSAGE_CLIENT_SINGLE_REQ }, raRemoteMessageListener) 141 | } 142 | 143 | fun sendMsgToServerSync(message: Message): Message? = sendSyncMessageToServer(message.apply { what = MESSAGE_CLIENT_SINGLE_REQ }) 144 | 145 | fun sendRegisterMsgToServer(inputMessenger: RaCustomMessenger) = notSafetySendAsyncMessageToServer(message = Message.obtain(null, MESSAGE_REGISTER_CLIENT_REQ).apply { 146 | // replyTo inputMessenger as Messenger if the client is failed to reflect the messenger. 147 | data = Bundle().apply { 148 | putParcelable(MESSAGE_BUNDLE_REPLY_TO_KEY, inputMessenger) 149 | } 150 | }) 151 | 152 | override fun onRemoteMessageArrived(msg: Message, isSync: Boolean): Message? { 153 | //TODO("Not yet implemented") 154 | // TODO : 跟服务端一样的实现。不过要反着来: 155 | // 即: 服务端动态代理接口,然后获取参数类型、参数、函数名字后,通过IPC调用,把相关参数传递给客户端,客户端通过「反射」执行对应返回并同步返回; 156 | // 那么异步情况如何处理? 157 | return null 158 | } 159 | 160 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/KotlinExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:JvmName("KotlinExtensions") 18 | 19 | package com.softtanck.ramessageclient.core.engine.retrofit 20 | 21 | import android.content.ComponentName 22 | import android.os.Parcelable 23 | import com.softtanck.model.RaRequestTypeParameter 24 | import com.softtanck.ramessageclient.RaClientApi 25 | import com.softtanck.ramessageclient.core.util.ResponseHandler 26 | import com.softtanck.ramessageservice.util.ServerUtil 27 | import kotlinx.coroutines.Dispatchers 28 | import kotlinx.coroutines.runBlocking 29 | import kotlinx.coroutines.suspendCancellableCoroutine 30 | import java.lang.reflect.Method 31 | import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED 32 | import kotlin.coroutines.intrinsics.intercepted 33 | import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn 34 | import kotlin.coroutines.resume 35 | import kotlin.coroutines.resumeWithException 36 | 37 | /** 38 | * Invoke the normal or suspend method 39 | * @param componentName The component name of the remote service 40 | * @param methodName The method name of the remote service 41 | * @param parameterTypes The parameters of the remote service. used to find the method 42 | * @param argsList The arguments of the remote service 43 | */ 44 | suspend fun awaitResponse(componentName: ComponentName, methodName: String, parameterTypes: ArrayList, argsList: ArrayList): T? = 45 | suspendCancellableCoroutine { continuation -> 46 | continuation.invokeOnCancellation { 47 | // TODO : Remove? 48 | } 49 | RaClientApi.INSTANCE.remoteMethodCallAsync(componentName = componentName, remoteMethodName = methodName, remoteMethodParameterTypes = parameterTypes, args = argsList) { message -> 50 | if (continuation.isActive) continuation.resume(ResponseHandler.makeupMessageForRsp(message)) 51 | } 52 | } 53 | 54 | /** 55 | * Force the calling coroutine to suspend before throwing [this]. 56 | * 57 | * This is needed when a checked exception is synchronously caught in a [java.lang.reflect.Proxy] 58 | * invocation to avoid being wrapped in [java.lang.reflect.UndeclaredThrowableException]. 59 | * 60 | * The implementation is derived from: 61 | * https://github.com/Kotlin/kotlinx.coroutines/pull/1667#issuecomment-556106349 62 | */ 63 | internal suspend fun Exception.suspendAndThrow(): Nothing { 64 | suspendCoroutineUninterceptedOrReturn { continuation -> 65 | Dispatchers.Default.dispatch(continuation.context) { 66 | continuation.intercepted().resumeWithException(this@suspendAndThrow) 67 | } 68 | COROUTINE_SUSPENDED 69 | } 70 | } 71 | 72 | /** 73 | * Invoke the normal or suspend method. 74 | * @param obj obj in the object 75 | * @param args any parameters for this the method 76 | */ 77 | fun Method.invokeCompat(obj: Any, vararg args: Any?): Any? { 78 | val isSuspendMethod = ServerUtil.isSuspendMethod(this) 79 | return if (isSuspendMethod) { 80 | runBlocking { this@invokeCompat.invokeSuspend(obj, *args) } 81 | } else { 82 | this.invoke(obj, *args) 83 | } 84 | } 85 | 86 | /** 87 | * Invoke the suspend method. 88 | * @param obj obj in the object 89 | * @param args any parameters for this the method 90 | */ 91 | suspend fun Method.invokeSuspend(obj: Any, vararg args: Any?): Any? = suspendCoroutineUninterceptedOrReturn { cont -> 92 | invoke(obj, *args, cont) 93 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/Platform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.ramessageclient.core.engine.retrofit; 17 | 18 | import android.annotation.TargetApi; 19 | import android.os.Build; 20 | 21 | import androidx.annotation.Nullable; 22 | 23 | import java.lang.invoke.MethodHandles; 24 | import java.lang.invoke.MethodHandles.Lookup; 25 | import java.lang.reflect.Constructor; 26 | import java.lang.reflect.InvocationHandler; 27 | import java.lang.reflect.InvocationTargetException; 28 | import java.lang.reflect.Method; 29 | 30 | abstract class Platform { 31 | private static final Platform PLATFORM = createPlatform(); 32 | 33 | static Platform get() { 34 | return PLATFORM; 35 | } 36 | 37 | private static Platform createPlatform() { 38 | switch (System.getProperty("java.vm.name")) { 39 | case "Dalvik": 40 | if (Android24.isSupported()) { 41 | return new Android24(); 42 | } 43 | return new Android21(); 44 | 45 | case "RoboVM": 46 | return new RoboVm(); 47 | 48 | default: 49 | if (Java16.isSupported()) { 50 | return new Java16(); 51 | } 52 | if (Java14.isSupported()) { 53 | return new Java14(); 54 | } 55 | return new Java8(); 56 | } 57 | } 58 | 59 | abstract boolean isDefaultMethod(Method method); 60 | 61 | abstract @Nullable 62 | Object invokeDefaultMethod( 63 | Method method, Class declaringClass, Object proxy, Object... args) throws Throwable; 64 | 65 | private static final class Android21 extends Platform { 66 | @Override 67 | boolean isDefaultMethod(Method method) { 68 | return false; 69 | } 70 | 71 | @Nullable 72 | @Override 73 | Object invokeDefaultMethod( 74 | Method method, Class declaringClass, Object proxy, Object... args) { 75 | throw new AssertionError(); 76 | } 77 | } 78 | 79 | @TargetApi(24) 80 | private static final class Android24 extends Platform { 81 | static boolean isSupported() { 82 | return Build.VERSION.SDK_INT >= 24; 83 | } 84 | 85 | private @Nullable 86 | Constructor lookupConstructor; 87 | 88 | @Override 89 | public boolean isDefaultMethod(Method method) { 90 | return method.isDefault(); 91 | } 92 | 93 | @Nullable 94 | @Override 95 | public Object invokeDefaultMethod( 96 | Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { 97 | if (Build.VERSION.SDK_INT < 26) { 98 | throw new UnsupportedOperationException( 99 | "Calling default methods on API 24 and 25 is not supported"); 100 | } 101 | Constructor lookupConstructor = this.lookupConstructor; 102 | if (lookupConstructor == null) { 103 | lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); 104 | lookupConstructor.setAccessible(true); 105 | this.lookupConstructor = lookupConstructor; 106 | } 107 | return lookupConstructor 108 | .newInstance(declaringClass, -1 /* trusted */) 109 | .unreflectSpecial(method, declaringClass) 110 | .bindTo(proxy) 111 | .invokeWithArguments(args); 112 | } 113 | } 114 | 115 | private static final class RoboVm extends Platform { 116 | 117 | @Override 118 | boolean isDefaultMethod(Method method) { 119 | return false; 120 | } 121 | 122 | @Nullable 123 | @Override 124 | Object invokeDefaultMethod( 125 | Method method, Class declaringClass, Object proxy, Object... args) { 126 | throw new AssertionError(); 127 | } 128 | } 129 | 130 | @SuppressWarnings("NewApi") // Not used for Android. 131 | private static final class Java8 extends Platform { 132 | private @Nullable 133 | Constructor lookupConstructor; 134 | 135 | @Override 136 | public boolean isDefaultMethod(Method method) { 137 | return method.isDefault(); 138 | } 139 | 140 | @Override 141 | public @Nullable 142 | Object invokeDefaultMethod( 143 | Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { 144 | Constructor lookupConstructor = this.lookupConstructor; 145 | if (lookupConstructor == null) { 146 | lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); 147 | lookupConstructor.setAccessible(true); 148 | this.lookupConstructor = lookupConstructor; 149 | } 150 | return lookupConstructor 151 | .newInstance(declaringClass, -1 /* trusted */) 152 | .unreflectSpecial(method, declaringClass) 153 | .bindTo(proxy) 154 | .invokeWithArguments(args); 155 | } 156 | } 157 | 158 | /** 159 | * Java 14 allows a regular lookup to succeed for invoking default methods. 160 | * 161 | *

https://bugs.openjdk.java.net/browse/JDK-8209005 162 | */ 163 | @SuppressWarnings("NewApi") // Not used for Android. 164 | private static final class Java14 extends Platform { 165 | static boolean isSupported() { 166 | try { 167 | Object version = Runtime.class.getMethod("version").invoke(null); 168 | Integer feature = (Integer) version.getClass().getMethod("feature").invoke(version); 169 | return feature >= 14; 170 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException ignored) { 171 | return false; 172 | } 173 | } 174 | 175 | @Override 176 | public boolean isDefaultMethod(Method method) { 177 | return method.isDefault(); 178 | } 179 | 180 | @Nullable 181 | @Override 182 | public Object invokeDefaultMethod( 183 | Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { 184 | return MethodHandles.lookup() 185 | .unreflectSpecial(method, declaringClass) 186 | .bindTo(proxy) 187 | .invokeWithArguments(args); 188 | } 189 | } 190 | 191 | /** 192 | * Java 16 has a supported public API for invoking default methods on a proxy. We invoke it 193 | * reflectively because we cannot compile against the API directly. 194 | */ 195 | @SuppressWarnings("NewApi") // Not used for Android. 196 | private static final class Java16 extends Platform { 197 | static boolean isSupported() { 198 | try { 199 | Object version = Runtime.class.getMethod("version").invoke(null); 200 | Integer feature = (Integer) version.getClass().getMethod("feature").invoke(version); 201 | return feature >= 16; 202 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException ignored) { 203 | return false; 204 | } 205 | } 206 | 207 | private @Nullable 208 | Method invokeDefaultMethod; 209 | 210 | @Override 211 | public boolean isDefaultMethod(Method method) { 212 | return method.isDefault(); 213 | } 214 | 215 | @SuppressWarnings("JavaReflectionMemberAccess") // Only available on Java 16, as we expect. 216 | @Nullable 217 | @Override 218 | public Object invokeDefaultMethod( 219 | Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { 220 | Method invokeDefaultMethod = this.invokeDefaultMethod; 221 | if (invokeDefaultMethod == null) { 222 | invokeDefaultMethod = 223 | InvocationHandler.class.getMethod( 224 | "invokeDefault", Object.class, Method.class, Object[].class); 225 | this.invokeDefaultMethod = invokeDefaultMethod; 226 | } 227 | return invokeDefaultMethod.invoke(null, proxy, method, args); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/RaRetrofit.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.engine.retrofit 2 | 3 | import android.content.ComponentName 4 | import java.lang.reflect.InvocationHandler 5 | import java.lang.reflect.Method 6 | import java.lang.reflect.Modifier 7 | import java.lang.reflect.Proxy 8 | import java.util.* 9 | import java.util.concurrent.ConcurrentHashMap 10 | 11 | internal class RaRetrofit(private val validateEagerly: Boolean) { 12 | private val serviceMethodCache: MutableMap?>> = ConcurrentHashMap() 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | // Single-interface proxy creation guarded by parameter safety. 16 | fun create(componentName: ComponentName, service: Class): T { 17 | validateServiceInterface(componentName = componentName, service = service) 18 | return Proxy.newProxyInstance( 19 | service.classLoader, arrayOf>(service), 20 | object : InvocationHandler { 21 | private val emptyArgs = Array(0) {} 22 | 23 | @Throws(Throwable::class) 24 | override fun invoke(proxy: Any, method: Method, args: Array?): Any? { 25 | // If the method is a method from Object then defer to normal invocation. 26 | if (method.declaringClass == Any::class.java) { 27 | return method.invoke(this, *args!!) 28 | } 29 | val platform = Platform.get() 30 | return if (platform.isDefaultMethod(method)) platform.invokeDefaultMethod(method, service, proxy, *(args ?: emptyArgs)) else loadServiceMethod(componentName = componentName, method = method)?.invoke(args) 31 | } 32 | }) as T 33 | } 34 | 35 | private fun validateServiceInterface(componentName: ComponentName, service: Class<*>) { 36 | require(service.isInterface) { "API declarations must be interfaces." } 37 | val check: Deque> = ArrayDeque(1) 38 | check.add(service) 39 | while (!check.isEmpty()) { 40 | val candidate = check.removeFirst() 41 | if (candidate.typeParameters.isNotEmpty()) { 42 | val message = StringBuilder("Type parameters are unsupported on ").append(candidate.name) 43 | if (candidate != service) { 44 | message.append(" which is an interface of ").append(service.name) 45 | } 46 | throw IllegalArgumentException(message.toString()) 47 | } 48 | Collections.addAll(check, *candidate.interfaces) 49 | } 50 | if (validateEagerly) { 51 | val platform = Platform.get() 52 | for (method in service.declaredMethods) { 53 | if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.modifiers)) { 54 | loadServiceMethod(componentName = componentName, method = method) 55 | } 56 | } 57 | } 58 | } 59 | 60 | fun loadServiceMethod(componentName: ComponentName, method: Method): ServiceMethod<*>? { 61 | var result = serviceMethodCache[componentName]?.get(method) 62 | if (result != null) return result 63 | synchronized(serviceMethodCache) { 64 | result = serviceMethodCache[componentName]?.get(method) 65 | if (result == null) { 66 | result = ServiceMethod.parseAnnotations(componentName, method) 67 | serviceMethodCache[componentName]?.set(method, result) 68 | } 69 | } 70 | return result 71 | } 72 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/RemoteServiceMethod.java: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.engine.retrofit; 2 | 3 | import android.content.ComponentName; 4 | import android.os.Parcelable; 5 | import android.util.Log; 6 | 7 | import androidx.annotation.Nullable; 8 | 9 | import com.softtanck.model.RaRequestTypeArg; 10 | import com.softtanck.model.RaRequestTypeParameter; 11 | import com.softtanck.ramessageclient.RaClientApi; 12 | import com.softtanck.util.Utils; 13 | 14 | import java.lang.reflect.Method; 15 | import java.lang.reflect.ParameterizedType; 16 | import java.lang.reflect.Type; 17 | import java.util.ArrayList; 18 | 19 | import kotlin.coroutines.Continuation; 20 | 21 | /** 22 | * This method is copied from retrofit, And this class will be changed later. 23 | * TODO : Remove unused parameters, and the Coroutines will be supported later. -Softtanck 24 | */ 25 | abstract class RemoteServiceMethod extends ServiceMethod { 26 | 27 | private static final String TAG = RemoteServiceMethod.class.getSimpleName(); 28 | 29 | static RemoteServiceMethod parseAnnotations(ComponentName componentName, Method method, RequestFactory requestFactory) { 30 | boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction; 31 | boolean methodHasReturnValue = Utils.hasReturnValueType(method, method.getGenericReturnType()); 32 | 33 | if (isKotlinSuspendFunction) { 34 | Type[] parameterTypes = method.getGenericParameterTypes(); 35 | Type responseType = Utils.getParameterLowerBound(0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]); 36 | methodHasReturnValue = Utils.hasReturnValueType(method, responseType); 37 | } 38 | if (isKotlinSuspendFunction) { 39 | return new SuspendAdapted<>(componentName, method); 40 | } else { 41 | return new CallAdapted<>(componentName, method, methodHasReturnValue); 42 | } 43 | } 44 | 45 | protected abstract @Nullable ReturnT adapt(Object[] args); 46 | 47 | @Nullable 48 | @Override 49 | ReturnT invoke(Object[] args) { 50 | return adapt(args); 51 | } 52 | 53 | static final class SuspendAdapted extends RemoteServiceMethod { 54 | 55 | private final Method method; 56 | private final ComponentName componentName; 57 | 58 | SuspendAdapted(ComponentName componentName, Method method) { 59 | this.method = method; 60 | this.componentName = componentName; 61 | } 62 | 63 | @SuppressWarnings("unchecked") 64 | @Nullable 65 | @Override 66 | protected ReturnT adapt(Object[] args) { 67 | //noinspection unchecked Checked by reflection inside RequestFactory. 68 | Continuation continuation = (Continuation) args[args.length - 1]; 69 | 70 | final ArrayList parameters = new ArrayList<>(); 71 | final ArrayList argsList = new ArrayList<>(); 72 | 73 | for (Class parameterType : method.getParameterTypes()) { 74 | if (Utils.getRawType(parameterType) == Continuation.class) parameters.add(new RaRequestTypeParameter(Continuation.class)); 75 | else parameters.add(new RaRequestTypeParameter(parameterType)); 76 | } 77 | for (Object arg : args) { 78 | if (arg instanceof Continuation) continue; 79 | if (arg instanceof Parcelable) argsList.add((T) arg); 80 | else argsList.add((T) new RaRequestTypeArg(arg)); 81 | } 82 | Log.d(TAG, "[CLIENT] Start the remote methods, methodName:" + method.getName() + ", parameters.size:" + parameters.size() + ", argsList.size:" + argsList.size()); 83 | // See SuspendForBody for explanation about this try/catch. 84 | try { 85 | return (ReturnT) KotlinExtensions.awaitResponse(componentName, method.getName(), parameters, argsList, continuation); 86 | } catch (Exception e) { 87 | return (ReturnT) KotlinExtensions.suspendAndThrow(e, continuation); 88 | } 89 | } 90 | } 91 | 92 | static final class CallAdapted extends RemoteServiceMethod { 93 | 94 | private final Method method; 95 | private final ComponentName componentName; 96 | private final Boolean methodHasReturnValue; 97 | private final ArrayList parameters = new ArrayList<>(); 98 | private final ArrayList argsList = new ArrayList<>(); 99 | 100 | CallAdapted(ComponentName componentName, Method method, Boolean methodHasReturnValue) { 101 | this.method = method; 102 | this.methodHasReturnValue = methodHasReturnValue; 103 | this.componentName = componentName; 104 | } 105 | 106 | @SuppressWarnings("unchecked") 107 | @Override 108 | protected ReturnT adapt(@Nullable Object[] args) { 109 | synchronized (parameters) { 110 | parameters.clear(); 111 | for (Class parameterType : method.getParameterTypes()) { 112 | parameters.add(new RaRequestTypeParameter(parameterType)); 113 | } 114 | } 115 | synchronized (argsList) { 116 | argsList.clear(); 117 | if (args != null) { 118 | for (Object arg : args) { 119 | if (arg instanceof Parcelable) argsList.add((T) arg); 120 | else argsList.add((T) new RaRequestTypeArg(arg)); 121 | } 122 | } 123 | } 124 | Log.d(TAG, "[CLIENT] Start the remote methods, methodName:" + method.getName() + ", parameters.size:" + parameters.size() + ", argsList.size:" + argsList.size()); 125 | if (!methodHasReturnValue) { 126 | RaClientApi.getINSTANCE().remoteMethodCallAsync(componentName, method.getName(), parameters, argsList); 127 | return null; 128 | } else { 129 | Object returnValue = RaClientApi.getINSTANCE().remoteMethodCallSync(componentName, method.getName(), parameters, argsList); 130 | if (returnValue != null) { 131 | return (ReturnT) returnValue; 132 | } else { 133 | return null; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/RequestFactory.java: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.engine.retrofit; 2 | 3 | 4 | import com.softtanck.util.Utils; 5 | 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Type; 8 | 9 | import kotlin.coroutines.Continuation; 10 | 11 | final class RequestFactory { 12 | public static RequestFactory parseAnnotations(Method method) { 13 | return new Builder(method).build(); 14 | } 15 | 16 | final Method method; 17 | final Type[] parameterTypes; 18 | final boolean isKotlinSuspendFunction; 19 | 20 | RequestFactory(Builder builder) { 21 | method = builder.method; 22 | parameterTypes = builder.parameterTypes; 23 | isKotlinSuspendFunction = builder.isKotlinSuspendFunction; 24 | } 25 | 26 | static final class Builder { 27 | final Method method; 28 | final Type[] parameterTypes; 29 | boolean isKotlinSuspendFunction; 30 | 31 | Builder(Method method) { 32 | this.method = method; 33 | this.parameterTypes = method.getGenericParameterTypes(); 34 | } 35 | 36 | RequestFactory build() { 37 | for (Type parameterType : parameterTypes) { 38 | try { 39 | if (Utils.getRawType(parameterType) == Continuation.class) { 40 | isKotlinSuspendFunction = true; 41 | break; 42 | } 43 | } catch (NoClassDefFoundError ignored) { 44 | // Ignored 45 | } 46 | } 47 | return new RequestFactory(this); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/engine/retrofit/ServiceMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.ramessageclient.core.engine.retrofit; 17 | 18 | import android.content.ComponentName; 19 | 20 | import androidx.annotation.Nullable; 21 | 22 | import java.lang.reflect.Method; 23 | 24 | abstract class ServiceMethod { 25 | 26 | static ServiceMethod parseAnnotations(ComponentName componentName, Method method) { 27 | RequestFactory requestFactory = RequestFactory.parseAnnotations(method); 28 | return RemoteServiceMethod.parseAnnotations(componentName, method, requestFactory); 29 | } 30 | 31 | abstract @Nullable T invoke(Object[] args); 32 | } 33 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/listener/BindStatusChangedListener.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.listener 2 | 3 | import android.content.ComponentName 4 | 5 | /** 6 | * @author Softtanck 7 | * @date 2022/3/12 8 | * Description: TODO: TBD 9 | */ 10 | interface BindStatusChangedListener { 11 | 12 | /** 13 | * This method will be invoked if the client is failed to bound the remote server; 14 | * In general, it may be caused by permission issues 15 | * @param componentName the componentName of remote 16 | */ 17 | fun onConnectRaServicesFailed(componentName: ComponentName) 18 | 19 | /** 20 | * This method will be invoked When the client connects to the server successfully. 21 | * What the thread? Currently, Always on background. 22 | * @param componentName the componentName of remote 23 | */ 24 | fun onConnectedToRaServices(componentName: ComponentName) 25 | 26 | /** 27 | * This method will be invoked if the client is disconnected from the server. 28 | * @param disconnectedReason [RA_DISCONNECTED_ABNORMAL] It could be a service exception or a lower level error. [RA_DISCONNECTED_MANUAL] 29 | * Developer actively disconnects 30 | * @param componentName the componentName of remote 31 | */ 32 | fun onDisconnectedFromRaServices(componentName: ComponentName, @DisconnectedReason disconnectedReason: Int) 33 | } 34 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/listener/ClientListenerManager.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.listener 2 | 3 | import android.content.ComponentName 4 | import android.os.Build 5 | 6 | /** 7 | * @author Softtanck 8 | * @date 2022/3/12 9 | * Description: TODO 10 | */ 11 | // TODO : How about weakReference? looks like the WeakReference is not necessary. And will be removed in the future. 12 | internal class ClientListenerManager private constructor() { 13 | companion object { 14 | @JvmStatic 15 | val INSTANCE: ClientListenerManager by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 16 | ClientListenerManager() 17 | } 18 | } 19 | 20 | // first: componentName second: BindStatusListener 21 | private val clientsBindStatusChangedListenerList by lazy { LinkedHashSet>() } 22 | 23 | /** 24 | * Remember all callbacks from the client. And WeakReference is used as value. 25 | * That can be void memory leaks here. 26 | */ 27 | private val broadcastCallbacks by lazy { mutableListOf>() } 28 | 29 | fun addBindStatusChangedListener(componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener) { 30 | synchronized(clientsBindStatusChangedListenerList) { 31 | clientsBindStatusChangedListenerList.add(Pair(first = componentName, second = bindStatusChangedListener)) 32 | } 33 | } 34 | 35 | fun removeBindStatusChangedListener(componentName: ComponentName, bindStatusChangedListener: BindStatusChangedListener) { 36 | synchronized(clientsBindStatusChangedListenerList) { 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 38 | clientsBindStatusChangedListenerList.removeIf { it.first == componentName && it.second == bindStatusChangedListener } 39 | } else { 40 | clientsBindStatusChangedListenerList.remove(Pair(first = componentName, second = bindStatusChangedListener)) 41 | } 42 | } 43 | } 44 | 45 | fun clearAllBindStatusChangedListener() { 46 | synchronized(clientsBindStatusChangedListenerList) { 47 | clientsBindStatusChangedListenerList.clear() 48 | } 49 | } 50 | 51 | fun clearAllBindStatusChangedListener(componentName: ComponentName) { 52 | synchronized(clientsBindStatusChangedListenerList) { 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 54 | clientsBindStatusChangedListenerList.removeIf { it.first == componentName } 55 | } else { 56 | clientsBindStatusChangedListenerList.removeAll { it.first == componentName } 57 | } 58 | } 59 | } 60 | 61 | // TIPS: use the list of copy 62 | fun getAllBindStatusChangedListener(componentName: ComponentName): List = clientsBindStatusChangedListenerList.filter { it.first == componentName }.map { it.second } 63 | 64 | /** 65 | * Add a broadcast callback to the list. 66 | * @param remoteMessageListener The callback to be added. 67 | */ 68 | fun addRemoteBroadCastMessageCallback(componentName: ComponentName, remoteMessageListener: RaRemoteMessageListener) { 69 | synchronized(broadcastCallbacks) { 70 | broadcastCallbacks.add(Pair(componentName, remoteMessageListener)) 71 | } 72 | } 73 | 74 | /** 75 | * Remove a broadcast callback from the list. 76 | */ 77 | fun removeRemoteBroadCastMessageCallback(componentName: ComponentName, remoteMessageListener: RaRemoteMessageListener) { 78 | synchronized(broadcastCallbacks) { 79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 80 | broadcastCallbacks.removeIf { it.first == componentName && it.second == remoteMessageListener } 81 | } else { 82 | broadcastCallbacks.removeAll { it.first == componentName && it.second == remoteMessageListener } 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Clear all broadcast callbacks. 89 | */ 90 | fun clearAllRemoteBroadCastMessageCallbacks(componentName: ComponentName) { 91 | synchronized(broadcastCallbacks) { 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 93 | broadcastCallbacks.removeIf { it.first == componentName } 94 | } else { 95 | broadcastCallbacks.removeAll { it.first == componentName } 96 | } 97 | } 98 | } 99 | 100 | fun clearAllRemoteBroadCastMessageCallbacks() { 101 | synchronized(broadcastCallbacks) { 102 | broadcastCallbacks.clear() 103 | } 104 | } 105 | 106 | fun getAllRemoteBroadCastMessageCallbacks(componentName: ComponentName): List = broadcastCallbacks.filter { it.first == componentName }.map { it.second } 107 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/listener/DisconnectedReason.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.listener 2 | 3 | import androidx.annotation.IntDef 4 | 5 | @MustBeDocumented 6 | @IntDef(RA_DISCONNECTED_MANUAL, RA_DISCONNECTED_ABNORMAL) 7 | @Target(AnnotationTarget.VALUE_PARAMETER) 8 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE) 9 | annotation class DisconnectedReason 10 | 11 | /** 12 | * Developer actively disconnects 13 | */ 14 | const val RA_DISCONNECTED_MANUAL: Int = 1 15 | 16 | /** 17 | * It could be a service exception or a lower level error. 18 | */ 19 | const val RA_DISCONNECTED_ABNORMAL: Int = 2 20 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/listener/RaRemoteMessageListener.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.listener 2 | 3 | import android.os.Message 4 | 5 | /** 6 | * @author Softtanck 7 | * @date 2022/3/12 8 | * Description: TODO 9 | */ 10 | fun interface RaRemoteMessageListener { 11 | /** 12 | * This method will be invoked if the message is arrived from outside. 13 | * @param message the message, null will be returned if request is failed. 14 | */ 15 | fun onMessageArrived(message: Message?) 16 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/model/RaClientBindStatus.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.model 2 | 3 | import android.content.ComponentName 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | 6 | /** 7 | * A model class for bind status. 8 | * @param componentName the componentName of remote 9 | * @param bindStatus the bind status 10 | * @param bindInProgress the bind in progress 11 | */ 12 | data class RaClientBindStatus( 13 | val componentName: ComponentName, 14 | var bindStatus: MutableStateFlow, 15 | var bindInProgress: MutableStateFlow 16 | ) 17 | -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/util/LockHelper.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.util 2 | 3 | /** 4 | * Created by Softtanck on 2022/3/12 5 | * A lock helper for the client. 6 | * These locks both are working on the client. 7 | */ 8 | internal object LockHelper { 9 | /** 10 | * The lock is added when the result of the binding is retrieved 11 | */ 12 | val BIND_RESULT_OBJ_LOCK = Object() 13 | 14 | /** 15 | * Add a lock to the binding process 16 | */ 17 | val BIND_IN_PROGRESS_OBJ_LOCK = Object() 18 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageclient/core/util/ResponseHandler.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageclient.core.util 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.Message 6 | import android.os.Parcelable 7 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_CHAR_SEQUENCE_TYPE 8 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_INTEGER_TYPE 9 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_PARCELABLE_TYPE 10 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_STRING_TYPE 11 | import com.softtanck.MESSAGE_BUNDLE_BOOLEAN_TYPE 12 | import com.softtanck.MESSAGE_BUNDLE_BYTE_TYPE 13 | import com.softtanck.MESSAGE_BUNDLE_CHAR_TYPE 14 | import com.softtanck.MESSAGE_BUNDLE_NORMAL_RSP_KEY 15 | import com.softtanck.MESSAGE_BUNDLE_PARCELABLE_TYPE 16 | import com.softtanck.MESSAGE_BUNDLE_RSP_TYPE_KEY 17 | import com.softtanck.MESSAGE_BUNDLE_STRING_TYPE 18 | 19 | // TODO: TBD 20 | internal object ResponseHandler { 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | fun makeupMessageForRsp(message: Message?): T? { 24 | val serBundle: Bundle = message?.data ?: return null 25 | val remoteInvokeResult = serBundle.apply { classLoader = ResponseHandler.javaClass.classLoader }.run { 26 | when (getInt(MESSAGE_BUNDLE_RSP_TYPE_KEY)) { 27 | MESSAGE_BUNDLE_PARCELABLE_TYPE -> { 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 29 | getParcelable(MESSAGE_BUNDLE_NORMAL_RSP_KEY, Parcelable::class.java) 30 | } else { 31 | @Suppress("DEPRECATION") 32 | getParcelable(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 33 | } 34 | } 35 | 36 | MESSAGE_BUNDLE_ARRAYLIST_CHAR_SEQUENCE_TYPE -> { 37 | getCharSequenceArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 38 | } 39 | 40 | MESSAGE_BUNDLE_ARRAYLIST_INTEGER_TYPE -> { 41 | getIntegerArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 42 | } 43 | 44 | MESSAGE_BUNDLE_ARRAYLIST_STRING_TYPE -> { 45 | getStringArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 46 | } 47 | 48 | MESSAGE_BUNDLE_ARRAYLIST_PARCELABLE_TYPE -> { 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 50 | getParcelableArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY, Parcelable::class.java) 51 | } else { 52 | @Suppress("DEPRECATION") 53 | getParcelableArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 54 | } 55 | } 56 | 57 | MESSAGE_BUNDLE_BOOLEAN_TYPE -> { 58 | getBoolean(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 59 | } 60 | 61 | MESSAGE_BUNDLE_CHAR_TYPE -> { 62 | getChar(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 63 | } 64 | 65 | MESSAGE_BUNDLE_STRING_TYPE -> { 66 | getString(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 67 | } 68 | 69 | MESSAGE_BUNDLE_BYTE_TYPE -> { 70 | getByte(MESSAGE_BUNDLE_NORMAL_RSP_KEY) 71 | } 72 | 73 | else -> { 74 | null 75 | } 76 | } 77 | } 78 | return remoteInvokeResult as? T 79 | } 80 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/BaseConnectionService.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.HandlerThread 6 | import android.os.IBinder 7 | import android.os.Message 8 | import android.util.Log 9 | import com.softtanck.RaNotification 10 | import com.softtanck.model.RaCustomMessenger 11 | import com.softtanck.ramessageservice.engine.RaClientManager 12 | import com.softtanck.ramessageservice.engine.RaServerHandler 13 | import com.softtanck.ramessageservice.intercept.IRaResponseIntercept 14 | import com.softtanck.ramessageservice.intercept.RaDefaultInterceptImpl 15 | import com.softtanck.ramessageservice.intercept.RaUnbindInterceptImpl 16 | import com.softtanck.ramessageservice.intercept.RealInterceptorChain 17 | 18 | /** 19 | * @author Softtanck 20 | * @date 2022/3/12 21 | * Description: TODO 22 | * @param startInForeground is start in foreground, that is, show a notification 23 | */ 24 | abstract class BaseConnectionService(private val startInForeground: Boolean = true) : Service() { 25 | private val TAG: String = BaseConnectionService::class.java.simpleName 26 | private val workHandlerThread = HandlerThread("RaServiceWorkThread") 27 | 28 | // Add an interceptor chain for developers to add their own interceptors 29 | private val intercepts = mutableListOf().apply { 30 | add(RaUnbindInterceptImpl(serviceKey = this@BaseConnectionService.javaClass.name)) 31 | add(RaDefaultInterceptImpl()) 32 | } 33 | 34 | init { 35 | // Start the work's thread at first, And it is a single object! 36 | if (!workHandlerThread.isAlive) workHandlerThread.start() 37 | } 38 | 39 | fun onRemoteMessageArrived(message: Message, isSyncCall: Boolean): Message? { 40 | Log.d(TAG, "[SERVER] onRemoteMessageArrived: $message, what:${message.what} isSyncCall:$isSyncCall, trxID:${message.arg1}, thread:${Thread.currentThread()}") 41 | val response = RealInterceptorChain(intercepts, 0, this@BaseConnectionService).proceed(message, isSyncCall) 42 | return if (isSyncCall) { // If it is a sync call, return the response 43 | response 44 | } else { 45 | // The message will be changed in the interceptor chain, so we need to return the message directly 46 | sendMsgToClient(serviceKey = this@BaseConnectionService.javaClass.name, message = message) 47 | null 48 | } 49 | } 50 | 51 | private val customProcessHandler: RaServerHandler by lazy(LazyThreadSafetyMode.NONE) { RaServerHandler(workHandlerThread.looper, this) } 52 | 53 | override fun onCreate() { 54 | super.onCreate() 55 | Log.d(TAG, "[SERVER] onCreate") 56 | if (startInForeground) { // if start in foreground, show a notification 57 | startForeground(RaNotification.BASE_CONNECTION_SERVICE_NOTIFICATION_ID, RaNotification.getNotificationForInitSetup(this)) 58 | } 59 | } 60 | 61 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 62 | Log.d(TAG, "[SERVER] onStartCommand") 63 | return START_STICKY 64 | } 65 | 66 | override fun onBind(intent: Intent): IBinder? { 67 | val clientVersionCode = intent.getIntExtra(RaCustomMessenger.raMsgVersion.first, -1) 68 | Log.d(TAG, "[SERVER] onBind coming, the client version code is:$clientVersionCode") 69 | val tempCustomMessenger = customProcessHandler.innerMessenger 70 | // Return our custom Binder if that is supported in Clients. (IPC) 71 | return if (tempCustomMessenger.asBinder() != null && clientVersionCode != -1 && RaCustomMessenger.raMsgVersion.second >= clientVersionCode) { 72 | tempCustomMessenger.asBinder() 73 | } else { 74 | throw IllegalStateException("[SERVER] Failed to get the Messenger, Please check your handler") 75 | } 76 | } 77 | 78 | override fun onUnbind(intent: Intent?): Boolean { 79 | Log.d(TAG, "[SERVER] onUnbind, intent:$intent") 80 | return super.onUnbind(intent) 81 | } 82 | 83 | override fun onDestroy() { 84 | super.onDestroy() 85 | customProcessHandler.removeCallbacksAndMessages(null) 86 | if (workHandlerThread.isAlive) workHandlerThread.quitSafely() 87 | Log.d(TAG, "[SERVER] onDestroy") 88 | } 89 | 90 | override fun onTaskRemoved(rootIntent: Intent?) { 91 | super.onTaskRemoved(rootIntent) 92 | Log.d(TAG, "[SERVER] onTaskRemoved") 93 | } 94 | 95 | private fun sendMsgToClient(serviceKey: String?, message: Message) { 96 | RaClientManager.sendMsgToClient(serviceKey = serviceKey, message = message) 97 | } 98 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/RaServerApi.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice 2 | 3 | import android.os.Message 4 | import com.softtanck.MESSAGE_CLIENT_BROADCAST_RSP 5 | import com.softtanck.ramessageservice.engine.RaClientManager 6 | 7 | /** 8 | * @author Softtanck 9 | * @date 2022/3/28 10 | * Description: TODO 11 | */ 12 | class RaServerApi private constructor() { 13 | companion object { 14 | @JvmStatic 15 | val INSTANCE: RaServerApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 16 | RaServerApi() 17 | } 18 | } 19 | 20 | /** 21 | * Send a broadcast message to all clients 22 | * @param message a broadcast message 23 | */ 24 | fun sendBroadcastToAllClients(serviceKey: String?, message: Message) { 25 | RaClientManager.sendMsgToClient(serviceKey = serviceKey, message = message.apply { // Change the message type to MESSAGE_CLIENT_BROADCAST_RSP before send 26 | what = MESSAGE_CLIENT_BROADCAST_RSP 27 | }) 28 | } 29 | 30 | fun getAllRaClientServiceKeys() = RaClientManager.clients.map { it.serviceKey } 31 | 32 | // TODO : 获取对应客户端的Binder,然后通过remoteMethodCallAsync、remoteMethodCallSync等方法调用对应客户的的方法 33 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/engine/BaseServerSyncHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Softtanck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.ramessageservice.engine 17 | 18 | import android.os.Handler 19 | import android.os.Looper 20 | import android.os.Message 21 | import com.softtanck.ramessage.IRaMessenger 22 | import com.softtanck.ramessageservice.BaseConnectionService 23 | 24 | /** 25 | * @author Softtanck 26 | * @date 2022/3/12 27 | * Description: TODO 28 | */ 29 | internal open class BaseServerSyncHandler(looper: Looper, private val baseConnectionService: BaseConnectionService) : Handler(looper) { 30 | 31 | // IRaMessenger / IMessenger 32 | val innerMessenger: IRaMessenger.Stub = BaseSyncHandlerImpl() 33 | 34 | private inner class BaseSyncHandlerImpl : IRaMessenger.Stub() { 35 | override fun send(msg: Message) { 36 | msg.sendingUid = getCallingUid() 37 | this@BaseServerSyncHandler.sendMessage(msg) 38 | } 39 | 40 | override fun sendSync(msg: Message): Message { 41 | msg.sendingUid = getCallingUid() 42 | return baseConnectionService.onRemoteMessageArrived(msg, true) ?: Message.obtain(msg) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/engine/RaClientManager.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.engine 2 | 3 | import android.os.Build 4 | import android.os.Message 5 | import android.os.Parcelable 6 | import android.os.RemoteException 7 | import android.util.Log 8 | import com.softtanck.MESSAGE_BUNDLE_REPLY_TO_KEY 9 | import com.softtanck.MESSAGE_CLIENT_BROADCAST_RSP 10 | import com.softtanck.MESSAGE_CLIENT_SINGLE_RSP 11 | import com.softtanck.MESSAGE_REGISTER_CLIENT_RSP 12 | import com.softtanck.model.RaClient 13 | import com.softtanck.model.RaCustomMessenger 14 | import com.softtanck.sharedlib.BuildConfig 15 | 16 | /** 17 | * @author Softtanck 18 | * @date 2022/6/29 19 | * Description: An object to hold the binder of the client. 20 | */ 21 | internal object RaClientManager { 22 | private val TAG = RaClientManager::class.java.simpleName 23 | 24 | /** 25 | * All clients put into this set. 26 | */ 27 | val clients: LinkedHashSet> = LinkedHashSet() 28 | 29 | /** 30 | * Add current client into the clients map, then send the connection register state to the client. 31 | * @param serviceKey The service key of the client. like: com.softtanck.ramessageservice.RaConnectionService 32 | * @param msg The message from the clients. 33 | */ 34 | fun registerClientFromBinderWithMessage(serviceKey: String, msg: Message) { 35 | synchronized(clients) { 36 | Log.d(TAG, "[SERVER] RaServerHandler handleMessage: MESSAGE_REGISTER_CLIENT_REQ, msg.sendingUid:${msg.sendingUid}") 37 | val dataFromClient = msg.data.apply { classLoader = this@RaClientManager.javaClass.classLoader } 38 | val tempRaClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 39 | RaClient(clientUID = msg.sendingUid, clientMessenger = dataFromClient.getParcelable(MESSAGE_BUNDLE_REPLY_TO_KEY, Parcelable::class.java) as? RaCustomMessenger ?: msg.replyTo, serviceKey = serviceKey) 40 | } else { 41 | @Suppress("DEPRECATION") 42 | RaClient(clientUID = msg.sendingUid, clientMessenger = dataFromClient.getParcelable(MESSAGE_BUNDLE_REPLY_TO_KEY) as? RaCustomMessenger ?: msg.replyTo, serviceKey = serviceKey) 43 | } 44 | clients.add(tempRaClient) 45 | try { 46 | tempRaClient.sendAsyncMessageToClient(Message.obtain(null, MESSAGE_REGISTER_CLIENT_RSP)) 47 | } catch (e: Exception) { 48 | Log.e(TAG, "[SERVER] Exception occurs when send register state to client state: " + e.message, e) 49 | } 50 | Log.d(TAG, "[SERVER] Client is registered. No of active clients : ${clients.size}") 51 | } 52 | } 53 | 54 | /** 55 | * Remove the client from the clients map. 56 | * @param serviceKey The service key of the client. like: com.softtanck.ramessageservice.RaConnectionService 57 | * @param msg The message from the clients. 58 | */ 59 | fun removeClientFromBinderWithMessage(serviceKey: String, msg: Message) { 60 | synchronized(clients) { 61 | Log.d(TAG, "[SERVER] RaServerHandler handleMessage: MESSAGE_CLIENT_DISCONNECT_REQ, msg.sendingUid:${msg.sendingUid}") 62 | val iterator = clients.iterator() 63 | while (iterator.hasNext()) { 64 | val client = iterator.next() 65 | if (client.clientUID == msg.sendingUid && client.serviceKey == serviceKey) { 66 | iterator.remove() 67 | Log.d(TAG, "[SERVER] Client is removed. No of active clients : ${clients.size}") 68 | break 69 | } 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Send a message to client. 76 | * @param serviceKey The service key of the client. like: com.softtanck.ramessageservice.RaConnectionService 77 | * @param message The message to send. 78 | */ 79 | fun sendMsgToClient(serviceKey: String?, message: Message) { 80 | synchronized(clients) { 81 | val iterator = clients.iterator() 82 | while (iterator.hasNext()) { 83 | val client = iterator.next() 84 | Log.d(TAG, "[SERVER] sendMsgToClient(what:${message.what}): client.uid:${client.clientUID}, client.serviceKey:${client.serviceKey}, current serviceKey:$serviceKey") 85 | if (!serviceKey.isNullOrEmpty() && client.serviceKey != serviceKey) { 86 | Log.d(TAG, "[SERVER] sendMsgToClient(what:${message.what}): dropped, since client.serviceKey:${client.serviceKey} != current serviceKey:$serviceKey") 87 | continue 88 | } 89 | // Need to check the pid if the client is running in the same process. 90 | // and send the message to the client only if the client is running in the same process. 91 | // Also, the request type of message needs to checked (Single point and broadcast) 92 | if (message.what == MESSAGE_CLIENT_BROADCAST_RSP || message.sendingUid == client.clientUID) { 93 | val clientBinder = client.getClientBinder() 94 | if (clientBinder != null && clientBinder.isBinderAlive) { 95 | try { 96 | client.sendAsyncMessageToClient(Message.obtain(message).apply { 97 | if (message.what != MESSAGE_CLIENT_BROADCAST_RSP) { 98 | // Only not broadcast message need to change the message type to MESSAGE_CLIENT_SINGLE_RSP 99 | what = MESSAGE_CLIENT_SINGLE_RSP 100 | } 101 | }) 102 | if (BuildConfig.DEBUG) Log.d(TAG, "[SERVER] Sent msg(${message.what},$message) to Clients") 103 | } catch (e: RemoteException) { 104 | iterator.remove() 105 | // The client is dead. Remove it from the list; 106 | Log.d(TAG, "[SERVER] Removing inactive client. New client count is " + clients.size) 107 | } catch (ex: Exception) { 108 | ex.printStackTrace() 109 | Log.e(TAG, "[SERVER] Send failed for client : $client") 110 | iterator.remove() 111 | } 112 | } else { 113 | Log.d(TAG, "[SERVER] Removing inactive client(Binder Died). New client count is " + clients.size) 114 | iterator.remove() 115 | } 116 | } else { 117 | Log.d(TAG, "[SERVER] Current UID(${message.sendingUid}) not is ${client.clientUID} or type is not broadcast(${message.what}), Skip sending message to client") 118 | } 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/engine/RaServerHandler.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.engine 2 | 3 | import android.os.Looper 4 | import android.os.Message 5 | import android.util.Log 6 | import com.softtanck.MESSAGE_CLIENT_SINGLE_REQ 7 | import com.softtanck.MESSAGE_REGISTER_CLIENT_REQ 8 | import com.softtanck.ramessageservice.BaseConnectionService 9 | 10 | internal class RaServerHandler internal constructor(looper: Looper, private val baseConnectionService: BaseConnectionService) : BaseServerSyncHandler(looper, baseConnectionService) { 11 | private val TAG = this.javaClass.name 12 | 13 | override fun handleMessage(msg: Message) { 14 | Log.d(TAG, "[SERVER] RaServerHandler handleMessage: ${msg.what}") 15 | when (msg.what) { 16 | MESSAGE_REGISTER_CLIENT_REQ -> { 17 | RaClientManager.registerClientFromBinderWithMessage(serviceKey = baseConnectionService.javaClass.name, msg = msg) 18 | } 19 | MESSAGE_CLIENT_SINGLE_REQ -> { 20 | baseConnectionService.onRemoteMessageArrived(msg, false) 21 | } 22 | else -> { 23 | Log.d(TAG, "[SERVER] Not in protocol, Discarding the message. msg:$msg") 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/intercept/IRaResponseIntercept.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.intercept 2 | 3 | import android.os.Message 4 | import com.softtanck.ramessageservice.model.RaChain 5 | 6 | /** 7 | * @author Softtanck 8 | * @date 2022/3/23 9 | * Description: TODO 10 | */ 11 | interface IRaResponseIntercept { 12 | 13 | /** 14 | * Intercept the response before it is handled by the chain. 15 | */ 16 | fun intercept(raChain: RaChain, message: Message, isSyncCall: Boolean): Message? 17 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/intercept/RaDefaultInterceptImpl.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.intercept 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.Message 6 | import android.os.Parcelable 7 | import android.text.TextUtils 8 | import android.util.Log 9 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_CHAR_SEQUENCE_TYPE 10 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_INTEGER_TYPE 11 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_PARCELABLE_TYPE 12 | import com.softtanck.MESSAGE_BUNDLE_ARRAYLIST_STRING_TYPE 13 | import com.softtanck.MESSAGE_BUNDLE_BOOLEAN_TYPE 14 | import com.softtanck.MESSAGE_BUNDLE_BYTE_TYPE 15 | import com.softtanck.MESSAGE_BUNDLE_CHAR_TYPE 16 | import com.softtanck.MESSAGE_BUNDLE_METHOD_NAME_KEY 17 | import com.softtanck.MESSAGE_BUNDLE_NORMAL_RSP_KEY 18 | import com.softtanck.MESSAGE_BUNDLE_PARCELABLE_TYPE 19 | import com.softtanck.MESSAGE_BUNDLE_RSP_TYPE_KEY 20 | import com.softtanck.MESSAGE_BUNDLE_STRING_TYPE 21 | import com.softtanck.MESSAGE_BUNDLE_TYPE_ARG_KEY 22 | import com.softtanck.MESSAGE_BUNDLE_TYPE_PARAMETER_KEY 23 | import com.softtanck.model.RaRequestTypeArg 24 | import com.softtanck.model.RaRequestTypeParameter 25 | import com.softtanck.ramessageclient.core.engine.retrofit.invokeCompat 26 | import com.softtanck.ramessageservice.model.RaChain 27 | import com.softtanck.ramessageservice.util.ServerUtil 28 | 29 | /** 30 | * @author Softtanck 31 | * @date 2022/3/23 32 | * Description: TODO 33 | */ 34 | internal class RaDefaultInterceptImpl : IRaResponseIntercept { 35 | 36 | companion object { 37 | private const val TAG = "RaDefaultIntercept" 38 | } 39 | 40 | override fun intercept(raChain: RaChain, message: Message, isSyncCall: Boolean): Message? { 41 | try { 42 | val serBundle: Bundle? = message.data?.apply { classLoader = this@RaDefaultInterceptImpl.javaClass.classLoader } 43 | if (serBundle == null) { 44 | return raChain.proceed(message, isSyncCall) 45 | } else { 46 | val remoteMethodName = serBundle.getString(MESSAGE_BUNDLE_METHOD_NAME_KEY) 47 | if (TextUtils.isEmpty(remoteMethodName)) return raChain.proceed(message, isSyncCall) 48 | Log.d(TAG, "[SERVER] remoteMethodName:$remoteMethodName, thread:${Thread.currentThread()}") 49 | val requestParameters: ArrayList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 50 | serBundle.getParcelableArrayList(MESSAGE_BUNDLE_TYPE_PARAMETER_KEY, RaRequestTypeParameter::class.java) as ArrayList 51 | } else { 52 | @Suppress("DEPRECATION") 53 | serBundle.getParcelableArrayList(MESSAGE_BUNDLE_TYPE_PARAMETER_KEY) as ArrayList 54 | } 55 | val loadServiceMethod = ServerUtil.loadServiceMethod(remoteMethodName!!, requestParameters, raChain.baseConnectionService) 56 | val requestArgs: ArrayList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 57 | serBundle.getParcelableArrayList(MESSAGE_BUNDLE_TYPE_ARG_KEY, Parcelable::class.java) as ArrayList 58 | } else { 59 | @Suppress("DEPRECATION") 60 | serBundle.getParcelableArrayList(MESSAGE_BUNDLE_TYPE_ARG_KEY) as ArrayList 61 | } 62 | if (loadServiceMethod == null) { 63 | Log.e(TAG, "[SERVER] loadServiceMethod is null") 64 | return raChain.proceed(message, isSyncCall) 65 | } 66 | val remoteCallResult = loadServiceMethod.method.invokeCompat(raChain.baseConnectionService, *Array(requestArgs.size) { 67 | if (requestArgs[it] is RaRequestTypeArg) { 68 | (requestArgs[it] as RaRequestTypeArg).arg 69 | } else { 70 | requestArgs[it] 71 | } 72 | }) 73 | message.arg1 = message.arg1 // Remember the callback key 74 | message.data = Bundle().apply { 75 | when (remoteCallResult) { 76 | is Parcelable -> { 77 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_PARCELABLE_TYPE) 78 | putParcelable(MESSAGE_BUNDLE_NORMAL_RSP_KEY, remoteCallResult as Parcelable?) 79 | } 80 | 81 | is ArrayList<*> -> { 82 | val tempTypedStringArrayList = remoteCallResult.asListOfType() 83 | val tempTypedIntArrayList = remoteCallResult.asListOfType() 84 | val tempTypedParcelableArrayList = remoteCallResult.asListOfType() 85 | val tempTypedCharSequenceArrayList = remoteCallResult.asListOfType() 86 | when { 87 | tempTypedStringArrayList != null -> { 88 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_ARRAYLIST_STRING_TYPE) 89 | putStringArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY, tempTypedStringArrayList) 90 | } 91 | 92 | tempTypedIntArrayList != null -> { 93 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_ARRAYLIST_INTEGER_TYPE) 94 | putIntegerArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY, tempTypedIntArrayList) 95 | } 96 | 97 | tempTypedParcelableArrayList != null -> { 98 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_ARRAYLIST_PARCELABLE_TYPE) 99 | putParcelableArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY, tempTypedParcelableArrayList) 100 | } 101 | 102 | tempTypedCharSequenceArrayList != null -> { 103 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_ARRAYLIST_CHAR_SEQUENCE_TYPE) 104 | putCharSequenceArrayList(MESSAGE_BUNDLE_NORMAL_RSP_KEY, tempTypedCharSequenceArrayList) 105 | } 106 | } 107 | } 108 | 109 | is Boolean -> { 110 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_BOOLEAN_TYPE) 111 | putBoolean(MESSAGE_BUNDLE_NORMAL_RSP_KEY, remoteCallResult) 112 | } 113 | 114 | is Char -> { 115 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_CHAR_TYPE) 116 | putChar(MESSAGE_BUNDLE_NORMAL_RSP_KEY, remoteCallResult) 117 | } 118 | 119 | is String -> { 120 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_STRING_TYPE) 121 | putString(MESSAGE_BUNDLE_NORMAL_RSP_KEY, remoteCallResult) 122 | } 123 | 124 | is Byte -> { 125 | putInt(MESSAGE_BUNDLE_RSP_TYPE_KEY, MESSAGE_BUNDLE_BYTE_TYPE) 126 | putByte(MESSAGE_BUNDLE_NORMAL_RSP_KEY, remoteCallResult) 127 | } 128 | } 129 | } 130 | } 131 | } catch (e: Exception) { 132 | e.printStackTrace() 133 | Log.e(TAG, "[SERVER] Remote call failed:${e.message}") 134 | return null 135 | } 136 | return raChain.proceed(message, isSyncCall) 137 | } 138 | 139 | // https://kotlinlang.org/docs/typecasts.html#unchecked-casts 140 | private inline fun ArrayList<*>.asListOfType(): ArrayList? = 141 | if (all { it is T }) { 142 | @Suppress("UNCHECKED_CAST") 143 | this as ArrayList 144 | } else { 145 | null 146 | } 147 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/intercept/RaUnbindInterceptImpl.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.intercept 2 | 3 | import android.os.Message 4 | import android.util.Log 5 | import com.softtanck.MESSAGE_CLIENT_DISCONNECT_REQ 6 | import com.softtanck.ramessageservice.engine.RaClientManager 7 | import com.softtanck.ramessageservice.model.RaChain 8 | 9 | /** 10 | * @author Softtanck 11 | * @date 2023/11/21 12 | * Description: to handle the unbind event 13 | * @param serviceKey the service key for the client 14 | */ 15 | internal class RaUnbindInterceptImpl(private val serviceKey: String) : IRaResponseIntercept { 16 | companion object { 17 | private const val TAG = "RaUnbindIntercept" 18 | } 19 | 20 | override fun intercept(raChain: RaChain, message: Message, isSyncCall: Boolean): Message? { 21 | return runCatching { 22 | if (message.what == MESSAGE_CLIENT_DISCONNECT_REQ) { 23 | Log.d(TAG, "[SERVER] onDisconnect coming: $message, what:${message.what} isSyncCall:$isSyncCall, trxID:${message.arg1}, serviceKey:${serviceKey}, thread:${Thread.currentThread()}") 24 | RaClientManager.removeClientFromBinderWithMessage(msg = message, serviceKey = serviceKey) 25 | null 26 | } else { 27 | raChain.proceed(message, isSyncCall) 28 | } 29 | }.getOrNull() 30 | } 31 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/intercept/RealInterceptorChain.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.intercept 2 | 3 | import android.os.Message 4 | import com.softtanck.ramessageservice.BaseConnectionService 5 | import com.softtanck.ramessageservice.model.RaChain 6 | 7 | /** 8 | * @author Softtanck 9 | * @date 2022/3/23 10 | * Description: TODO 11 | */ 12 | internal class RealInterceptorChain(private val interceptors: List, private val targetIndex: Int, baseConnectionService: BaseConnectionService) : RaChain(baseConnectionService) { 13 | override fun proceed(message: Message, isSyncCall: Boolean): Message? { 14 | interceptors.size.let { 15 | if (targetIndex >= it) { 16 | return message 17 | } else if (targetIndex < 0) { 18 | return message 19 | } 20 | val interceptor = interceptors[targetIndex] 21 | val realInterceptorChain = RealInterceptorChain(interceptors, targetIndex + 1, baseConnectionService) 22 | return interceptor.intercept(realInterceptorChain, message, isSyncCall) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/model/RaChain.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.model 2 | 3 | import android.os.Message 4 | import com.softtanck.ramessageservice.BaseConnectionService 5 | 6 | /** 7 | * @author Softtanck 8 | * @date 2022/3/23 9 | * Description: TODO 10 | */ 11 | abstract class RaChain(val baseConnectionService: BaseConnectionService) { 12 | 13 | abstract fun proceed(message: Message, isSyncCall: Boolean): Message? 14 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/model/RaRemoteMethod.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.model 2 | 3 | import com.softtanck.model.RaRequestTypeParameter 4 | import java.lang.reflect.Method 5 | 6 | /** 7 | * @author Softtanck 8 | * @date 2022/3/23 9 | * Description: TODO 10 | */ 11 | internal data class RaRemoteMethod(val methodName: String, val methodRequestParams: ArrayList, val method: Method) -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/ramessageservice/util/ServerUtil.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessageservice.util 2 | 3 | import android.util.Log 4 | import com.softtanck.model.RaRequestTypeParameter 5 | import com.softtanck.ramessageservice.BaseConnectionService 6 | import com.softtanck.ramessageservice.model.RaRemoteMethod 7 | import com.softtanck.sharedlib.BuildConfig 8 | import com.softtanck.util.Utils 9 | import java.lang.reflect.Method 10 | import java.util.concurrent.ConcurrentHashMap 11 | import kotlin.coroutines.Continuation 12 | 13 | /** 14 | * @author Softtanck 15 | * @date 2022/3/23 16 | * Description: TODO 17 | */ 18 | internal object ServerUtil { 19 | 20 | private val serviceMethodCache: MutableMap> = ConcurrentHashMap() 21 | 22 | private const val TAG = "ServerUtil" 23 | 24 | fun loadServiceMethod(remoteMethodName: String, requestParameters: ArrayList, baseConnectionService: BaseConnectionService): RaRemoteMethod? { 25 | val targetServiceKey = baseConnectionService.javaClass.name 26 | var result = serviceMethodCache[targetServiceKey]?.get(remoteMethodName) 27 | val tempRequestParameters = result?.methodRequestParams 28 | if (result != null && tempRequestParameters != null && isEqual(requestParameters, tempRequestParameters)) { 29 | if (BuildConfig.DEBUG) Log.d(TAG, "loadServiceMethod: Use the cached methods") 30 | return result 31 | } 32 | synchronized(serviceMethodCache) { 33 | result = serviceMethodCache[targetServiceKey]?.get(remoteMethodName) 34 | if (result == null) { 35 | try { 36 | val remoteMethod: Method = baseConnectionService.javaClass.getDeclaredMethod(remoteMethodName, *Array(requestParameters.size) { requestParameters[it].parameterTypeClasses }) 37 | remoteMethod.isAccessible = true 38 | result = RaRemoteMethod(remoteMethodName, requestParameters, remoteMethod) 39 | serviceMethodCache[targetServiceKey]?.set(remoteMethodName, result as RaRemoteMethod) 40 | } catch (e: NoSuchMethodException) { 41 | e.printStackTrace() 42 | Log.e(TAG, "loadServiceMethod: remoteMethodName failed to found") 43 | } 44 | } 45 | } 46 | return result 47 | } 48 | 49 | /** 50 | * Check current method is suspend method 51 | * @param targetMethod the checked method 52 | * @return true if the method is suspend method 53 | */ 54 | fun isSuspendMethod(targetMethod: Method): Boolean { 55 | for (parameterType in targetMethod.genericParameterTypes) { 56 | try { 57 | if (Utils.getRawType(parameterType) == Continuation::class.java) { 58 | return true 59 | } 60 | } catch (ignored: NoClassDefFoundError) { 61 | // Ignored 62 | } 63 | } 64 | return false 65 | } 66 | 67 | private fun isEqual(first: List, second: List): Boolean { 68 | if (first.size != second.size) { 69 | return false 70 | } 71 | return first.zip(second).all { (x, y) -> x == y } 72 | } 73 | } -------------------------------------------------------------------------------- /SharedLib/src/main/java/com/softtanck/util/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.softtanck.util; 17 | 18 | import java.lang.reflect.Array; 19 | import java.lang.reflect.GenericArrayType; 20 | import java.lang.reflect.Method; 21 | import java.lang.reflect.ParameterizedType; 22 | import java.lang.reflect.Type; 23 | import java.lang.reflect.TypeVariable; 24 | import java.lang.reflect.WildcardType; 25 | import java.util.Objects; 26 | 27 | import kotlin.Unit; 28 | 29 | public final class Utils { 30 | 31 | private Utils() { 32 | // No instances. 33 | } 34 | 35 | public static Class getRawType(Type type) { 36 | Objects.requireNonNull(type, "type == null"); 37 | 38 | if (type instanceof Class) { 39 | // Type is a normal class. 40 | return (Class) type; 41 | } 42 | if (type instanceof ParameterizedType) { 43 | ParameterizedType parameterizedType = (ParameterizedType) type; 44 | 45 | // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but 46 | // suspects some pathological case related to nested classes exists. 47 | Type rawType = parameterizedType.getRawType(); 48 | if (!(rawType instanceof Class)) throw new IllegalArgumentException(); 49 | return (Class) rawType; 50 | } 51 | if (type instanceof GenericArrayType) { 52 | Type componentType = ((GenericArrayType) type).getGenericComponentType(); 53 | return Array.newInstance(getRawType(componentType), 0).getClass(); 54 | } 55 | if (type instanceof TypeVariable) { 56 | // We could use the variable's bounds, but that won't work if there are multiple. Having a raw 57 | // type that's more general than necessary is okay. 58 | return Object.class; 59 | } 60 | if (type instanceof WildcardType) { 61 | return getRawType(((WildcardType) type).getUpperBounds()[0]); 62 | } 63 | 64 | throw new IllegalArgumentException( 65 | "Expected a Class, ParameterizedType, or " 66 | + "GenericArrayType, but <" 67 | + type 68 | + "> is of type " 69 | + type.getClass().getName()); 70 | } 71 | 72 | public static boolean hasReturnValueType(Method method, Type type) { 73 | return !method.getGenericReturnType().equals(Void.TYPE) && Utils.getRawType(type) != Unit.class; 74 | } 75 | 76 | public static Type getParameterLowerBound(int index, ParameterizedType type) { 77 | Type paramType = type.getActualTypeArguments()[index]; 78 | if (paramType instanceof WildcardType) { 79 | return ((WildcardType) paramType).getLowerBounds()[0]; 80 | } 81 | return paramType; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-parcelize' 4 | id 'kotlin-android' 5 | } 6 | 7 | android { 8 | defaultConfig { 9 | applicationId "com.softtanck.ramessage" 10 | minSdkVersion 23 11 | compileSdk 34 12 | buildToolsVersion = "34.0.0" 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_17 21 | targetCompatibility JavaVersion.VERSION_17 22 | } 23 | kotlinOptions { 24 | jvmTarget = '17' 25 | } 26 | 27 | buildTypes { 28 | debug { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | namespace 'com.softtanck.ramessage' 38 | } 39 | 40 | dependencies { 41 | implementation 'androidx.core:core-ktx:1.12.0' 42 | implementation 'androidx.appcompat:appcompat:1.6.1' 43 | implementation 'com.google.android.material:material:1.10.0' 44 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" 45 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" 46 | implementation project(path: ':SharedLib') 47 | // androidx.test 48 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 49 | // org.junit 50 | testImplementation 'junit:junit:4.13.2' 51 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/softtanck/ramessage/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessage 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.softtanck.ramessage", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/shared/model/Food.kt: -------------------------------------------------------------------------------- 1 | package com.shared.model 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Keep 8 | @Parcelize 9 | data class Food( 10 | val name: String 11 | ) : Parcelable { 12 | 13 | override fun toString(): String { 14 | return "name:$name" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/softtanck/ramessage/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.softtanck.ramessage 2 | 3 | import android.content.ComponentName 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.widget.Button 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.lifecycleScope 9 | import com.shared.model.Food 10 | import com.softtanck.ramessage.ipc.RaTestInterface 11 | import com.softtanck.ramessageclient.RaClientApi 12 | import com.softtanck.ramessageclient.core.listener.BindStatusChangedListener 13 | import com.softtanck.ramessageclient.core.listener.DisconnectedReason 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.launch 16 | import kotlin.system.exitProcess 17 | 18 | class MainActivity : AppCompatActivity() { 19 | 20 | private val componentName = ComponentName("com.softtanck.ramessageservice", "com.softtanck.ramessageservice.RaConnectionService") 21 | private val componentNameV2 = ComponentName("com.softtanck.ramessageservice", "com.softtanck.ramessageservice.RaConnectionServiceV2") 22 | 23 | companion object { 24 | private const val TAG = "MainActivity" 25 | } 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | setContentView(R.layout.activity_main) 30 | 31 | findViewById