Skip to content

Commit

Permalink
Add Live Update & fallback (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlepinski authored Oct 1, 2024
1 parent 3a0ca80 commit d4f95d3
Show file tree
Hide file tree
Showing 25 changed files with 734 additions and 183 deletions.
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ Airship_minSdkVersion=21
Airship_targetSdkVersion=34
Airship_compileSdkVersion=34
Airship_ndkversion=26.1.10909125
Airship_airshipProxyVersion=8.3.0
Airship_airshipProxyVersion=9.1.3
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.urbanairship.UAirship
* Register the extender fully qualified class name in the manifest under the key
* `com.urbanairship.reactnative.AIRSHIP_EXTENDER`.
*/
interface AirshipExtender {
@Deprecated("Use com.urbanairship.android.framework.proxy.AirshipPluginExtender instead and register it under the manifest key `com.urbanairship.plugin.extender`")
interface AirshipExtender {
fun onAirshipReady(context: Context, airship: UAirship)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import com.urbanairship.PendingResult
import com.urbanairship.actions.ActionResult
import com.urbanairship.actions.ActionValue
import com.urbanairship.android.framework.proxy.EventType
import com.urbanairship.android.framework.proxy.NotificationConfig
import com.urbanairship.android.framework.proxy.ProxyLogger
import com.urbanairship.android.framework.proxy.events.EventEmitter
import com.urbanairship.android.framework.proxy.proxies.AirshipProxy
import com.urbanairship.android.framework.proxy.proxies.EnableUserNotificationsArgs
import com.urbanairship.android.framework.proxy.proxies.FeatureFlagProxy
import com.urbanairship.android.framework.proxy.proxies.LiveUpdateRequest
import com.urbanairship.android.framework.proxy.proxies.SuspendingPredicate
import com.urbanairship.json.JsonMap
import com.urbanairship.json.JsonSerializable
Expand Down Expand Up @@ -238,9 +241,12 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) :
}

@ReactMethod
override fun pushEnableUserNotifications(promise: Promise) {
override fun pushEnableUserNotifications(options: ReadableMap?, promise: Promise) {
promise.resolveSuspending(scope) {
proxy.push.enableUserPushNotifications()
val args = options?.let {
EnableUserNotificationsArgs.fromJson(Utils.convertMap(it).toJsonValue())
}
proxy.push.enableUserPushNotifications(args = args)
}
}

Expand Down Expand Up @@ -693,6 +699,12 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) :
}
}

override fun liveActivityListAll(promise: Promise) {
promise.resolveResult {
throw IllegalStateException("Not supported on Android")
}
}

override fun liveActivityList(request: ReadableMap?, promise: Promise) {
promise.resolveResult {
throw IllegalStateException("Not supported on Android")
Expand All @@ -717,6 +729,48 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) :
}
}

override fun liveUpdateListAll(promise: Promise) {
promise.resolveSuspending(scope) {
proxy.liveUpdateManager.listAll().let {
JsonValue.wrapOpt(it)
}
}
}

override fun liveUpdateList(request: ReadableMap?, promise: Promise) {
promise.resolveSuspending(scope) {
proxy.liveUpdateManager.list(
LiveUpdateRequest.List.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue())
).let {
JsonValue.wrapOpt(it)
}
}
}

override fun liveUpdateCreate(request: ReadableMap?, promise: Promise) {
promise.resolveSuspending(scope) {
proxy.liveUpdateManager.create(
LiveUpdateRequest.Create.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue())
)
}
}

override fun liveUpdateUpdate(request: ReadableMap?, promise: Promise) {
promise.resolveSuspending(scope) {
proxy.liveUpdateManager.update(
LiveUpdateRequest.Update.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue())
)
}
}

override fun liveUpdateEnd(request: ReadableMap?, promise: Promise) {
promise.resolveSuspending(scope) {
proxy.liveUpdateManager.end(
LiveUpdateRequest.End.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue())
)
}
}

private fun notifyPending() {
if (context.hasActiveReactInstance()) {
val appEventEmitter = context.getJSModule(RCTNativeAppEventEmitter::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import com.urbanairship.embedded.AirshipEmbeddedInfo
import com.urbanairship.embedded.AirshipEmbeddedObserver
import com.urbanairship.json.JsonMap
import com.urbanairship.json.jsonMapOf
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

Expand All @@ -26,33 +28,32 @@ import kotlinx.coroutines.launch
*/
class ReactAutopilot : BaseAutopilot() {

override fun onAirshipReady(airship: UAirship) {
super.onAirshipReady(airship)
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

override fun onReady(context: Context, airship: UAirship) {
ProxyLogger.info("Airship React Native version: %s, SDK version: %s", BuildConfig.AIRSHIP_MODULE_VERSION, UAirship.getVersion())

val context = UAirship.getApplicationContext()

MainScope().launch {
scope.launch {
EventEmitter.shared().pendingEventListener
.filter { !it.type.isForeground() }
.collect {
AirshipHeadlessEventService.startService(context)
}
}

MainScope().launch {
scope.launch {
AirshipEmbeddedObserver(filter = { true }).embeddedViewInfoFlow.collect {
EventEmitter.shared().addEvent(PendingEmbeddedUpdated(it))
}
}

// Set our custom notification providerr
// Set our custom notification provider
val notificationProvider = ReactNotificationProvider(context, airship.airshipConfigOptions)
airship.pushManager.notificationProvider = notificationProvider

airship.analytics.registerSDKExtension(Extension.REACT_NATIVE, BuildConfig.AIRSHIP_MODULE_VERSION)

// Legacy extender
val extender = createExtender(context)
extender?.onAirshipReady(context, airship)
}
Expand All @@ -61,6 +62,7 @@ class ReactAutopilot : BaseAutopilot() {
DataMigrator(context).migrateData(proxyStore)
}

@Suppress("deprecation")
private fun createExtender(context: Context): AirshipExtender? {
val ai: ApplicationInfo
try {
Expand All @@ -77,7 +79,7 @@ class ReactAutopilot : BaseAutopilot() {

try {
val extenderClass = Class.forName(classname)
return extenderClass.newInstance() as AirshipExtender
return extenderClass.getDeclaredConstructor().newInstance() as AirshipExtender
} catch (e: Exception) {
ProxyLogger.error(e, "Unable to create extender: $classname")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun pushEnableUserNotifications(promise: Promise)
abstract fun pushEnableUserNotifications(options: ReadableMap?, promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
Expand Down Expand Up @@ -405,6 +405,10 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext
@com.facebook.proguard.annotations.DoNotStrip
abstract fun featureFlagManagerTrackInteraction(flag: ReadableMap?, promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveActivityListAll(promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveActivityList(request: ReadableMap?, promise: Promise)
Expand All @@ -420,6 +424,24 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext
@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveActivityEnd(request: ReadableMap?, promise: Promise)
}

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveUpdateListAll(promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveUpdateList(request: ReadableMap?, promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveUpdateCreate(request: ReadableMap?, promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveUpdateUpdate(request: ReadableMap?, promise: Promise)

@ReactMethod
@com.facebook.proguard.annotations.DoNotStrip
abstract fun liveUpdateEnd(request: ReadableMap?, promise: Promise)
}
39 changes: 22 additions & 17 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">

<meta-data
android:name="com.urbanairship.plugin.extender"
android:value="com.urbanairship.sample.AirshipExtender" />

<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.urbanairship.sample

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.Keep
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.urbanairship.UAirship
import com.urbanairship.android.framework.proxy.AirshipPluginExtender
import com.urbanairship.json.requireField
import com.urbanairship.liveupdate.LiveUpdate
import com.urbanairship.liveupdate.LiveUpdateEvent
import com.urbanairship.liveupdate.LiveUpdateManager
import com.urbanairship.liveupdate.LiveUpdateResult
import com.urbanairship.liveupdate.SuspendLiveUpdateNotificationHandler


@Keep
public final class AirshipExtender: AirshipPluginExtender {
override fun onAirshipReady(context: Context, airship: UAirship) {
LiveUpdateManager.shared().register("Example", ExampleLiveUpdateHandler())
}
}

public final class ExampleLiveUpdateHandler: SuspendLiveUpdateNotificationHandler() {
override suspend fun onUpdate(
context: Context,
event: LiveUpdateEvent,
update: LiveUpdate
): LiveUpdateResult<NotificationCompat.Builder> {

if (event == LiveUpdateEvent.END) {
// Dismiss the live update on END. The default behavior will leave the Live Update
// in the notification tray until the dismissal time is reached or the user dismisses it.
return LiveUpdateResult.cancel()
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("emoji-example", "Emoji example", importance)
channel.description = "Emoji example"
NotificationManagerCompat.from(context).createNotificationChannel(channel)
}

val launchIntent = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.addCategory(update.name)
?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
?.setPackage(null)

val contentIntent = PendingIntent.getActivity(
context, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(context, "emoji-example")
.setSmallIcon(R.drawable.ic_notification)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setContentTitle("Example Live Update")
.setContentText(update.content.requireField<String>("emoji"))
.setContentIntent(contentIntent)

return LiveUpdateResult.ok(notification)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"
android:fillColor="#FF000000"/>
</group>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ PODS:
- Airship/Core
- Airship/PreferenceCenter (18.9.2):
- Airship/Core
- AirshipFrameworkProxy (8.3.0):
- AirshipFrameworkProxy (9.1.3):
- Airship (= 18.9.2)
- AirshipServiceExtension (18.9.2)
- boost (1.83.0)
Expand Down Expand Up @@ -908,7 +908,7 @@ PODS:
- glog
- React-debug
- react-native-airship (19.3.2):
- AirshipFrameworkProxy (= 8.3.0)
- AirshipFrameworkProxy (= 9.1.3)
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1280,7 +1280,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Airship: 7f891aa9bb142d02f35aaef5ebdb09c2b5730a6d
AirshipFrameworkProxy: 48208d21ca1376d9bc111efd31981af84bfcf8f3
AirshipFrameworkProxy: 9a983b72a47ce10d8eda32b446ea553ef7bcc8f2
AirshipServiceExtension: 0ed795b521a76f8391e13896fbe1dee6ce9196ca
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
Expand Down Expand Up @@ -1311,7 +1311,7 @@ SPEC CHECKSUMS:
React-jsinspector: 9ac353eccf6ab54d1e0a33862ba91221d1e88460
React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab
React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad
react-native-airship: c68835c32f7b5a100b25bf5f2ecc6e4fae6ddce4
react-native-airship: 6afeeef72fa57a06a716afaca0ababb8adfaee81
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c
React-NativeModulesApple: 0123905d5699853ac68519607555a9a4f5c7b3ac
Expand Down
Loading

0 comments on commit d4f95d3

Please sign in to comment.