Skip to content

Commit

Permalink
Add Android notification plugin (#150)
Browse files Browse the repository at this point in the history
* Add Android notification plugin

* Fix CI issues

* Change notification icon on Android

* Optimize imports & reformat dart code

* Rename notification icon

* Allow tracking a payment without destination

---------

Co-authored-by: Erdem Yerebasmaz <[email protected]>
  • Loading branch information
dangeross and erdemyerebasmaz authored Sep 2, 2024
1 parent 20921ef commit cc3fefc
Show file tree
Hide file tree
Showing 43 changed files with 924 additions and 27 deletions.
32 changes: 25 additions & 7 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,27 @@ if (flutterVersionName == null) {

android {
namespace = "com.breez.liquid.l_breez"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileSdkVersion 34
ndkVersion flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

defaultConfig {
applicationId = "com.breez.liquid.l_breez"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = 24
minSdkVersion 24 // Android 7.0
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
Expand Down Expand Up @@ -124,6 +132,16 @@ flutter {
}

dependencies {
// Import the Firebase BoM
implementation platform("com.google.firebase:firebase-bom:33.0.0")
implementation "androidx.core:core-ktx:1.12.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22"
// Import FCM
implementation 'com.google.firebase:firebase-messaging:23.4.1'
// Import breez_sdk_liquid for notifications
implementation("net.java.dev.jna:jna:5.14.0@aar")
implementation("com.github.breez:breez-sdk-liquid:0.2.2-dev14") {
exclude group:"net.java.dev.jna"
}
// Logging
implementation 'org.tinylog:tinylog-api-kotlin:2.6.2'
implementation 'org.tinylog:tinylog-impl:2.6.2'
}
9 changes: 9 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.editing.** { *; }

# Tinylog
-keepnames interface org.tinylog.**
-keepnames class * implements org.tinylog.**
-keepclassmembers class * implements org.tinylog.** { <init>(...); }

# JNA
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }

# To ignore minifyEnabled: true error
# https://github.com/flutter/flutter/issues/19250
# https://github.com/flutter/flutter/issues/37441
Expand Down
37 changes: 34 additions & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
paxkage="com.breez.liquid.l_breez">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <!-- Android 9+ -->
<uses-permission android:name="android.permission.NFC"/> <!-- Deep links -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 13+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Android 9+ -->

<application
android:label="${appName}"
android:name="${applicationName}"
android:roundIcon="@mipmap/ic_launcher_round"
android:icon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false">

<service
android:name=".BreezFcmService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<service
android:name=".BreezForegroundService"
android:foregroundServiceType="shortService"
android:exported="false"
android:stopWithTask="false">
</service>

<activity
android:name=".MainActivity"
android:exported="true"
Expand Down Expand Up @@ -120,6 +140,17 @@
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="false"/>
<!-- Notifications -->
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_ic_notification" />
<!-- Set color used with incoming notification messages. This is used when no color is set
for the incoming notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/breez_notification_color" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.breez.liquid.l_breez

import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.Process
import android.os.SystemClock
import androidx.core.content.ContextCompat
import breez_sdk_liquid_notification.Constants
import breez_sdk_liquid_notification.Message
import breez_sdk_liquid_notification.MessagingService
import com.breez.liquid.l_breez.BreezLogger.Companion.configureLogger
import com.google.android.gms.common.util.PlatformVersion.isAtLeastLollipop
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import org.tinylog.kotlin.Logger

@SuppressLint("MissingFirebaseInstanceTokenRefresh")
class BreezFcmService : MessagingService, FirebaseMessagingService() {
companion object {
private const val TAG = "BreezFcmService"
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
configureLogger(applicationContext)
Logger.tag(TAG).debug { "FCM message received!" }

if (remoteMessage.priority == RemoteMessage.PRIORITY_HIGH) {
Logger.tag(TAG).debug { "onMessageReceived from: ${remoteMessage.from}" }
Logger.tag(TAG).debug { "onMessageReceived data: ${remoteMessage.data}" }
remoteMessage.asMessage()
?.also { message -> startServiceIfNeeded(applicationContext, message) }
} else {
Logger.tag(TAG).debug { "Ignoring FCM message" }
}
}

private fun RemoteMessage.asMessage(): Message? {
return data[Constants.MESSAGE_DATA_TYPE]?.let {
Message(
data[Constants.MESSAGE_DATA_TYPE], data[Constants.MESSAGE_DATA_PAYLOAD]
)
}
}

override fun startForegroundService(message: Message) {
Logger.tag(TAG).debug { "Starting BreezForegroundService w/ message ${message.type}: ${message.payload}" }
val intent = Intent(applicationContext, BreezForegroundService::class.java)
intent.putExtra(Constants.EXTRA_REMOTE_MESSAGE, message)
ContextCompat.startForegroundService(applicationContext, intent)
}

@SuppressLint("VisibleForTests")
override fun isAppForeground(context: Context): Boolean {
val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (keyguardManager.isKeyguardLocked) {
return false // Screen is off or lock screen is showing
}
// Screen is on and unlocked, now check if the process is in the foreground
if (!isAtLeastLollipop()) {
// Before L the process has IMPORTANCE_FOREGROUND while it executes BroadcastReceivers.
// As soon as the service is started the BroadcastReceiver should stop.
// UNFORTUNATELY the system might not have had the time to downgrade the process
// (this is happening consistently in JellyBean).
// With SystemClock.sleep(10) we tell the system to give a little bit more of CPU
// to the main thread (this code is executing on a secondary thread) allowing the
// BroadcastReceiver to exit the onReceive() method and downgrade the process priority.
SystemClock.sleep(10)
}
val pid = Process.myPid()
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val appProcesses = am.runningAppProcesses
if (appProcesses != null) {
for (process in appProcesses) {
if (process.pid == pid) {
return process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
}
}
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.breez.liquid.l_breez

import android.content.SharedPreferences
import breez_sdk_liquid.ConnectRequest
import breez_sdk_liquid.LiquidNetwork
import breez_sdk_liquid.LogEntry
import breez_sdk_liquid.Logger as SdkLogger
import breez_sdk_liquid.defaultConfig
import breez_sdk_liquid.setLogger as setSdkLogger
import breez_sdk_liquid_notification.ForegroundService
import breez_sdk_liquid_notification.NotificationHelper.Companion.registerNotificationChannels
import com.breez.liquid.l_breez.utils.FlutterSecuredStorageHelper.Companion.readSecuredValue
import io.flutter.util.PathUtils
import org.tinylog.kotlin.Logger

class BreezForegroundService : SdkLogger, ForegroundService() {
companion object {
private const val TAG = "BreezForegroundService"

private const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"
private const val ACCOUNT_MNEMONIC = "account_mnemonic"
private const val DEFAULT_CLICK_ACTION = "FLUTTER_NOTIFICATION_CLICK"
private const val ELEMENT_PREFERENCES_KEY_PREFIX =
"VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIHNlY3VyZSBzdG9yYWdlCg"
}

override fun onCreate() {
super.onCreate()
setLogger(this)
setSdkLogger(this)
Logger.tag(TAG).debug { "Creating Breez foreground service..." }
registerNotificationChannels(applicationContext, DEFAULT_CLICK_ACTION)
Logger.tag(TAG).debug { "Breez foreground service created." }
}

override fun getConnectRequest(): ConnectRequest? {
val config = defaultConfig(LiquidNetwork.MAINNET)

config.workingDir = PathUtils.getDataDirectory(applicationContext)

return readSecuredValue(
applicationContext,
"${ELEMENT_PREFERENCES_KEY_PREFIX}_${ACCOUNT_MNEMONIC}"
)
?.let { mnemonic ->
ConnectRequest(config, mnemonic)
}
}

override fun log(l: LogEntry) {
when (l.level) {
"ERROR" -> Logger.tag(TAG).error { l.line }
"WARN" -> Logger.tag(TAG).warn { l.line }
"INFO" -> Logger.tag(TAG).info { l.line }
"DEBUG" -> Logger.tag(TAG).debug { l.line }
"TRACE" -> Logger.tag(TAG).trace { l.line }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.breez.liquid.l_breez

import android.content.Context
import io.flutter.util.PathUtils
import org.tinylog.kotlin.Logger
import java.io.File

class BreezLogger {
companion object {
private const val TAG = "BreezLogger"

private var isInit: Boolean? = null

internal fun configureLogger(applicationContext: Context): Boolean? {
synchronized(this) {
/** Get `/logs` folder from Flutter app data directory */
val loggingDir =
File(PathUtils.getDataDirectory(applicationContext), "/logs").apply {
/** Create a new directory denoted by the pathname and also
* all the non existent parent directories of the pathname */
mkdirs()
}

System.setProperty("tinylog.directory", loggingDir.absolutePath)
System.setProperty("tinylog.timestamp", System.currentTimeMillis().toString())

if (isInit == false) {
Logger.tag(TAG).debug { "Starting ${BuildConfig.APPLICATION_ID}..." }
Logger.tag(TAG).debug { "Logs directory: '$loggingDir'" }
isInit = true
}
return isInit
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.breez.liquid.l_breez.utils

import android.content.Context
import android.util.Base64
import java.nio.charset.Charset

class FlutterSecuredStorageHelper {
companion object {
@Throws(java.lang.Exception::class)
fun readSecuredValue(appContext: Context, key: String): String? {
val preferences = appContext.getSharedPreferences("FlutterSecureStorage", Context.MODE_PRIVATE)
val rawValue = preferences.getString(key, null)
return decodeRawValue(appContext, rawValue)
}

@Throws(java.lang.Exception::class)
private fun decodeRawValue(appContext: Context, value: String?): String? {
if (value == null) {
return null
}
val charset = Charset.forName("UTF-8")
val data: ByteArray = Base64.decode(value, 0)
val keyCipherAlgo = RSACipher18Implementation(context = appContext)
val storageCipher = StorageCipher18Implementation(appContext, keyCipherAlgo)
val result: ByteArray = storageCipher.decrypt(data)
return String(result, charset)
}
}
}
Loading

0 comments on commit cc3fefc

Please sign in to comment.