Skip to content

Commit bbf4694

Browse files
jyaganehrlepinski
andauthored
Async Live Update Handlers (#1330)
* Add async live update handler support * Update sports LU layout and existing handler * Add sample async live update handler * WIP * Format * Fix build --------- Co-authored-by: Ryan Lepinski <[email protected]>
1 parent 45839a8 commit bbf4694

File tree

13 files changed

+595
-42
lines changed

13 files changed

+595
-42
lines changed

.idea/codeStyles/Project.xml

-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sample/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ dependencies {
7777
// Airship Debug: Normally this is a debug only dependency but we embed the view in sample app
7878
implementation project(':urbanairship-debug')
7979

80+
implementation 'com.github.bumptech.glide:glide:4.16.0'
81+
8082
// Testing-only dependencies
8183

8284
// Espresso
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.urbanairship.sample;
2+
3+
import android.app.PendingIntent;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
import android.graphics.Bitmap;
7+
import android.os.Handler;
8+
import android.os.Looper;
9+
import android.util.Log;
10+
import android.widget.RemoteViews;
11+
12+
import com.bumptech.glide.Glide;
13+
import com.urbanairship.json.JsonMap;
14+
import com.urbanairship.liveupdate.CallbackLiveUpdateNotificationHandler;
15+
import com.urbanairship.liveupdate.LiveUpdate;
16+
import com.urbanairship.liveupdate.LiveUpdateEvent;
17+
18+
import java.util.concurrent.ExecutionException;
19+
import java.util.concurrent.ExecutorService;
20+
import java.util.concurrent.Executors;
21+
22+
import androidx.annotation.MainThread;
23+
import androidx.annotation.NonNull;
24+
import androidx.annotation.Nullable;
25+
import androidx.annotation.WorkerThread;
26+
import androidx.core.app.NotificationCompat;
27+
28+
/**
29+
* Sample sports live update handler, with support for asynchronously fetching team images during
30+
* onUpdate.
31+
*/
32+
public class SampleAsyncLiveUpdate implements CallbackLiveUpdateNotificationHandler {
33+
34+
private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
35+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
36+
37+
@Override
38+
public void onUpdate(@NonNull Context context,
39+
@NonNull LiveUpdateEvent event,
40+
@NonNull LiveUpdate update,
41+
@NonNull LiveUpdateResultCallback callback) {
42+
43+
Log.d("SampleAsyncLiveUpdate", "onUpdate: action=" + event + ", update=" + update);
44+
45+
if (event == LiveUpdateEvent.END) {
46+
// Dismiss the live update on END. The default behavior will leave the Live Update
47+
// in the notification tray until the dismissal time is reached or the user dismisses it.
48+
callback.cancel();
49+
return;
50+
}
51+
52+
JsonMap content = update.getContent();
53+
int teamOneScore = content.opt("team_one_score").getInt(0);
54+
int teamTwoScore = content.opt("team_two_score").getInt(0);
55+
String teamOneName = content.opt("team_one_name").getString("Foxes");
56+
String teamTwoName = content.opt("team_two_name").getString("Tigers");
57+
String teamOneImageUrl = content.opt("team_one_image").getString("");
58+
String teamTwoImageUrl = content.opt("team_two_image").getString("");
59+
String statusUpdate = content.opt("status_update").optString();
60+
61+
RemoteViews bigLayout = new RemoteViews(context.getPackageName(), R.layout.live_update_notification_big);
62+
bigLayout.setTextViewText(R.id.teamOneName, teamOneName);
63+
bigLayout.setTextViewText(R.id.teamTwoName, teamTwoName);
64+
bigLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore));
65+
bigLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore));
66+
bigLayout.setTextViewText(R.id.statusUpdate, statusUpdate);
67+
68+
RemoteViews smallLayout = new RemoteViews(context.getPackageName(), R.layout.live_update_notification_small);
69+
smallLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore));
70+
smallLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore));
71+
72+
Intent launchIntent = context.getPackageManager()
73+
.getLaunchIntentForPackage(context.getPackageName())
74+
.addCategory(update.getName())
75+
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP)
76+
.setPackage(null);
77+
78+
PendingIntent contentIntent = PendingIntent.getActivity(
79+
context, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE);
80+
81+
NotificationCompat.Builder builder =
82+
new NotificationCompat.Builder(context, "sports")
83+
.setSmallIcon(R.drawable.ic_notification)
84+
.setPriority(NotificationCompat.PRIORITY_MAX)
85+
.setCategory(NotificationCompat.CATEGORY_EVENT)
86+
.setStyle(new NotificationCompat.DecoratedCustomViewStyle())
87+
.setCustomContentView(smallLayout)
88+
.setCustomBigContentView(bigLayout)
89+
.setContentIntent(contentIntent);
90+
91+
if (teamOneImageUrl.isEmpty() && teamTwoImageUrl.isEmpty()) {
92+
callback.ok(builder);
93+
} else {
94+
fetchTeamIcons(context, teamOneImageUrl, teamTwoImageUrl, (teamOneBmp, teamTwoBmp) -> {
95+
if (teamOneBmp != null && teamTwoBmp != null) {
96+
smallLayout.setImageViewBitmap(R.id.teamTwoImage, teamTwoBmp);
97+
smallLayout.setImageViewBitmap(R.id.teamOneImage, teamOneBmp);
98+
bigLayout.setImageViewBitmap(R.id.teamOneImage, teamOneBmp);
99+
bigLayout.setImageViewBitmap(R.id.teamTwoImage, teamTwoBmp);
100+
}
101+
102+
NotificationResult result = callback.ok(builder);
103+
104+
// ...
105+
result.getNotification();
106+
result.getNotificationId();
107+
result.getNotificationTag();
108+
});
109+
}
110+
}
111+
112+
private void fetchTeamIcons(Context context, String teamOneUrl, String teamTwoUrl, TeamIconsCallback callback) {
113+
backgroundExecutor.execute(() -> {
114+
try {
115+
Bitmap teamOneBitmap = fetchBitmap(context, teamOneUrl);
116+
Bitmap teamTwoBitmap = fetchBitmap(context, teamTwoUrl);
117+
118+
mainHandler.post(() -> callback.onIconsReady(teamOneBitmap, teamTwoBitmap));
119+
} catch (Exception e) {
120+
mainHandler.post(() -> callback.onIconsReady(null, null));
121+
}
122+
});
123+
}
124+
125+
@WorkerThread
126+
private Bitmap fetchBitmap(Context context, String url) throws ExecutionException, InterruptedException {
127+
return Glide.with(context)
128+
.asBitmap()
129+
.load(url)
130+
.submit()
131+
.get();
132+
}
133+
134+
private interface TeamIconsCallback {
135+
@MainThread
136+
void onIconsReady(@Nullable Bitmap teamOneBitmap, @Nullable Bitmap teamTwoBitmap);
137+
}
138+
}

sample/src/main/java/com/urbanairship/sample/SampleAutopilot.java

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public void onAirshipReady(@NonNull UAirship airship) {
5454

5555
// Register handlers for Live Updates.
5656
LiveUpdateManager.shared().register("sports", new SampleLiveUpdate());
57+
LiveUpdateManager.shared().register("sports-async", new SampleAsyncLiveUpdate());
5758

5859
MessageCenter.shared().setOnShowMessageCenterListener(messageId -> {
5960
// Use an implicit navigation deep link for now as explicit deep links are broken

sample/src/main/java/com/urbanairship/sample/SampleLiveUpdate.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.urbanairship.sample;
22

3+
import android.annotation.SuppressLint;
4+
import android.app.Notification;
35
import android.app.PendingIntent;
46
import android.content.Context;
57
import android.content.Intent;
68
import android.util.Log;
79
import android.widget.RemoteViews;
810

11+
import com.bumptech.glide.Glide;
12+
import com.bumptech.glide.request.target.NotificationTarget;
913
import com.urbanairship.json.JsonMap;
1014
import com.urbanairship.liveupdate.LiveUpdate;
1115
import com.urbanairship.liveupdate.LiveUpdateEvent;
@@ -15,11 +19,17 @@
1519

1620
import org.jetbrains.annotations.NotNull;
1721

22+
import androidx.annotation.IdRes;
1823
import androidx.annotation.NonNull;
1924
import androidx.core.app.NotificationCompat;
2025

21-
26+
/**
27+
* Sample sports live update handler, with support for loading team images via Glide, in the
28+
* optional onNotificationPosted callback.
29+
*/
2230
public class SampleLiveUpdate implements LiveUpdateNotificationHandler {
31+
32+
@SuppressLint("MissingPermission")
2333
@Override
2434
@NotNull
2535
public LiveUpdateResult<NotificationCompat.Builder> onUpdate(@NonNull Context context, @NonNull LiveUpdateEvent event, @NonNull LiveUpdate update) {
@@ -33,11 +43,21 @@ public LiveUpdateResult<NotificationCompat.Builder> onUpdate(@NonNull Context co
3343
}
3444

3545
JsonMap content = update.getContent();
46+
47+
String teamOneName = content.opt("team_one_name").getString("Foxes");
48+
String teamTwoName = content.opt("team_two_name").getString("Tigers");
49+
50+
String teamOneImageUrl = content.opt("team_one_image").getString("");
51+
String teamTwoImageUrl = content.opt("team_two_image").getString("");
52+
3653
int teamOneScore = content.opt("team_one_score").getInt(0);
3754
int teamTwoScore = content.opt("team_two_score").getInt(0);
55+
3856
String statusUpdate = content.opt("status_update").optString();
3957

4058
RemoteViews bigLayout = new RemoteViews(context.getPackageName(), R.layout.live_update_notification_big);
59+
bigLayout.setTextViewText(R.id.teamOneName, teamOneName);
60+
bigLayout.setTextViewText(R.id.teamTwoName, teamTwoName);
4161
bigLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore));
4262
bigLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore));
4363
bigLayout.setTextViewText(R.id.statusUpdate, statusUpdate);
@@ -65,6 +85,23 @@ public LiveUpdateResult<NotificationCompat.Builder> onUpdate(@NonNull Context co
6585
.setCustomBigContentView(bigLayout)
6686
.setContentIntent(contentIntent);
6787

68-
return LiveUpdateResult.ok(builder);
88+
return LiveUpdateResult.ok(builder)
89+
.extend((notification, notificationId, tag) -> {
90+
// Load the team icons
91+
loadTeamIcon(context, teamOneImageUrl, R.id.teamOneImage, notification, notificationId, tag);
92+
loadTeamIcon(context, teamTwoImageUrl, R.id.teamTwoImage, notification, notificationId, tag);
93+
});
94+
}
95+
96+
@SuppressLint("MissingPermission")
97+
private void loadTeamIcon(Context context, String iconUrl, @IdRes int viewId, Notification notification, int id, String tag) {
98+
if (iconUrl.isEmpty()) {
99+
return;
100+
}
101+
102+
Glide.with(context).asBitmap()
103+
.load(iconUrl)
104+
.into(new NotificationTarget(
105+
context, viewId, notification.contentView, notification, id, tag));
69106
}
70107
}

sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java

+77-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
* Fragment that displays the channel ID.
3030
*/
3131
public class HomeFragment extends Fragment {
32-
3332
@NonNull
3433
@Override
3534
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -47,9 +46,86 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
4746
});
4847
});
4948

49+
bindSportsLiveUpdate(binding);
50+
bindSportsLiveUpdateAsync(binding);
51+
5052
return binding.getRoot();
5153
}
5254

55+
public void bindSportsLiveUpdate(@NonNull FragmentHomeBinding binding) {
56+
AtomicInteger score1 = new AtomicInteger(0);
57+
AtomicInteger score2 = new AtomicInteger(0);
58+
59+
binding.luStart.setOnClickListener(v -> {
60+
score1.set(0);
61+
score2.set(0);
62+
63+
LiveUpdateManager.shared().start(
64+
"sports",
65+
"sports",
66+
JsonMap.newBuilder()
67+
.put("team_one_image", "https://content.sportslogos.net/logos/28/127/full/1458.gif")
68+
.put("team_one_score", score1.getAndIncrement())
69+
.put("team_two_image", "https://content.sportslogos.net/logos/28/116/full/1439.gif")
70+
.put("team_two_score", score2.getAndIncrement())
71+
.build()
72+
);
73+
});
74+
75+
binding.luUpdate.setOnClickListener(v -> {
76+
LiveUpdateManager.shared().update(
77+
"sports",
78+
JsonMap.newBuilder()
79+
.put("team_one_image", "https://content.sportslogos.net/logos/28/127/full/1458.gif")
80+
.put("team_one_score", score1.getAndIncrement())
81+
.put("team_two_image", "https://content.sportslogos.net/logos/28/116/full/1439.gif")
82+
.put("team_two_score", score2.getAndIncrement())
83+
.build()
84+
);
85+
});
86+
87+
binding.luEnd.setOnClickListener(v -> {
88+
LiveUpdateManager.shared().end("sports");
89+
});
90+
}
91+
92+
public void bindSportsLiveUpdateAsync(@NonNull FragmentHomeBinding binding) {
93+
AtomicInteger score1 = new AtomicInteger(0);
94+
AtomicInteger score2 = new AtomicInteger(0);
95+
96+
binding.luAsyncStart.setOnClickListener(v -> {
97+
score1.set(0);
98+
score2.set(0);
99+
100+
LiveUpdateManager.shared().start(
101+
"sports-async",
102+
"sports-async",
103+
JsonMap.newBuilder()
104+
.put("team_one_image", "https://content.sportslogos.net/logos/28/127/full/1458.gif")
105+
.put("team_one_score", score1.getAndIncrement())
106+
.put("team_two_image", "https://content.sportslogos.net/logos/28/116/full/1439.gif")
107+
.put("team_two_score", score2.getAndIncrement())
108+
.build()
109+
);
110+
});
111+
112+
binding.luAsyncUpdate.setOnClickListener(v -> {
113+
LiveUpdateManager.shared().update(
114+
"sports-async",
115+
JsonMap.newBuilder()
116+
.put("team_one_image", "https://content.sportslogos.net/logos/28/116/full/1439.gif")
117+
.put("team_one_score", score1.getAndIncrement())
118+
.put("team_two_image", "https://content.sportslogos.net/logos/28/127/full/1458.gif")
119+
.put("team_two_score", score2.getAndIncrement())
120+
.build()
121+
);
122+
});
123+
124+
binding.luAsyncEnd.setOnClickListener(v -> {
125+
LiveUpdateManager.shared().end("sports-async");
126+
});
127+
}
128+
53129
@Override
54130
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
55131
super.onViewCreated(view, savedInstanceState);

0 commit comments

Comments
 (0)