From 12c009f11830a4e22ebaeccf30e5cacccf59ba67 Mon Sep 17 00:00:00 2001 From: Josh Yaganeh <319444+jyaganeh@users.noreply.github.com> Date: Mon, 22 May 2023 11:04:39 -0700 Subject: [PATCH] Merge feature/live-update -> main (#1249) --- .gitignore | 1 + .idea/kotlinc.xml | 6 - gradle/libs.versions.toml | 1 + sample/build.gradle | 3 + .../urbanairship/sample/CustomLiveUpdate.java | 37 ++ .../urbanairship/sample/SampleAutopilot.java | 19 + .../urbanairship/sample/SampleLiveUpdate.java | 70 +++ .../sample/home/HomeFragment.java | 70 +++ sample/src/main/res/drawable/foxes.xml | 21 + sample/src/main/res/drawable/tigers.xml | 18 + sample/src/main/res/layout/fragment_home.xml | 66 +++ .../layout/live_update_notification_big.xml | 84 ++++ .../layout/live_update_notification_small.xml | 83 ++++ sample/src/main/res/values/strings.xml | 5 + settings.gradle | 1 + .../urbanairship/AirshipComponentGroups.java | 3 +- .../main/java/com/urbanairship/UAirship.java | 4 + .../com/urbanairship/job/JobDispatcher.java | 8 +- .../com/urbanairship/modules/Modules.java | 21 + .../liveupdate/LiveUpdateModuleFactory.java | 32 ++ .../com/urbanairship/push/PushMessage.java | 16 + urbanairship-live-update/build.gradle | 42 ++ urbanairship-live-update/proguard-rules.pro | 21 + .../1.json | 90 ++++ .../src/main/AndroidManifest.xml | 8 + .../liveupdate/AirshipDispatchers.kt | 25 + .../com/urbanairship/liveupdate/LiveUpdate.kt | 51 +++ .../liveupdate/LiveUpdateEvent.kt | 23 + .../liveupdate/LiveUpdateHandler.kt | 73 +++ .../liveupdate/LiveUpdateManager.kt | 219 +++++++++ .../liveupdate/LiveUpdateModuleFactoryImpl.kt | 46 ++ .../liveupdate/LiveUpdateProcessor.kt | 288 ++++++++++++ .../liveupdate/LiveUpdateRegistrar.kt | 248 ++++++++++ .../api/ChannelBulkUpdateApiClient.kt | 164 +++++++ .../liveupdate/data/Converters.kt | 20 + .../liveupdate/data/LiveUpdateDao.kt | 87 ++++ .../liveupdate/data/LiveUpdateDatabase.kt | 43 ++ .../liveupdate/data/LiveUpdateEntities.kt | 42 ++ .../LiveUpdateNotificationReceiver.kt | 73 +++ .../notification/LiveUpdatePayload.kt | 66 +++ .../notification/NotificationTimeoutCompat.kt | 48 ++ .../liveupdate/util/JsonExtensions.kt | 69 +++ .../liveupdate/LiveUpdateManagerTest.kt | 66 +++ .../liveupdate/LiveUpdateProcessorTest.kt | 431 ++++++++++++++++++ .../liveupdate/LiveUpdateRegistrarTest.kt | 127 ++++++ .../liveupdate/LiveUpdateStressTest.kt | 158 +++++++ .../urbanairship/liveupdate/TestHandler.kt | 21 + .../api/ChannelBulkUpdateApiClientTest.kt | 125 +++++ .../liveupdate/data/LiveUpdateDaoTest.kt | 123 +++++ .../src/test/resources/robolectric.properties | 2 + urbanairship-preference-center/build.gradle | 5 +- 51 files changed, 3363 insertions(+), 10 deletions(-) delete mode 100644 .idea/kotlinc.xml create mode 100644 sample/src/main/java/com/urbanairship/sample/CustomLiveUpdate.java create mode 100644 sample/src/main/java/com/urbanairship/sample/SampleLiveUpdate.java create mode 100644 sample/src/main/res/drawable/foxes.xml create mode 100644 sample/src/main/res/drawable/tigers.xml create mode 100644 sample/src/main/res/layout/live_update_notification_big.xml create mode 100644 sample/src/main/res/layout/live_update_notification_small.xml create mode 100644 urbanairship-core/src/main/java/com/urbanairship/modules/liveupdate/LiveUpdateModuleFactory.java create mode 100644 urbanairship-live-update/build.gradle create mode 100644 urbanairship-live-update/proguard-rules.pro create mode 100644 urbanairship-live-update/schemas/com.urbanairship.liveupdate.data.LiveUpdateDatabase/1.json create mode 100644 urbanairship-live-update/src/main/AndroidManifest.xml create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/AirshipDispatchers.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdate.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateEvent.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateHandler.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateManager.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateModuleFactoryImpl.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateProcessor.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateRegistrar.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/api/ChannelBulkUpdateApiClient.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/Converters.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDao.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDatabase.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateEntities.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/notification/LiveUpdateNotificationReceiver.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/notification/LiveUpdatePayload.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/notification/NotificationTimeoutCompat.kt create mode 100644 urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/util/JsonExtensions.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateManagerTest.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateProcessorTest.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateRegistrarTest.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateStressTest.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/TestHandler.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/api/ChannelBulkUpdateApiClientTest.kt create mode 100644 urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/data/LiveUpdateDaoTest.kt create mode 100644 urbanairship-live-update/src/test/resources/robolectric.properties diff --git a/.gitignore b/.gitignore index 4eee2c99c..8e512bd43 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ captures/ .idea/misc.xml .idea/kotlinScripting.xml .idea/compiler.xml +.idea/kotlinc.xml # Android Studio 3 in .gitignore file. .idea/caches/build_file_checksums.ser diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 2cfd1d9b2..000000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4df44b4b..a0c9a3520 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,6 +120,7 @@ androidx-preferencektx = { module = "androidx.preference:preference-ktx", versio androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerview" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "androidx-room" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } diff --git a/sample/build.gradle b/sample/build.gradle index 85149b500..840734eab 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -67,6 +67,9 @@ dependencies { // Airship Automation (In-App) implementation project(':urbanairship-automation') + // Airship Live Updates + implementation project(':urbanairship-live-update') + // Airship location implementation project(':urbanairship-location') // Allows Airship location services to use Fused Location diff --git a/sample/src/main/java/com/urbanairship/sample/CustomLiveUpdate.java b/sample/src/main/java/com/urbanairship/sample/CustomLiveUpdate.java new file mode 100644 index 000000000..8203e05fa --- /dev/null +++ b/sample/src/main/java/com/urbanairship/sample/CustomLiveUpdate.java @@ -0,0 +1,37 @@ +package com.urbanairship.sample; + +import android.content.Context; +import android.util.Log; + +import com.urbanairship.json.JsonMap; +import com.urbanairship.liveupdate.LiveUpdate; +import com.urbanairship.liveupdate.LiveUpdateEvent; +import com.urbanairship.liveupdate.LiveUpdateCustomHandler; +import com.urbanairship.liveupdate.LiveUpdateResult; + +import org.jetbrains.annotations.NotNull; + +import androidx.annotation.NonNull; + +// TODO(live-update): Implement a custom live update handler to feed data to a widget. +public class CustomLiveUpdate implements LiveUpdateCustomHandler { + @Override + @NotNull + public LiveUpdateResult onUpdate(@NonNull Context context, @NonNull LiveUpdateEvent event, @NonNull LiveUpdate update) { + + Log.d("CustomLiveUpdate", "onUpdate: action=" + event + ", update=" + update); + + if (event == LiveUpdateEvent.END) { + // Dismiss the live update on STOP. The default behavior will leave the Live Update + // active until the dismissal time is reached. + return LiveUpdateResult.cancel(); + } + + JsonMap content = update.getContent(); + int teamOneScore = content.opt("team_one_score").getInt(0); + int teamTwoScore = content.opt("team_two_score").getInt(0); + String statusUpdate = content.opt("status_update").optString(); + + return LiveUpdateResult.ok(); + } +} diff --git a/sample/src/main/java/com/urbanairship/sample/SampleAutopilot.java b/sample/src/main/java/com/urbanairship/sample/SampleAutopilot.java index 01b4f89c7..f4b0e9576 100644 --- a/sample/src/main/java/com/urbanairship/sample/SampleAutopilot.java +++ b/sample/src/main/java/com/urbanairship/sample/SampleAutopilot.java @@ -10,10 +10,15 @@ import com.urbanairship.AirshipConfigOptions; import com.urbanairship.Autopilot; import com.urbanairship.UAirship; +import com.urbanairship.liveupdate.LiveUpdateManager; import com.urbanairship.messagecenter.MessageCenter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.NotificationChannelCompat; +import androidx.core.app.NotificationManagerCompat; + +import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH; /** * Autopilot that enables user notifications on first run. @@ -36,6 +41,20 @@ public void onAirshipReady(@NonNull UAirship airship) { airship.getPushManager().setUserNotificationsEnabled(true); } + // Create notification channel for Live Updates. + NotificationChannelCompat sportsChannel = + new NotificationChannelCompat.Builder("sports", IMPORTANCE_HIGH) + .setDescription("Live sports updates!") + .setName("Sports!") + .setVibrationEnabled(false) + .build(); + + Context context = UAirship.getApplicationContext(); + NotificationManagerCompat.from(context).createNotificationChannel(sportsChannel); + + // Register handlers for Live Updates. + LiveUpdateManager.shared().register("sports", new SampleLiveUpdate()); + MessageCenter.shared().setOnShowMessageCenterListener(messageId -> { // Use an implicit navigation deep link for now as explicit deep links are broken // with multi navigation host fragments diff --git a/sample/src/main/java/com/urbanairship/sample/SampleLiveUpdate.java b/sample/src/main/java/com/urbanairship/sample/SampleLiveUpdate.java new file mode 100644 index 000000000..3ceb7e87d --- /dev/null +++ b/sample/src/main/java/com/urbanairship/sample/SampleLiveUpdate.java @@ -0,0 +1,70 @@ +package com.urbanairship.sample; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; + +import com.urbanairship.Logger; +import com.urbanairship.json.JsonMap; +import com.urbanairship.liveupdate.LiveUpdate; +import com.urbanairship.liveupdate.LiveUpdateEvent; +import com.urbanairship.liveupdate.LiveUpdateNotificationHandler; +import com.urbanairship.liveupdate.LiveUpdateResult; +import com.urbanairship.util.PendingIntentCompat; + +import org.jetbrains.annotations.NotNull; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + + +public class SampleLiveUpdate implements LiveUpdateNotificationHandler { + @Override + @NotNull + public LiveUpdateResult onUpdate(@NonNull Context context, @NonNull LiveUpdateEvent event, @NonNull LiveUpdate update) { + + Logger.debug("SampleLiveUpdate - onUpdate: action=" + event + ", update=" + update); + + if (event == LiveUpdateEvent.END) { + // Dismiss the live update on STOP. 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(); + } + + JsonMap content = update.getContent(); + int teamOneScore = content.opt("team_one_score").getInt(0); + int teamTwoScore = content.opt("team_two_score").getInt(0); + String statusUpdate = content.opt("status_update").optString(); + + RemoteViews bigLayout = new RemoteViews(context.getPackageName(), R.layout.live_update_notification_big); + bigLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); + bigLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); + bigLayout.setTextViewText(R.id.statusUpdate, statusUpdate); + + RemoteViews smallLayout = new RemoteViews(context.getPackageName(), R.layout.live_update_notification_small); + smallLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); + smallLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); + + Intent launchIntent = context.getPackageManager() + .getLaunchIntentForPackage(context.getPackageName()) + .addCategory(update.getName()) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP) + .setPackage(null); + + PendingIntent contentIntent = PendingIntentCompat.getActivity( + context, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, "sports") + .setSmallIcon(R.drawable.ic_notification) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_EVENT) + .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(smallLayout) + .setCustomBigContentView(bigLayout) + .setContentIntent(contentIntent); + + return LiveUpdateResult.ok(builder); + } +} diff --git a/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java b/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java index 81bf4b895..89c9c0280 100644 --- a/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java +++ b/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java @@ -10,9 +10,13 @@ import com.urbanairship.actions.ActionRunRequest; import com.urbanairship.actions.ClipboardAction; +import com.urbanairship.json.JsonMap; +import com.urbanairship.liveupdate.LiveUpdateManager; import com.urbanairship.sample.R; import com.urbanairship.sample.databinding.FragmentHomeBinding; +import java.util.concurrent.atomic.AtomicInteger; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; @@ -42,6 +46,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); }); + setupLiveUpdateTestButtons(binding); + return binding.getRoot(); } @@ -52,4 +58,68 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat NavigationUI.setupWithNavController(toolbar, Navigation.findNavController(view)); } + // TODO: Replace with something less hacky when backend is ready to send real Live Updates. + // Should live update stuff even be on the home screen? Could be in settings instead... + private void setupLiveUpdateTestButtons(FragmentHomeBinding binding) { + AtomicInteger score1 = new AtomicInteger(); + AtomicInteger score2 = new AtomicInteger(); + + // Start button + binding.startLiveUpdate.setOnClickListener(v -> { + JsonMap content = JsonMap.newBuilder() + .put("team_one_score", 0) + .put("team_two_score", 0) + .put("status_update", "Match start!") + .build(); + + LiveUpdateManager.shared().start("foxes-tigers", "sports", content); + }); + + // +1 Foxes button + binding.updateLiveUpdate1.setOnClickListener(v -> { + JsonMap content = JsonMap.newBuilder() + .put("team_one_score", score1.getAndIncrement()) + .put("team_two_score", score2.get()) + .put("status_update", "Foxes score!") + .build(); + + LiveUpdateManager.shared().update("foxes-tigers", content); + }); + + // +1 Tigers button + binding.updateLiveUpdate2.setOnClickListener(v -> { + JsonMap content = JsonMap.newBuilder() + .put("team_one_score", score1.get()) + .put("team_two_score", score2.getAndIncrement()) + .put("status_update", "Tigers score!") + .build(); + + LiveUpdateManager.shared().update("foxes-tigers", content); + }); + + // Stop button + binding.stopLiveUpdate.setOnClickListener(v -> { + int s1 = score1.get(); + int s2 = score2.get(); + String status; + if (s1 == s2) { + status = "It's a tie!"; + } else if (s1 > s2) { + status = "Foxes win!"; + } else { + status = "Tigers win!"; + } + + JsonMap content = JsonMap.newBuilder() + .put("teamOneScore", s1) + .put("team_two_score", s2) + .put("status_update", status) + .build(); + + LiveUpdateManager.shared().stop("foxes-tigers", content); + + score1.set(0); + score2.set(0); + }); + } } diff --git a/sample/src/main/res/drawable/foxes.xml b/sample/src/main/res/drawable/foxes.xml new file mode 100644 index 000000000..8a0f552c3 --- /dev/null +++ b/sample/src/main/res/drawable/foxes.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/sample/src/main/res/drawable/tigers.xml b/sample/src/main/res/drawable/tigers.xml new file mode 100644 index 000000000..f1f806dfb --- /dev/null +++ b/sample/src/main/res/drawable/tigers.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/sample/src/main/res/layout/fragment_home.xml b/sample/src/main/res/layout/fragment_home.xml index 88993ce4f..6c99da37d 100644 --- a/sample/src/main/res/layout/fragment_home.xml +++ b/sample/src/main/res/layout/fragment_home.xml @@ -57,6 +57,72 @@ tools:text="a61635e7-60c5-47e7-b9fb-870754e70a86" /> +