11package com.damus.notedeck
22
3+ import android.Manifest
4+ import android.content.Context
35import android.content.Intent
6+ import android.content.pm.PackageManager
7+ import android.os.Build
48import android.database.Cursor
59import android.net.Uri
610import android.os.Bundle
@@ -9,31 +13,47 @@ import android.util.Log
913import android.view.MotionEvent
1014import android.view.View
1115import android.view.ViewGroup
16+ import androidx.core.app.ActivityCompat
17+ import androidx.core.content.ContextCompat
1218import androidx.core.graphics.Insets
1319import androidx.core.view.ViewCompat
1420import androidx.core.view.WindowCompat
1521import androidx.core.view.WindowInsetsCompat
1622import androidx.core.view.WindowInsetsControllerCompat
23+ import com.damus.notedeck.service.NotepushClient
1724import 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
1830import java.io.ByteArrayOutputStream
1931import 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 */
2740class 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