Skip to content

Commit 560efaa

Browse files
alltheseasclaude
andcommitted
android: add notification methods to MainActivity
Implement the 8 methods that the Rust JNI bridge (android.rs) calls on the activity context for notification management: - getNotificationMode / setNotificationMode: persist mode index (0=FCM, 1=Native, 2=Disabled) in SharedPreferences - enableFcmNotifications: read cached FCM token and register with notepush server via NotepushClient on a background coroutine - disableFcmNotifications: unregister from notepush and clear token - enableNativeNotifications / disableNativeNotifications: store or clear relay config for a future native WebSocket service - isNotificationPermissionGranted: check POST_NOTIFICATIONS on API 33+ - requestNotificationPermission: launch runtime permission dialog, forward result to Rust via nativeOnNotificationPermissionResult Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4113f4c commit 560efaa

File tree

1 file changed

+182
-1
lines changed
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck

1 file changed

+182
-1
lines changed

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.kt

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.damus.notedeck
22

3+
import android.Manifest
4+
import android.content.Context
35
import android.content.Intent
6+
import android.content.pm.PackageManager
7+
import android.os.Build
48
import android.database.Cursor
59
import android.net.Uri
610
import android.os.Bundle
@@ -9,31 +13,47 @@ import android.util.Log
913
import android.view.MotionEvent
1014
import android.view.View
1115
import android.view.ViewGroup
16+
import androidx.core.app.ActivityCompat
17+
import androidx.core.content.ContextCompat
1218
import androidx.core.graphics.Insets
1319
import androidx.core.view.ViewCompat
1420
import androidx.core.view.WindowCompat
1521
import androidx.core.view.WindowInsetsCompat
1622
import androidx.core.view.WindowInsetsControllerCompat
23+
import com.damus.notedeck.service.NotepushClient
1724
import com.google.androidgamesdk.GameActivity
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.SupervisorJob
28+
import kotlinx.coroutines.cancel
29+
import kotlinx.coroutines.launch
1830
import java.io.ByteArrayOutputStream
1931
import java.io.IOException
2032

2133
/**
2234
* Main activity for Notedeck Android, extending GameActivity for NDK/OpenGL rendering.
2335
*
2436
* Hosts the native Rust application via JNI and handles Android-specific concerns
25-
* like file picking, window insets, and touch event offset correction.
37+
* like file picking, window insets, touch event offset correction, and notification
38+
* management (FCM registration, permission requests, mode persistence).
2639
*/
2740
class MainActivity : GameActivity() {
2841

2942
companion object {
3043
const val REQUEST_CODE_PICK_FILE = 420
44+
const val REQUEST_CODE_NOTIFICATION_PERMISSION = 1001
45+
private const val PREFS_NAME = "notedeck_notifications"
46+
private const val KEY_MODE = "notification_mode"
3147
private const val TAG = "MainActivity"
3248
}
3349

50+
private val notificationScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
51+
private val notepushClient = NotepushClient()
52+
3453
// Native method declarations (implemented in Rust via JNI)
3554
private external fun nativeOnFilePickedFailed(uri: String, e: String)
3655
private external fun nativeOnFilePickedWithContent(uriInfo: Array<Any?>, content: ByteArray)
56+
private external fun nativeOnNotificationPermissionResult(granted: Boolean)
3757

3858
/**
3959
* Launch the system document picker for selecting one or more files.
@@ -188,6 +208,7 @@ class MainActivity : GameActivity() {
188208

189209
override fun onDestroy() {
190210
super.onDestroy()
211+
notificationScope.cancel()
191212
}
192213

193214
/**
@@ -221,4 +242,164 @@ class MainActivity : GameActivity() {
221242
private fun getContent(): View {
222243
return window.decorView.findViewById(android.R.id.content)
223244
}
245+
246+
// =========================================================================
247+
// Notification methods (called from Rust via JNI)
248+
// =========================================================================
249+
250+
/**
251+
* Read the persisted notification mode from SharedPreferences.
252+
*
253+
* @return Mode index: 0 = FCM, 1 = Native, 2 = Disabled (default).
254+
*/
255+
fun getNotificationMode(): Int {
256+
return getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
257+
.getInt(KEY_MODE, 2)
258+
}
259+
260+
/**
261+
* Persist the notification mode to SharedPreferences.
262+
*
263+
* @param mode 0 = FCM, 1 = Native, 2 = Disabled.
264+
*/
265+
fun setNotificationMode(mode: Int) {
266+
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
267+
.edit()
268+
.putInt(KEY_MODE, mode)
269+
.apply()
270+
}
271+
272+
/**
273+
* Enable FCM push notifications for the given pubkey.
274+
*
275+
* Reads the locally cached FCM token (written by
276+
* [NotedeckFirebaseMessagingService.onNewToken]) and registers the
277+
* device with the notepush server on a background coroutine.
278+
*
279+
* @param pubkeyHex The user's Nostr public key in hex format.
280+
*/
281+
fun enableFcmNotifications(pubkeyHex: String) {
282+
val fcmToken = getSharedPreferences("notedeck_fcm", Context.MODE_PRIVATE)
283+
.getString("fcm_token", null)
284+
285+
if (fcmToken == null) {
286+
Log.e(TAG, "enableFcmNotifications: no FCM token available")
287+
return
288+
}
289+
290+
// Store pubkey so disableFcmNotifications can unregister later
291+
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
292+
.edit()
293+
.putString("registered_pubkey", pubkeyHex)
294+
.apply()
295+
296+
notificationScope.launch {
297+
val success = notepushClient.registerDevice(pubkeyHex, fcmToken)
298+
if (success) {
299+
Log.d(TAG, "FCM registration succeeded for ${pubkeyHex.take(8)}")
300+
} else {
301+
Log.e(TAG, "FCM registration failed for ${pubkeyHex.take(8)}")
302+
}
303+
}
304+
}
305+
306+
/**
307+
* Disable FCM push notifications.
308+
*
309+
* Unregisters from the notepush server (if a token and pubkey are
310+
* available). The FCM token itself is retained so re-enabling
311+
* doesn't require a new token from Firebase.
312+
*/
313+
fun disableFcmNotifications() {
314+
val prefs = getSharedPreferences("notedeck_fcm", Context.MODE_PRIVATE)
315+
val fcmToken = prefs.getString("fcm_token", null)
316+
val pubkeyHex = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
317+
.getString("registered_pubkey", null)
318+
319+
if (fcmToken != null && pubkeyHex != null) {
320+
notificationScope.launch {
321+
val success = notepushClient.unregisterDevice(pubkeyHex, fcmToken)
322+
if (success) {
323+
Log.d(TAG, "FCM unregistration succeeded")
324+
} else {
325+
Log.e(TAG, "FCM unregistration failed")
326+
}
327+
}
328+
}
329+
}
330+
331+
/**
332+
* Store configuration for a future native (WebSocket) notification service.
333+
*
334+
* @param pubkeyHex The user's Nostr public key in hex format.
335+
* @param relaysJson JSON-serialized array of relay URLs.
336+
*/
337+
fun enableNativeNotifications(pubkeyHex: String, relaysJson: String) {
338+
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
339+
.edit()
340+
.putString("native_pubkey", pubkeyHex)
341+
.putString("native_relays", relaysJson)
342+
.apply()
343+
Log.d(TAG, "Native notification config stored for ${pubkeyHex.take(8)}")
344+
}
345+
346+
/**
347+
* Clear stored native notification configuration.
348+
*/
349+
fun disableNativeNotifications() {
350+
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
351+
.edit()
352+
.remove("native_pubkey")
353+
.remove("native_relays")
354+
.apply()
355+
Log.d(TAG, "Native notification config cleared")
356+
}
357+
358+
/**
359+
* Check whether the POST_NOTIFICATIONS permission is granted.
360+
* On API < 33 (pre-Tiramisu), notifications are always permitted.
361+
*/
362+
fun isNotificationPermissionGranted(): Boolean {
363+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true
364+
365+
return ContextCompat.checkSelfPermission(
366+
this,
367+
Manifest.permission.POST_NOTIFICATIONS
368+
) == PackageManager.PERMISSION_GRANTED
369+
}
370+
371+
/**
372+
* Request the POST_NOTIFICATIONS runtime permission (API 33+).
373+
*
374+
* The result is delivered to [onRequestPermissionsResult], which
375+
* forwards it to Rust via [nativeOnNotificationPermissionResult].
376+
* On pre-33 devices the permission is implicit, so we report granted
377+
* immediately.
378+
*/
379+
fun requestNotificationPermission() {
380+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
381+
nativeOnNotificationPermissionResult(true)
382+
return
383+
}
384+
385+
ActivityCompat.requestPermissions(
386+
this,
387+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
388+
REQUEST_CODE_NOTIFICATION_PERMISSION
389+
)
390+
}
391+
392+
override fun onRequestPermissionsResult(
393+
requestCode: Int,
394+
permissions: Array<out String>,
395+
grantResults: IntArray
396+
) {
397+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
398+
399+
if (requestCode != REQUEST_CODE_NOTIFICATION_PERMISSION) return
400+
401+
val granted = grantResults.isNotEmpty() &&
402+
grantResults[0] == PackageManager.PERMISSION_GRANTED
403+
nativeOnNotificationPermissionResult(granted)
404+
}
224405
}

0 commit comments

Comments
 (0)