Skip to content

Commit ed56b1f

Browse files
committed
feat: add push notifications for android app
1 parent e4ea9da commit ed56b1f

File tree

9 files changed

+544
-74
lines changed

9 files changed

+544
-74
lines changed

android/app/capacitor.build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ dependencies {
1212
implementation project(':capacitor-community-file-opener')
1313
implementation project(':capacitor-app')
1414
implementation project(':capacitor-filesystem')
15+
implementation project(':capacitor-local-notifications')
16+
implementation project(':capacitor-preferences')
1517
implementation project(':capacitor-push-notifications')
1618

1719
}

android/app/src/main/AndroidManifest.xml

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
android:roundIcon="@mipmap/ic_launcher_round"
88
android:supportsRtl="true"
99
android:theme="@style/AppTheme">
10+
1011
<activity
1112
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
1213
android:name=".MainActivity"
@@ -19,7 +20,7 @@
1920
<category android:name="android.intent.category.LAUNCHER" />
2021
</intent-filter>
2122

22-
<!-- Deep links for Adamant -->
23+
<!-- Deep links for ADAMANT protocol -->
2324
<intent-filter android:autoVerify="true">
2425
<action android:name="android.intent.action.VIEW" />
2526
<category android:name="android.intent.category.DEFAULT" />
@@ -42,24 +43,36 @@
4243
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
4344
</provider>
4445

45-
<!-- Firebase Messaging Service for Push Notifications -->
46-
<service
47-
android:name="com.capacitorjs.plugins.pushnotifications.MessagingService"
48-
android:exported="false">
49-
<intent-filter>
46+
<!-- Custom Firebase Messaging Service for ADAMANT Push Notifications -->
47+
<service
48+
android:name=".AdamantFirebaseMessagingService"
49+
android:exported="false"
50+
android:stopWithTask="false">
51+
<intent-filter android:priority="1000">
5052
<action android:name="com.google.firebase.MESSAGING_EVENT" />
5153
</intent-filter>
5254
</service>
5355

56+
<!-- Disable Firebase automatic features to use custom implementation -->
57+
<meta-data
58+
android:name="firebase_messaging_auto_init_enabled"
59+
android:value="false" />
60+
<meta-data
61+
android:name="firebase_analytics_collection_enabled"
62+
android:value="false" />
63+
5464
</application>
5565

56-
<!-- Permissions -->
66+
<!-- Basic permissions -->
5767
<uses-permission android:name="android.permission.INTERNET" />
5868
<uses-permission android:name="android.permission.CAMERA" />
5969
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
60-
<!-- Push Notifications -->
70+
71+
<!-- Push notification permissions -->
6172
<uses-permission android:name="android.permission.WAKE_LOCK" />
6273
<uses-permission android:name="android.permission.VIBRATE" />
63-
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
74+
75+
<!-- Notification permissions for Android 13+ (API 33+) -->
76+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
6477

65-
</manifest>
78+
</manifest>
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
package im.adamant.adamantmessengerpwa;
2+
3+
import android.app.NotificationChannel;
4+
import android.app.NotificationManager;
5+
import android.app.PendingIntent;
6+
import android.content.Context;
7+
import android.content.Intent;
8+
import android.content.SharedPreferences;
9+
import android.os.Build;
10+
import android.util.Log;
11+
import androidx.core.app.NotificationCompat;
12+
import com.google.firebase.messaging.FirebaseMessagingService;
13+
import com.google.firebase.messaging.RemoteMessage;
14+
import java.util.HashSet;
15+
import android.os.Handler;
16+
import android.os.Looper;
17+
import org.json.JSONObject;
18+
19+
public class AdamantFirebaseMessagingService extends FirebaseMessagingService {
20+
21+
private static final String TAG = "AdamantFirebaseMsg";
22+
private static final String CHANNEL_ID = "adamant_notifications";
23+
private static final long CLEANUP_INTERVAL = 2 * 60 * 1000;
24+
25+
private static final HashSet<String> processedEvents = new HashSet<>();
26+
private static Handler cleanupHandler = new Handler(Looper.getMainLooper());
27+
28+
static {
29+
cleanupHandler.postDelayed(new Runnable() {
30+
@Override
31+
public void run() {
32+
processedEvents.clear();
33+
cleanupHandler.postDelayed(this, CLEANUP_INTERVAL);
34+
}
35+
}, CLEANUP_INTERVAL);
36+
}
37+
38+
@Override
39+
public void onCreate() {
40+
super.onCreate();
41+
createNotificationChannel();
42+
}
43+
44+
@Override
45+
public void handleIntent(Intent intent) {
46+
RemoteMessage remoteMessage = null;
47+
try {
48+
if (intent.getExtras() != null) {
49+
remoteMessage = new RemoteMessage(intent.getExtras());
50+
}
51+
} catch (Exception e) {
52+
Log.e(TAG, "Error creating RemoteMessage", e);
53+
}
54+
55+
if (remoteMessage == null) {
56+
super.handleIntent(intent);
57+
return;
58+
}
59+
60+
if (areNotificationsDisabled()) {
61+
return;
62+
}
63+
64+
String txnData = remoteMessage.getData().get("txn");
65+
if (txnData != null) {
66+
String transactionId = extractTransactionId(txnData);
67+
if (transactionId != null) {
68+
if (processedEvents.contains(transactionId)) {
69+
return;
70+
}
71+
processedEvents.add(transactionId);
72+
73+
// Also check if this is a signal message that should be hidden
74+
String body = formatNotificationText(txnData);
75+
if (body == null) {
76+
// Signal message - don't show notification but still mark as processed
77+
return;
78+
}
79+
}
80+
}
81+
82+
if (isAppInForeground()) {
83+
super.handleIntent(intent);
84+
} else {
85+
showBackgroundNotification(remoteMessage);
86+
}
87+
}
88+
89+
@Override
90+
public void onMessageReceived(RemoteMessage remoteMessage) {
91+
super.onMessageReceived(remoteMessage);
92+
}
93+
94+
private void showBackgroundNotification(RemoteMessage remoteMessage) {
95+
try {
96+
String txnData = remoteMessage.getData().get("txn");
97+
if (txnData == null) {
98+
return;
99+
}
100+
101+
String transactionId = extractTransactionId(txnData);
102+
String senderId = extractSenderId(txnData);
103+
if (transactionId == null || senderId == null) {
104+
return;
105+
}
106+
107+
String title = senderId;
108+
String body = formatNotificationText(txnData);
109+
110+
Intent clickIntent = new Intent(this, MainActivity.class);
111+
clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
112+
clickIntent.putExtra("openChat", true);
113+
clickIntent.putExtra("senderId", senderId);
114+
clickIntent.putExtra("transactionId", transactionId);
115+
116+
PendingIntent pendingIntent = PendingIntent.getActivity(
117+
this,
118+
transactionId.hashCode(),
119+
clickIntent,
120+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
121+
);
122+
123+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
124+
.setSmallIcon(android.R.drawable.ic_dialog_info)
125+
.setContentTitle(title)
126+
.setContentText(body)
127+
.setAutoCancel(true)
128+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
129+
.setContentIntent(pendingIntent);
130+
131+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
132+
int notificationId = Math.abs(transactionId.hashCode());
133+
notificationManager.notify(notificationId, builder.build());
134+
135+
MainActivity.saveNotificationId(this, senderId, notificationId);
136+
137+
} catch (Exception e) {
138+
Log.e(TAG, "Error showing notification", e);
139+
}
140+
}
141+
142+
private String formatNotificationText(String txnData) {
143+
try {
144+
JSONObject transaction = new JSONObject(txnData);
145+
146+
int transactionType = transaction.optInt("type", -1);
147+
long transactionAmount = transaction.optLong("amount", 0);
148+
149+
// Type 0: Pure ADM Transfer (without comments)
150+
if (transactionType == 0) {
151+
return formatADMTransfer(transactionAmount);
152+
}
153+
154+
// Type 8: Chat Message (can include ADM transfer with comment)
155+
if (transactionType == 8) {
156+
// If amount > 0, it's ADM transfer with comment
157+
if (transactionAmount > 0) {
158+
return formatADMTransferWithComment(transactionAmount);
159+
}
160+
161+
// Regular chat message - check asset.chat.type
162+
return formatChatMessage(transaction);
163+
}
164+
165+
return "New transaction";
166+
} catch (Exception e) {
167+
Log.e(TAG, "Error parsing notification", e);
168+
return "New message";
169+
}
170+
}
171+
172+
private String formatChatMessage(JSONObject transaction) {
173+
try {
174+
if (!transaction.has("asset")) {
175+
return "New message";
176+
}
177+
178+
JSONObject asset = transaction.getJSONObject("asset");
179+
if (!asset.has("chat")) {
180+
return "New message";
181+
}
182+
183+
JSONObject chat = asset.getJSONObject("chat");
184+
int chatType = chat.optInt("type", 1);
185+
186+
switch (chatType) {
187+
case 1:
188+
// Basic Encrypted Message
189+
return "New message";
190+
case 2:
191+
// Rich Content Message (crypto transfers, etc.)
192+
return "sent you crypto";
193+
case 3:
194+
// Signal Message (should be hidden per documentation)
195+
return null; // Don't show notification for signal messages
196+
default:
197+
return "New message";
198+
}
199+
} catch (Exception e) {
200+
return "New message";
201+
}
202+
}
203+
204+
private String formatADMTransfer(long amount) {
205+
if (amount == 0) {
206+
return "sent you ADM";
207+
}
208+
double admAmount = amount / 100000000.0;
209+
210+
java.math.BigDecimal bd = java.math.BigDecimal.valueOf(admAmount);
211+
return String.format("sent you %s ADM", bd.stripTrailingZeros().toPlainString());
212+
}
213+
214+
private String formatADMTransferWithComment(long amount) {
215+
if (amount == 0) {
216+
return "sent you ADM with message";
217+
}
218+
double admAmount = amount / 100000000.0;
219+
220+
java.math.BigDecimal bd = java.math.BigDecimal.valueOf(admAmount);
221+
return String.format("sent you %s ADM with message", bd.stripTrailingZeros().toPlainString());
222+
}
223+
224+
private boolean areNotificationsDisabled() {
225+
try {
226+
SharedPreferences prefs = getSharedPreferences("CapacitorStorage", Context.MODE_PRIVATE);
227+
String notificationTypeStr = prefs.getString("allowNotificationType", "2");
228+
return "0".equals(notificationTypeStr);
229+
} catch (Exception e) {
230+
return false;
231+
}
232+
}
233+
234+
private boolean isAppInForeground() {
235+
try {
236+
android.app.ActivityManager activityManager = (android.app.ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
237+
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
238+
239+
if (appProcesses == null) {
240+
return false;
241+
}
242+
243+
String packageName = getPackageName();
244+
for (android.app.ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
245+
if (appProcess.processName.equals(packageName)) {
246+
return appProcess.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
247+
}
248+
}
249+
return false;
250+
} catch (Exception e) {
251+
return false;
252+
}
253+
}
254+
255+
private String extractTransactionId(String txnData) {
256+
try {
257+
JSONObject transaction = new JSONObject(txnData);
258+
return transaction.optString("id", null);
259+
} catch (Exception e) {
260+
// Fallback to string parsing
261+
return extractFieldFromJson(txnData, "\"id\":\"", 6);
262+
}
263+
}
264+
265+
private String extractSenderId(String txnData) {
266+
try {
267+
JSONObject transaction = new JSONObject(txnData);
268+
return transaction.optString("senderId", null);
269+
} catch (Exception e) {
270+
// Fallback to string parsing
271+
return extractFieldFromJson(txnData, "\"senderId\":\"", 12);
272+
}
273+
}
274+
275+
private String extractFieldFromJson(String jsonData, String fieldPattern, int patternLength) {
276+
try {
277+
int startIndex = jsonData.indexOf(fieldPattern);
278+
if (startIndex == -1) {
279+
return null;
280+
}
281+
282+
startIndex += patternLength;
283+
int endIndex = jsonData.indexOf("\"", startIndex);
284+
return endIndex == -1 ? null : jsonData.substring(startIndex, endIndex);
285+
} catch (Exception e) {
286+
return null;
287+
}
288+
}
289+
290+
private void createNotificationChannel() {
291+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
292+
NotificationChannel channel = new NotificationChannel(
293+
CHANNEL_ID,
294+
"ADAMANT Notifications",
295+
NotificationManager.IMPORTANCE_DEFAULT
296+
);
297+
channel.setDescription("Notifications for ADAMANT Messenger");
298+
299+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
300+
notificationManager.createNotificationChannel(channel);
301+
}
302+
}
303+
304+
@Override
305+
public void onNewToken(String token) {
306+
super.onNewToken(token);
307+
}
308+
}

0 commit comments

Comments
 (0)