Skip to content

Commit 81317d2

Browse files
authored
Merge pull request #23825 from Expensify/arosiclair-persist-notification-cache
Persist notification cache to internal storage
2 parents 5775b4f + 6b3ff25 commit 81317d2

File tree

3 files changed

+214
-53
lines changed

3 files changed

+214
-53
lines changed

android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public void onNotificationPosted(@NonNull @NotNull NotificationInfo notification
2626

2727
@Override
2828
public boolean onNotificationOpened(@NonNull @NotNull NotificationInfo notificationInfo) {
29+
// The notification is also dismissed when it's tapped so handle that as well
30+
PushMessage message = notificationInfo.getMessage();
31+
provider.onDismissNotification(message);
32+
2933
return parent.onNotificationOpened(notificationInfo);
3034
}
3135

android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java

Lines changed: 17 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
import java.util.concurrent.Future;
5050
import java.util.concurrent.TimeUnit;
5151

52+
import com.expensify.chat.customairshipextender.NotificationCache.NotificationData;
53+
import com.expensify.chat.customairshipextender.NotificationCache.NotificationMessage;
54+
5255
public class CustomNotificationProvider extends ReactNotificationProvider {
5356
// Resize icons to 100 dp x 100 dp
5457
private static final int MAX_ICON_SIZE_DPS = 100;
@@ -71,7 +74,6 @@ public class CustomNotificationProvider extends ReactNotificationProvider {
7174
private static final String ONYX_DATA_KEY = "onyxData";
7275

7376
private final ExecutorService executorService = Executors.newCachedThreadPool();
74-
public final HashMap<Long, NotificationCache> cache = new HashMap<>();
7577

7678
public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) {
7779
super(context, configOptions);
@@ -168,8 +170,8 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
168170
}
169171

170172
// Retrieve and check for cached notifications
171-
NotificationCache notificationCache = findOrCreateNotificationCache(reportID);
172-
boolean hasExistingNotification = notificationCache.messages.size() >= 1;
173+
NotificationData notificationData = NotificationCache.getNotificationData(reportID);
174+
boolean hasExistingNotification = notificationData.messages.size() >= 1;
173175

174176
try {
175177
JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap();
@@ -183,8 +185,8 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
183185
String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString("");
184186

185187
// Retrieve or create the Person object who sent the latest report comment
186-
Person person = notificationCache.people.get(accountID);
187-
Bitmap personIcon = notificationCache.bitmapIcons.get(accountID);
188+
Person person = notificationData.getPerson(accountID);
189+
Bitmap personIcon = notificationData.getIcon(accountID);
188190

189191
if (personIcon == null) {
190192
personIcon = fetchIcon(context, avatar);
@@ -200,13 +202,12 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
200202
.setName(name)
201203
.build();
202204

203-
notificationCache.people.put(accountID, person);
204-
notificationCache.bitmapIcons.put(accountID, personIcon);
205+
notificationData.putPerson(accountID, name, personIcon);
205206
}
206207

207208
// Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications
208209
long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString(""));
209-
notificationCache.messages.add(new NotificationCache.Message(person, message, createdTimeInMillis));
210+
notificationData.messages.add(new NotificationMessage(accountID, message, createdTimeInMillis));
210211

211212

212213
// Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown)
@@ -217,16 +218,16 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
217218
.setConversationTitle(conversationName);
218219

219220
// Add all conversation messages to the notification, including the last one we just received.
220-
for (NotificationCache.Message cachedMessage : notificationCache.messages) {
221-
messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person);
221+
for (NotificationMessage cachedMessage : notificationData.messages) {
222+
messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, notificationData.getPerson(cachedMessage.accountID));
222223
}
223224
builder.setStyle(messagingStyle);
224225
}
225226

226227
// Clear the previous notification associated to this conversation so it looks like we are
227228
// replacing them with this new one we just built.
228-
if (notificationCache.prevNotificationID != -1) {
229-
NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationID);
229+
if (notificationData.prevNotificationID != -1) {
230+
NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID);
230231
}
231232

232233
} catch (Exception e) {
@@ -235,7 +236,9 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
235236

236237
// Store the new notification ID so we can replace the notification if this conversation
237238
// receives more messages
238-
notificationCache.prevNotificationID = notificationID;
239+
notificationData.prevNotificationID = notificationID;
240+
241+
NotificationCache.setNotificationData(reportID, notificationData);
239242
}
240243

241244
/**
@@ -254,25 +257,6 @@ private long getMessageTimeInMillis(String createdTime) {
254257
return Calendar.getInstance().getTimeInMillis();
255258
}
256259

257-
/**
258-
* Check if we are showing a notification related to a reportID.
259-
* If not, create a new NotificationCache so we can build a conversation notification
260-
* as the messages come.
261-
*
262-
* @param reportID Report ID.
263-
* @return Notification Cache.
264-
*/
265-
private NotificationCache findOrCreateNotificationCache(long reportID) {
266-
NotificationCache notificationCache = cache.get(reportID);
267-
268-
if (notificationCache == null) {
269-
notificationCache = new NotificationCache();
270-
cache.put(reportID, notificationCache);
271-
}
272-
273-
return notificationCache;
274-
}
275-
276260
/**
277261
* Remove the notification data from the cache when the user dismisses the notification.
278262
*
@@ -287,7 +271,7 @@ public void onDismissNotification(PushMessage message) {
287271
return;
288272
}
289273

290-
cache.remove(reportID);
274+
NotificationCache.setNotificationData(reportID, null);
291275
} catch (Exception e) {
292276
Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e);
293277
}
@@ -328,24 +312,4 @@ private Bitmap fetchIcon(@NonNull Context context, String urlString) {
328312

329313
return null;
330314
}
331-
332-
private static class NotificationCache {
333-
public Map<String, Person> people = new HashMap<>();
334-
public ArrayList<Message> messages = new ArrayList<>();
335-
336-
public Map<String, Bitmap> bitmapIcons = new HashMap<>();
337-
public int prevNotificationID = -1;
338-
339-
public static class Message {
340-
public Person person;
341-
public String text;
342-
public long time;
343-
344-
Message(Person person, String text, long time) {
345-
this.person = person;
346-
this.text = text;
347-
this.time = time;
348-
}
349-
}
350-
}
351315
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.expensify.chat.customairshipextender;
2+
3+
import android.content.Context;
4+
import android.graphics.Bitmap;
5+
import android.graphics.BitmapFactory;
6+
import android.util.Base64;
7+
8+
import androidx.core.app.Person;
9+
import androidx.core.graphics.drawable.IconCompat;
10+
11+
import com.expensify.chat.MainApplication;
12+
import com.urbanairship.UAirship;
13+
14+
import java.io.ByteArrayOutputStream;
15+
import java.io.File;
16+
import java.io.FileInputStream;
17+
import java.io.FileOutputStream;
18+
import java.io.IOException;
19+
import java.io.ObjectInputStream;
20+
import java.io.ObjectOutputStream;
21+
import java.io.Serializable;
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
25+
public class NotificationCache {
26+
27+
private static final String CACHE_FILE_NAME = "notification-cache";
28+
private static HashMap<String, NotificationData> cache = null;
29+
30+
/*
31+
* Get NotificationData for an existing notification or create a new instance
32+
* if it doesn't exist
33+
*/
34+
public static NotificationData getNotificationData(long reportID) {
35+
if (cache == null) {
36+
cache = readFromInternalStorage();
37+
}
38+
39+
NotificationData notificationData = cache.get(Long.toString(reportID));
40+
41+
if (notificationData == null) {
42+
notificationData = new NotificationData();
43+
setNotificationData(reportID, notificationData);
44+
}
45+
46+
return notificationData;
47+
}
48+
49+
/*
50+
* Set and persist NotificationData in the cache
51+
*/
52+
public static void setNotificationData(long reportID, NotificationData data) {
53+
if (cache == null) {
54+
cache = readFromInternalStorage();
55+
}
56+
57+
cache.put(Long.toString(reportID), data);
58+
writeToInternalStorage();
59+
}
60+
61+
private static void writeToInternalStorage() {
62+
Context context = UAirship.getApplicationContext();
63+
64+
FileOutputStream fos = null;
65+
ObjectOutputStream oos = null;
66+
try {
67+
File outputFile = new File(context.getFilesDir(), CACHE_FILE_NAME);
68+
fos = new FileOutputStream(outputFile);
69+
oos = new ObjectOutputStream(fos);
70+
oos.writeObject(cache);
71+
} catch (IOException e) {
72+
e.printStackTrace();
73+
} finally {
74+
try {
75+
if (oos != null) {
76+
oos.close();
77+
}
78+
if (fos != null) {
79+
fos.close();
80+
}
81+
} catch (IOException e) {
82+
e.printStackTrace();
83+
}
84+
}
85+
}
86+
87+
private static HashMap<String, NotificationData> readFromInternalStorage() {
88+
HashMap<String, NotificationData> result;
89+
Context context = UAirship.getApplicationContext();
90+
91+
FileInputStream fis = null;
92+
ObjectInputStream ois = null;
93+
try {
94+
File fileCache = new File(context.getFilesDir(), CACHE_FILE_NAME);
95+
fis = new FileInputStream(fileCache);
96+
ois = new ObjectInputStream(fis);
97+
result = (HashMap<String, NotificationData>) ois.readObject();
98+
} catch (IOException | ClassNotFoundException e) {
99+
e.printStackTrace();
100+
result = new HashMap<>();
101+
} finally {
102+
try {
103+
if (ois != null) {
104+
ois.close();
105+
}
106+
if (fis != null) {
107+
fis.close();
108+
}
109+
} catch (IOException e) {
110+
e.printStackTrace();
111+
}
112+
}
113+
114+
return result;
115+
}
116+
117+
/**
118+
* A class for caching data for notifications. We use this to track active notifications so we
119+
* can thread related notifications together
120+
*/
121+
public static class NotificationData implements Serializable {
122+
private final HashMap<String, String> names = new HashMap<>();
123+
124+
// A map of accountID => base64 encoded Bitmap
125+
// In order to make Bitmaps serializable, we encode them as base64 strings
126+
private final HashMap<String, String> icons = new HashMap<>();
127+
public ArrayList<NotificationMessage> messages = new ArrayList<>();
128+
129+
public int prevNotificationID = -1;
130+
131+
public NotificationData() {}
132+
133+
public Bitmap getIcon(String accountID) {
134+
return decodeToBitmap(icons.get(accountID));
135+
}
136+
137+
public void putIcon(String accountID, Bitmap bitmap) {
138+
icons.put(accountID, encodeToBase64(bitmap));
139+
}
140+
141+
public Person getPerson(String accountID) {
142+
if (!names.containsKey(accountID) || !icons.containsKey(accountID)) {
143+
return null;
144+
}
145+
146+
String name = names.get(accountID);
147+
Bitmap icon = getIcon(accountID);
148+
149+
return new Person.Builder()
150+
.setIcon(IconCompat.createWithBitmap(icon))
151+
.setKey(accountID)
152+
.setName(name)
153+
.build();
154+
}
155+
156+
public void putPerson(String accountID, String name, Bitmap icon) {
157+
names.put(accountID, name);
158+
putIcon(accountID, icon);
159+
}
160+
161+
public static String encodeToBase64(Bitmap bitmap) {
162+
if (bitmap == null) {
163+
return "";
164+
}
165+
166+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
167+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
168+
byte[] byteArray = byteArrayOutputStream.toByteArray();
169+
return Base64.encodeToString(byteArray, Base64.DEFAULT);
170+
}
171+
172+
public static Bitmap decodeToBitmap(String base64String) {
173+
if (base64String == null) {
174+
return null;
175+
}
176+
177+
byte[] decodedBytes = Base64.decode(base64String, Base64.DEFAULT);
178+
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
179+
}
180+
}
181+
182+
public static class NotificationMessage implements Serializable {
183+
public String accountID;
184+
public String text;
185+
public long time;
186+
187+
NotificationMessage(String accountID, String text, long time) {
188+
this.accountID = accountID;
189+
this.text = text;
190+
this.time = time;
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)