diff --git a/app/build.gradle b/app/build.gradle index 1a867113..34e0bc06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ apply plugin: 'com.google.gms.google-services' apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: 'kotlin-android' apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'org.jetbrains.kotlin.plugin.serialization' repositories { google() @@ -74,6 +75,10 @@ android { } } + buildFeatures { + buildConfig = true + } + applicationVariants.configureEach { variant -> variant.outputs.configureEach { output -> output.outputFileName = "bisq-${variant.name}.apk" @@ -125,7 +130,7 @@ dependencies { implementation 'com.google.firebase:firebase-analytics-ktx:22.1.2' implementation 'com.google.firebase:firebase-messaging:24.1.0' - implementation 'com.google.code.gson:gson:2.10.1' + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' diff --git a/app/src/androidTest/java/bisq/android/tests/NotificationTest.kt b/app/src/androidTest/java/bisq/android/tests/NotificationTest.kt index 4dd381ad..c515d100 100644 --- a/app/src/androidTest/java/bisq/android/tests/NotificationTest.kt +++ b/app/src/androidTest/java/bisq/android/tests/NotificationTest.kt @@ -40,9 +40,9 @@ import bisq.android.rules.ScreenshotRule import bisq.android.screens.NotificationTableScreen import bisq.android.services.BisqFirebaseMessagingService import bisq.android.util.CryptoUtil -import bisq.android.util.DateUtil import com.google.firebase.messaging.RemoteMessage -import com.google.gson.GsonBuilder +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -168,20 +168,17 @@ class NotificationTest { private fun buildBisqNotification(): BisqNotification { val now = Date() val tradeId = (100000..999999).random() - val bisqNotification = BisqNotification() - bisqNotification.type = NotificationType.TRADE.name - bisqNotification.title = "Trade confirmed" - bisqNotification.message = "The trade with ID $tradeId is confirmed." - bisqNotification.sentDate = now.time - 1000 * 60 - bisqNotification.receivedDate = now.time - return bisqNotification + return BisqNotification( + type = NotificationType.TRADE.name, + title = "Trade confirmed", + message = "The trade with ID $tradeId is confirmed.", + sentDate = now.time - 1000 * 60, + receivedDate = now.time + ) } private fun serializeNotificationPayload(bisqNotification: BisqNotification): String { - val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(Date::class.java, DateUtil()) - val gson = gsonBuilder.create() - return gson.toJson(bisqNotification) + return Json.encodeToString(bisqNotification) } private fun buildRemoteMessage(bisqNotification: BisqNotification): RemoteMessage { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b8d526a..37d4f45c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + . + */ + +package bisq.android + +import android.util.Log +import bisq.android.database.DebugLog +import bisq.android.database.DebugLogLevel +import bisq.android.database.DebugLogRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class Logging { + companion object { + private const val TAG = "Logging" + } + + // Attempt to initialize debugRepository with the application context. + // If the context is unavailable (e.g., in unit tests where Application is not initialized), + // catch the exception and log a warning instead of throwing a NullPointerException. + // This ensures that tests still work since trying to provide a mocked context is not straight forward. + @Suppress("SwallowedException", "TooGenericExceptionCaught") + private val debugRepository: DebugLogRepository? = try { + val context = Application.applicationContext() + DebugLogRepository(context) + } catch (e: NullPointerException) { + Log.w(TAG, "Skipping debugRepository initialization due to missing context") + null + } + + fun debug(tag: String, msg: String) { + Log.d(tag, msg) + insert(DebugLogLevel.DEBUG, msg) + } + + fun info(tag: String, msg: String) { + Log.i(tag, msg) + insert(DebugLogLevel.INFO, msg) + } + + fun warn(tag: String, msg: String) { + Log.w(tag, msg) + insert(DebugLogLevel.WARN, msg) + } + + fun error(tag: String, msg: String) { + Log.e(tag, msg) + insert(DebugLogLevel.ERROR, msg) + } + + private fun insert(level: DebugLogLevel, msg: String) { + debugRepository?.let { + CoroutineScope(Dispatchers.IO).launch { + it.insert( + DebugLog( + timestamp = System.currentTimeMillis(), + level = level, + text = msg + ) + ) + } + } ?: Log.w(TAG, "Skipping log insert; debugRepository is unavailable") + } +} diff --git a/app/src/main/java/bisq/android/database/BisqNotification.kt b/app/src/main/java/bisq/android/database/BisqNotification.kt index 23dd70d0..84c80876 100644 --- a/app/src/main/java/bisq/android/database/BisqNotification.kt +++ b/app/src/main/java/bisq/android/database/BisqNotification.kt @@ -20,49 +20,39 @@ package bisq.android.database import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable @Entity +@Serializable data class BisqNotification( @PrimaryKey(autoGenerate = true) - @SerializedName("uid") var uid: Int = 0, @ColumnInfo(name = "version") - @SerializedName("version") var version: Int = 0, @ColumnInfo(name = "type") - @SerializedName("type") var type: String? = null, @ColumnInfo(name = "title") - @SerializedName("title") var title: String? = null, @ColumnInfo(name = "message") - @SerializedName("message") var message: String? = null, @ColumnInfo(name = "actionRequired") - @SerializedName("actionRequired") var actionRequired: String? = null, @ColumnInfo(name = "txId") - @SerializedName("txId") var txId: String? = null, @ColumnInfo(name = "receivedDate") - @SerializedName("receivedDate") var receivedDate: Long = 0, @ColumnInfo(name = "sentDate") - @SerializedName("sentDate") var sentDate: Long = 0, @ColumnInfo(name = "read") - @SerializedName("read") var read: Boolean = false ) { override fun equals(other: Any?): Boolean { @@ -81,8 +71,4 @@ data class BisqNotification( return listOf(version, type, title, message, actionRequired, txId, sentDate) .hashCode() } - - override fun toString(): String { - return "BisqNotification[" + Gson().toJson(this) + "]" - } } diff --git a/app/src/main/java/bisq/android/database/DebugLog.kt b/app/src/main/java/bisq/android/database/DebugLog.kt new file mode 100644 index 00000000..26b1d2a2 --- /dev/null +++ b/app/src/main/java/bisq/android/database/DebugLog.kt @@ -0,0 +1,35 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.database + +import androidx.room.Entity +import androidx.room.PrimaryKey +import bisq.android.util.DateUtil + +@Entity +data class DebugLog( + @PrimaryKey(autoGenerate = true) + var id: Int = 0, + var timestamp: Long, + var level: DebugLogLevel, + var text: String +) { + override fun toString(): String { + return "${DateUtil.format(timestamp)} [$level] $text" + } +} diff --git a/app/src/main/java/bisq/android/database/DebugLogDao.kt b/app/src/main/java/bisq/android/database/DebugLogDao.kt new file mode 100644 index 00000000..303d850b --- /dev/null +++ b/app/src/main/java/bisq/android/database/DebugLogDao.kt @@ -0,0 +1,35 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.database + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query + +@Dao +interface DebugLogDao { + @get:Query("SELECT * FROM DebugLog ORDER BY timestamp DESC") + val all: LiveData> + + @Insert + suspend fun insert(debugLog: DebugLog): Long + + @Query("DELETE FROM DebugLog") + suspend fun deleteAll() +} diff --git a/app/src/main/java/bisq/android/database/DebugLogDatabase.kt b/app/src/main/java/bisq/android/database/DebugLogDatabase.kt new file mode 100644 index 00000000..dbcfa9b0 --- /dev/null +++ b/app/src/main/java/bisq/android/database/DebugLogDatabase.kt @@ -0,0 +1,48 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [DebugLog::class], version = 1, exportSchema = false) +abstract class DebugLogDatabase : RoomDatabase() { + + abstract fun debugLogDao(): DebugLogDao + + companion object { + + private var instance: DebugLogDatabase? = null + + fun getDatabase(context: Context): DebugLogDatabase { + if (instance == null) { + synchronized(DebugLogDatabase::class.java) { + if (instance == null) { + instance = Room.databaseBuilder( + context.applicationContext, + DebugLogDatabase::class.java, "debug.db" + ).build() + } + } + } + return instance!! + } + } +} diff --git a/app/src/main/java/bisq/android/database/DebugLogLevel.kt b/app/src/main/java/bisq/android/database/DebugLogLevel.kt new file mode 100644 index 00000000..2c9c1ea0 --- /dev/null +++ b/app/src/main/java/bisq/android/database/DebugLogLevel.kt @@ -0,0 +1,25 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.database + +enum class DebugLogLevel { + DEBUG, + INFO, + WARN, + ERROR +} diff --git a/app/src/main/java/bisq/android/database/DebugLogRepository.kt b/app/src/main/java/bisq/android/database/DebugLogRepository.kt new file mode 100644 index 00000000..26f6f6c3 --- /dev/null +++ b/app/src/main/java/bisq/android/database/DebugLogRepository.kt @@ -0,0 +1,46 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.database + +import android.content.Context +import androidx.lifecycle.LiveData +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +class DebugLogRepository(context: Context) { + + private val debugLogDao: DebugLogDao + + val allLogs: LiveData> + + init { + val db = DebugLogDatabase.getDatabase(context) + debugLogDao = db.debugLogDao() + allLogs = debugLogDao.all + } + + suspend fun insert(debugLog: DebugLog) = coroutineScope { + launch { + debugLogDao.insert(debugLog) + } + } + + suspend fun deleteAll() = coroutineScope { + launch { debugLogDao.deleteAll() } + } +} diff --git a/app/src/main/java/bisq/android/database/NotificationDatabase.kt b/app/src/main/java/bisq/android/database/NotificationDatabase.kt index 6cbb1de6..2c3b6c78 100644 --- a/app/src/main/java/bisq/android/database/NotificationDatabase.kt +++ b/app/src/main/java/bisq/android/database/NotificationDatabase.kt @@ -21,11 +21,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import bisq.android.util.DateUtil @Database(entities = [BisqNotification::class], version = 1, exportSchema = false) -@TypeConverters(DateUtil::class) abstract class NotificationDatabase : RoomDatabase() { abstract fun bisqNotificationDao(): BisqNotificationDao diff --git a/app/src/main/java/bisq/android/services/BisqFirebaseMessagingService.kt b/app/src/main/java/bisq/android/services/BisqFirebaseMessagingService.kt index b7ff35d3..18074f41 100644 --- a/app/src/main/java/bisq/android/services/BisqFirebaseMessagingService.kt +++ b/app/src/main/java/bisq/android/services/BisqFirebaseMessagingService.kt @@ -19,15 +19,16 @@ package bisq.android.services import android.content.Context import android.content.Intent -import android.util.Log import bisq.android.Application import bisq.android.Application.Companion.isAppInBackground +import bisq.android.Logging import bisq.android.R import bisq.android.database.BisqNotification import bisq.android.model.Device import bisq.android.model.DeviceStatus import bisq.android.ui.notification.NotificationSender import bisq.android.ui.welcome.WelcomeActivity +import bisq.android.util.MaskingUtil.maskSensitive import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailabilityLight import com.google.android.gms.tasks.OnCompleteListener @@ -62,7 +63,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { fun fetchFcmToken(onComplete: () -> Unit = {}) { if (!isFirebaseMessagingInitialized()) { - Log.e(TAG, "FirebaseMessaging is not initialized") + Logging().error(TAG, "FirebaseMessaging is not initialized") onComplete() return } @@ -71,7 +72,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { token.addOnCompleteListener( OnCompleteListener { getTokenTask -> if (!getTokenTask.isSuccessful) { - Log.e(TAG, "Fetching FCM token failed: ${getTokenTask.exception}") + Logging().error(TAG, "Fetching FCM token failed: ${getTokenTask.exception}") Device.instance.token = null onComplete() tokenBeingFetched = false @@ -79,21 +80,21 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { } val token: String? = getTokenTask.result if (token == null) { - Log.e(TAG, "FCM token is null") + Logging().error(TAG, "FCM token is null") Device.instance.token = null onComplete() tokenBeingFetched = false return@OnCompleteListener } if (Device.instance.token == token) { - Log.i(TAG, "FCM token has already been fetched") + Logging().info(TAG, "FCM token has already been fetched") onComplete() tokenBeingFetched = false return@OnCompleteListener } Device.instance.newToken(token) - Log.i(TAG, "New FCM token: $token") - Log.i(TAG, "Pairing token: ${Device.instance.pairingToken()}") + Logging().info(TAG, "New FCM token: ${maskSensitive(token)}") + Logging().info(TAG, "Pairing token: ${maskSensitive(Device.instance.pairingToken())}") onComplete() tokenBeingFetched = false } @@ -104,7 +105,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { @Suppress("ForbiddenComment") fun refreshFcmToken(onComplete: () -> Unit = {}) { if (!isFirebaseMessagingInitialized()) { - Log.e(TAG, "FirebaseMessaging is not initialized") + Logging().error(TAG, "FirebaseMessaging is not initialized") onComplete() return } @@ -121,9 +122,9 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { // FirebaseMessaging.getInstance().apply { // deleteToken().addOnCompleteListener { deleteTokenTask -> // if (!deleteTokenTask.isSuccessful) { -// Log.e(TAG, "Deleting FCM token failed: ${deleteTokenTask.exception}") +// Logging().error(TAG, "Deleting FCM token failed: ${deleteTokenTask.exception}") // } else { -// Log.i(TAG, "FCM token deleted") +// Logging().debug(TAG, "FCM token deleted") // } // fetchFcmToken(onComplete) // tokenBeingFetched = false @@ -138,12 +139,12 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { * For more details, see https://firebase.google.com/docs/cloud-messaging/android/receive. */ override fun onMessageReceived(remoteMessage: RemoteMessage) { - Log.i(TAG, "Message received") + Logging().debug(TAG, "Message received") super.onMessageReceived(remoteMessage) val encryptedData = remoteMessage.data["encrypted"] if (encryptedData == null) { - Log.w(TAG, "Message does not contain encrypted data; ${remoteMessage.data}") + Logging().warn(TAG, "Received message does not contain encrypted data; ${remoteMessage.data}") return } @@ -151,7 +152,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { // If the message contains notification data, then this method is only called while the app is in // the foreground. Since the app is running and the NotificationReceiver should be registered, only // need to broadcast the notification so the NotificationReceiver can process it. - Log.i( + Logging().debug( TAG, "Notification message received, broadcasting " + getString(R.string.notification_receiver_action) ) @@ -166,7 +167,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { // is in the foreground or background. The NotificationReceiver may not be registered if the app is in the // background, so cannot simply broadcast the notification. Instead, send it directly to the // NotificationReceiver. - Log.i(TAG, "Data message received") + Logging().debug(TAG, "Data message received") Intent().also { notificationIntent -> notificationIntent.putExtra( @@ -192,7 +193,7 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { return try { NotificationProcessor.processNotification(encryptedData) } catch (e: ProcessingException) { - e.message?.let { Log.e(TAG, it) } + e.message?.let { Logging().error(TAG, it) } null } } @@ -212,7 +213,10 @@ class BisqFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(newToken: String) { super.onNewToken(newToken) if (Device.instance.readFromPreferences(this)) { - Log.i(TAG, "New FCM token received, app needs to be re-paired: $newToken") + Logging().info( + TAG, + "New FCM token received, app needs to be re-paired: ${maskSensitive(newToken)}" + ) Device.instance.reset() Device.instance.clearPreferences(this) Device.instance.status = DeviceStatus.NEEDS_REPAIR diff --git a/app/src/main/java/bisq/android/services/IntentReceiver.kt b/app/src/main/java/bisq/android/services/IntentReceiver.kt index 566101a0..7a8082d2 100644 --- a/app/src/main/java/bisq/android/services/IntentReceiver.kt +++ b/app/src/main/java/bisq/android/services/IntentReceiver.kt @@ -21,8 +21,8 @@ import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.util.Log import android.widget.Toast +import bisq.android.Logging import bisq.android.R import bisq.android.model.Device import bisq.android.model.DeviceStatus @@ -37,12 +37,12 @@ class IntentReceiver(private val activity: Activity? = null) : BroadcastReceiver @Suppress("ReturnCount") override fun onReceive(context: Context, intent: Intent) { - Log.i(TAG, "Intent received") + Logging().debug(TAG, "Intent received") if (intent.action == null || !intent.action.equals(context.getString(R.string.intent_receiver_action)) ) { - Log.i( + Logging().debug( TAG, "Ignoring intent, action is not " + context.getString(R.string.intent_receiver_action) ) @@ -51,26 +51,26 @@ class IntentReceiver(private val activity: Activity? = null) : BroadcastReceiver if (intent.hasExtra("error")) { val errorMessage = intent.getStringExtra("error") - Log.e(TAG, errorMessage!!) + Logging().error(TAG, errorMessage!!) Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() return } if (!intent.hasExtra("type")) { - Log.i(TAG, "Ignoring intent, missing notification type") + Logging().debug(TAG, "Ignoring intent, missing notification type") return } val type = intent.getStringExtra("type") if (type == NotificationType.SETUP_CONFIRMATION.name && activity is UnpairedBaseActivity) { - Log.i(TAG, "Pairing confirmed") + Logging().debug(TAG, "Pairing confirmed") activity.pairingConfirmed() } else if (type == NotificationType.ERASE.name && activity is PairedBaseActivity) { - Log.i(TAG, "Pairing removed") + Logging().debug(TAG, "Pairing removed") Device.instance.status = DeviceStatus.UNPAIRED activity.pairingRemoved(context.getString(R.string.pairing_erased)) } else { - Log.i(TAG, "Ignoring $type notification") + Logging().debug(TAG, "Ignoring $type notification") } } } diff --git a/app/src/main/java/bisq/android/services/NotificationHandler.kt b/app/src/main/java/bisq/android/services/NotificationHandler.kt index 368cfa7a..0032ec08 100644 --- a/app/src/main/java/bisq/android/services/NotificationHandler.kt +++ b/app/src/main/java/bisq/android/services/NotificationHandler.kt @@ -19,9 +19,10 @@ package bisq.android.services import android.content.Context import android.content.Intent -import android.util.Log +import bisq.android.Logging import bisq.android.R import bisq.android.database.BisqNotification +import bisq.android.database.DebugLogRepository import bisq.android.database.NotificationRepository import bisq.android.model.Device import bisq.android.model.DeviceStatus @@ -35,40 +36,46 @@ object NotificationHandler { @Suppress("ReturnCount") suspend fun handleNotification(bisqNotification: BisqNotification, context: Context) { val notificationRepository = NotificationRepository(context) + val debugRepository = DebugLogRepository(context) when (bisqNotification.type) { NotificationType.SETUP_CONFIRMATION.name -> { - Log.i(TAG, "Setup confirmation") + Logging().debug(TAG, "Setup confirmation") if (Device.instance.token == null) { - Log.e(TAG, "Device token is null") + Logging().error(TAG, "Device token is null") return } if (Device.instance.key == null) { - Log.e(TAG, "Device key is null") + Logging().error(TAG, "Device key is null") return } if (Device.instance.status == DeviceStatus.PAIRED) { - Log.w(TAG, "Device is already paired") + Logging().warn(TAG, "Device is already paired") return } Device.instance.status = DeviceStatus.PAIRED Device.instance.saveToPreferences(context) } NotificationType.ERASE.name -> { - Log.i(TAG, "Erase pairing") + Logging().debug(TAG, "Erase pairing") Device.instance.reset() Device.instance.clearPreferences(context) notificationRepository.deleteAll() + debugRepository.deleteAll() Device.instance.status = DeviceStatus.REMOTE_ERASED refreshFcmToken() } + + null -> { + Logging().error(TAG, "Notification type is null: $bisqNotification") + } else -> { - Log.i(TAG, "Inserting ${bisqNotification.type} notification to repository") + Logging().debug(TAG, "Inserting ${bisqNotification.type} notification to repository") notificationRepository.insert(bisqNotification) } } - Log.i(TAG, "Broadcasting " + context.getString(R.string.intent_receiver_action)) + Logging().debug(TAG, "Broadcasting " + context.getString(R.string.intent_receiver_action)) Intent().also { broadcastIntent -> broadcastIntent.action = context.getString(R.string.intent_receiver_action) broadcastIntent.putExtra("type", bisqNotification.type) diff --git a/app/src/main/java/bisq/android/services/NotificationProcessor.kt b/app/src/main/java/bisq/android/services/NotificationProcessor.kt index f7869955..fb7eadc4 100644 --- a/app/src/main/java/bisq/android/services/NotificationProcessor.kt +++ b/app/src/main/java/bisq/android/services/NotificationProcessor.kt @@ -17,15 +17,14 @@ package bisq.android.services -import android.util.Log +import bisq.android.Logging import bisq.android.database.BisqNotification import bisq.android.model.Device import bisq.android.model.NotificationMessage import bisq.android.model.NotificationMessage.Companion.BISQ_MESSAGE_ANDROID_MAGIC import bisq.android.util.CryptoUtil -import bisq.android.util.DateUtil -import com.google.gson.GsonBuilder -import com.google.gson.JsonSyntaxException +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json import java.text.ParseException import java.util.Date @@ -43,14 +42,16 @@ object NotificationProcessor { notificationMessage.encryptedPayload, notificationMessage.initializationVector ) + Logging().debug(TAG, "Deserializing decrypted notification payload: $decryptedNotificationPayload") val bisqNotification = deserializeNotificationPayload(decryptedNotificationPayload) bisqNotification.receivedDate = Date().time + Logging().debug(TAG, "Deserialized notification payload: $bisqNotification") return bisqNotification } catch (e: Throwable) { when (e) { is ParseException, is DecryptingException, is DeserializationException -> { val message = "Failed to process notification; ${e.message}" - Log.e(TAG, message) + Logging().error(TAG, message) throw ProcessingException(message) } else -> throw e @@ -97,7 +98,7 @@ object NotificationProcessor { is IllegalArgumentException, is CryptoUtil.Companion.CryptoException -> { val message = "Failed to decrypt notification payload" - Log.e(TAG, "$message: $encryptedPayload") + Logging().error(TAG, "$message: $encryptedPayload") throw DecryptingException(message) } else -> throw e @@ -107,14 +108,11 @@ object NotificationProcessor { @Throws(DeserializationException::class) fun deserializeNotificationPayload(decryptedPayload: String): BisqNotification { - val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(Date::class.java, DateUtil()) - val gson = gsonBuilder.create() try { - return gson.fromJson(decryptedPayload, BisqNotification::class.java) - } catch (e: JsonSyntaxException) { - val message = "Failed to deserialize notification payload" - Log.e(TAG, "$message: $decryptedPayload") + return Json.decodeFromString(decryptedPayload) + } catch (e: SerializationException) { + val message = "Failed to deserialize notification payload, ${e.message}" + Logging().error(TAG, "$message: $decryptedPayload") throw DeserializationException(message) } } diff --git a/app/src/main/java/bisq/android/services/NotificationReceiver.kt b/app/src/main/java/bisq/android/services/NotificationReceiver.kt index 6657646e..28c8dfdf 100644 --- a/app/src/main/java/bisq/android/services/NotificationReceiver.kt +++ b/app/src/main/java/bisq/android/services/NotificationReceiver.kt @@ -20,7 +20,7 @@ package bisq.android.services import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.util.Log +import bisq.android.Logging import bisq.android.R import bisq.android.database.BisqNotification import bisq.android.ext.goAsync @@ -31,22 +31,23 @@ class NotificationReceiver : BroadcastReceiver() { private const val TAG = "NotificationReceiver" } + @Suppress("ReturnCount") override fun onReceive(context: Context, intent: Intent) { - Log.i(TAG, "Notification received") + Logging().debug(TAG, "Notification received") if (Device.instance.key == null) { - Log.w(TAG, "Ignoring notification, device does not have a key") + Logging().warn(TAG, "Ignoring received notification, device does not have a key") return } - Log.i(TAG, "Processing notification") + Logging().debug(TAG, "Processing notification") val bisqNotification: BisqNotification try { bisqNotification = NotificationProcessor.processNotification( intent.extras?.getString("encrypted").toString() ) } catch (e: ProcessingException) { - e.message?.let { Log.e(TAG, it) } + e.message?.let { Logging().error(TAG, it) } Intent().also { broadcastIntent -> broadcastIntent.action = context.getString(R.string.intent_receiver_action) broadcastIntent.putExtra( @@ -58,7 +59,12 @@ class NotificationReceiver : BroadcastReceiver() { return } - Log.i(TAG, "Handling ${bisqNotification.type} notification") + if (bisqNotification.type == null) { + Logging().error(TAG, "Notification type is null: $bisqNotification") + return + } + + Logging().debug(TAG, "Handling ${bisqNotification.type} notification") goAsync { NotificationHandler.handleNotification(bisqNotification, context) } diff --git a/app/src/main/java/bisq/android/ui/debug/DebugActivity.kt b/app/src/main/java/bisq/android/ui/debug/DebugActivity.kt new file mode 100644 index 00000000..35445ab4 --- /dev/null +++ b/app/src/main/java/bisq/android/ui/debug/DebugActivity.kt @@ -0,0 +1,114 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.ui.debug + +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import android.widget.Switch +import android.widget.TextView +import androidx.lifecycle.ViewModelProvider +import bisq.android.R +import bisq.android.database.DebugLogLevel +import bisq.android.model.Device +import bisq.android.ui.BaseActivity + +class DebugActivity : BaseActivity() { + private lateinit var viewModel: DebugViewModel + private lateinit var deviceStatusText: TextView + private lateinit var showDebugLogsLabel: TextView + private lateinit var showDebugLogsSwitch: Switch + private lateinit var logText: TextView + private lateinit var clearLogButton: Button + private lateinit var sendLogButton: Button + + private var showDebugLogs: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel = ViewModelProvider(this)[DebugViewModel::class.java] + + initView() + } + + override fun onStart() { + super.onStart() + viewModel.allLogs.observe(this) { _ -> + updateView() + } + } + + private fun initView() { + setContentView(R.layout.activity_debug) + + deviceStatusText = bind(R.id.device_status_value) + deviceStatusText.text = Device.instance.status.toString() + + showDebugLogsSwitch = bind(R.id.show_debug_log_switch) + showDebugLogsSwitch.setOnCheckedChangeListener { _, isChecked -> + showDebugLogs = isChecked + updateView() + } + + showDebugLogsLabel = bind(R.id.show_debug_log_label) + showDebugLogsLabel.setOnClickListener { + showDebugLogsSwitch.isChecked = !showDebugLogsSwitch.isChecked + } + + logText = bind(R.id.log_text) + + clearLogButton = bind(R.id.clear_log_button) + clearLogButton.setOnClickListener { + onClearLog() + } + + sendLogButton = bind(R.id.send_log_button) + sendLogButton.setOnClickListener { + onSendLog() + } + } + + private fun updateView() { + val allLogs = viewModel.allLogs.value ?: emptyList() + + val displayedLogs = if (!showDebugLogs) { + allLogs.filter { log -> log.level != DebugLogLevel.DEBUG } + } else { + allLogs + } + + logText.text = displayedLogs.joinToString(separator = "\n----------------------------------------------\n") { + it.toString() + } + } + + private fun onClearLog() { + viewModel.nukeTable() + } + + private fun onSendLog() { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, getString(R.string.send_log_subject)) + putExtra(Intent.EXTRA_TEXT, logText.text) + } + startActivity(Intent.createChooser(sendIntent, getString(R.string.send_log))) + } +} diff --git a/app/src/main/java/bisq/android/ui/debug/DebugViewModel.kt b/app/src/main/java/bisq/android/ui/debug/DebugViewModel.kt new file mode 100644 index 00000000..a8eaed4b --- /dev/null +++ b/app/src/main/java/bisq/android/ui/debug/DebugViewModel.kt @@ -0,0 +1,41 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.ui.debug + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import bisq.android.database.DebugLog +import bisq.android.database.DebugLogRepository +import kotlinx.coroutines.launch + +class DebugViewModel(application: Application) : AndroidViewModel(application) { + + private val repository: DebugLogRepository = DebugLogRepository(application) + + var allLogs: LiveData> = repository.allLogs + + fun insert(debugLog: DebugLog) = viewModelScope.launch { + repository.insert(debugLog) + } + + fun nukeTable() = viewModelScope.launch { + repository.deleteAll() + } +} diff --git a/app/src/main/java/bisq/android/ui/notification/NotificationSender.kt b/app/src/main/java/bisq/android/ui/notification/NotificationSender.kt index d7d7ed9e..1aea27c7 100644 --- a/app/src/main/java/bisq/android/ui/notification/NotificationSender.kt +++ b/app/src/main/java/bisq/android/ui/notification/NotificationSender.kt @@ -22,11 +22,11 @@ import android.app.PendingIntent import android.content.Intent import android.content.pm.PackageManager import android.media.RingtoneManager -import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import bisq.android.Application +import bisq.android.Logging import bisq.android.R import java.util.Date @@ -42,7 +42,7 @@ object NotificationSender { Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { - Log.d(TAG, "*** Unable to send notification; POST_NOTIFICATIONS permission not granted") + Logging().warn(TAG, "*** Unable to send notification; POST_NOTIFICATIONS permission not granted") return } diff --git a/app/src/main/java/bisq/android/ui/notification/NotificationTableActivity.kt b/app/src/main/java/bisq/android/ui/notification/NotificationTableActivity.kt index 7626e31f..01a336e9 100644 --- a/app/src/main/java/bisq/android/ui/notification/NotificationTableActivity.kt +++ b/app/src/main/java/bisq/android/ui/notification/NotificationTableActivity.kt @@ -193,10 +193,12 @@ class NotificationTableActivity : PairedBaseActivity() { @Suppress("MagicNumber") private fun addExampleNotifications() { for (counter in 1..5) { - val now = Date() - val bisqNotification = BisqNotification() - bisqNotification.receivedDate = now.time + counter * 1000 - bisqNotification.sentDate = bisqNotification.receivedDate - 1000 * 30 + val receivedDate = Date().time + counter * 1000 + val bisqNotification = BisqNotification( + type = "", + receivedDate = receivedDate, + sentDate = receivedDate - 1000 * 30 + ) when (counter) { 1 -> { bisqNotification.type = NotificationType.TRADE.name diff --git a/app/src/main/java/bisq/android/ui/settings/SettingsFragment.kt b/app/src/main/java/bisq/android/ui/settings/SettingsFragment.kt index 70d2130d..c11a6db8 100644 --- a/app/src/main/java/bisq/android/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/bisq/android/ui/settings/SettingsFragment.kt @@ -28,6 +28,7 @@ import androidx.preference.PreferenceFragmentCompat import bisq.android.Application import bisq.android.BISQ_MOBILE_URL import bisq.android.BISQ_NETWORK_URL +import bisq.android.BuildConfig import bisq.android.R import bisq.android.model.Device import bisq.android.model.DeviceStatus @@ -35,6 +36,8 @@ import bisq.android.services.BisqFirebaseMessagingService import bisq.android.ui.DialogBuilder import bisq.android.ui.ThemeProvider import bisq.android.ui.UiUtil.loadWebPage +import bisq.android.ui.debug.DebugActivity +import bisq.android.ui.debug.DebugViewModel import bisq.android.ui.notification.NotificationViewModel import bisq.android.ui.pairing.PairingScanActivity import bisq.android.ui.welcome.WelcomeActivity @@ -57,15 +60,20 @@ class SettingsFragment : PreferenceFragmentCompat() { private val aboutAppPreference by lazy { findPreference(getString(R.string.about_app_preferences_key)) } + private val debugPreference by lazy { + findPreference(getString(R.string.debug_preferences_key)) + } private val versionPreference by lazy { findPreference(getString(R.string.version_preferences_key)) } - private lateinit var viewModel: NotificationViewModel + private lateinit var notificationViewModel: NotificationViewModel + private lateinit var debugViewModel: DebugViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this)[NotificationViewModel::class.java] + notificationViewModel = ViewModelProvider(this)[NotificationViewModel::class.java] + debugViewModel = ViewModelProvider(this)[DebugViewModel::class.java] } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -75,6 +83,7 @@ class SettingsFragment : PreferenceFragmentCompat() { setScanPairingTokenPreference() setAboutBisqPreference() setAboutAppPreference() + setDebugPreference() setVersionPreference() } @@ -114,7 +123,8 @@ class SettingsFragment : PreferenceFragmentCompat() { { _, _ -> Device.instance.reset() Device.instance.clearPreferences(context) - viewModel.nukeTable() + notificationViewModel.nukeTable() + debugViewModel.nukeTable() Device.instance.status = DeviceStatus.ERASED BisqFirebaseMessagingService.refreshFcmToken() val intent = Intent(context, WelcomeActivity::class.java) @@ -145,6 +155,19 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + private fun setDebugPreference() { + if (!BuildConfig.DEBUG) { + debugPreference?.let { preferenceScreen.removePreference(it) } + } else { + debugPreference?.setOnPreferenceClickListener { + this.context?.let { context -> + startActivity(Intent(context, DebugActivity::class.java)) + } + true + } + } + } + private fun setVersionPreference() { versionPreference?.title = getString(R.string.version, Application.getAppVersion()) } diff --git a/app/src/main/java/bisq/android/ui/welcome/WelcomeActivity.kt b/app/src/main/java/bisq/android/ui/welcome/WelcomeActivity.kt index 00076b1b..4102aae7 100644 --- a/app/src/main/java/bisq/android/ui/welcome/WelcomeActivity.kt +++ b/app/src/main/java/bisq/android/ui/welcome/WelcomeActivity.kt @@ -22,13 +22,14 @@ import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Log import android.view.View import android.widget.Button import android.widget.ProgressBar import android.widget.Toast import androidx.core.content.ContextCompat import bisq.android.BISQ_MOBILE_URL +import bisq.android.BuildConfig +import bisq.android.Logging import bisq.android.R import bisq.android.model.Device import bisq.android.model.DeviceStatus @@ -38,11 +39,13 @@ import bisq.android.services.BisqFirebaseMessagingService.Companion.isTokenBeing import bisq.android.ui.DialogBuilder import bisq.android.ui.UiUtil.loadWebPage import bisq.android.ui.UnpairedBaseActivity +import bisq.android.ui.debug.DebugActivity import bisq.android.ui.notification.NotificationTableActivity import bisq.android.ui.pairing.PairingScanActivity @Suppress("TooManyFunctions") class WelcomeActivity : UnpairedBaseActivity() { + private lateinit var debugButton: Button private lateinit var learnMoreButton: Button private lateinit var pairButton: Button private lateinit var progressBar: ProgressBar @@ -104,6 +107,23 @@ class WelcomeActivity : UnpairedBaseActivity() { private fun initView() { setContentView(R.layout.activity_welcome) + debugButton = bind(R.id.welcome_debug_button) + debugButton.setOnClickListener { + startActivity( + Intent( + this, + DebugActivity::class.java + ) + ) + } + if (BuildConfig.DEBUG) { + debugButton.visibility = View.VISIBLE + debugButton.isEnabled = true + } else { + debugButton.visibility = View.GONE + debugButton.isEnabled = false + } + pairButton = bind(R.id.welcome_pair_button) if (isGooglePlayServicesAvailable(this)) { pairButton.setOnClickListener { @@ -144,7 +164,7 @@ class WelcomeActivity : UnpairedBaseActivity() { } private fun fetchFcmToken(onFetchFcmTokenComplete: () -> Unit = {}) { - Log.i(TAG, "Fetching FCM token") + Logging().info(TAG, "Fetching FCM token") disablePairButton() BisqFirebaseMessagingService.fetchFcmToken { enablePairButton() @@ -194,10 +214,10 @@ class WelcomeActivity : UnpairedBaseActivity() { private fun maybeProcessOpenedNotification() { val extras = intent.extras if (extras != null) { - Log.i(TAG, "Processing opened notification") + Logging().debug(TAG, "Processing opened notification") val notificationMessage = extras.getString("encrypted") if (notificationMessage != null) { - Log.i(TAG, "Broadcasting " + getString(R.string.notification_receiver_action)) + Logging().debug(TAG, "Broadcasting " + getString(R.string.notification_receiver_action)) Intent().also { broadcastIntent -> broadcastIntent.action = getString(R.string.notification_receiver_action) broadcastIntent.flags = Intent.FLAG_INCLUDE_STOPPED_PACKAGES diff --git a/app/src/main/java/bisq/android/util/DateUtil.kt b/app/src/main/java/bisq/android/util/DateUtil.kt index 75f10957..8f6c5cb5 100644 --- a/app/src/main/java/bisq/android/util/DateUtil.kt +++ b/app/src/main/java/bisq/android/util/DateUtil.kt @@ -17,63 +17,22 @@ package bisq.android.util -import android.util.Log -import androidx.room.TypeConverter -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import java.lang.reflect.Type -import java.text.ParseException import java.text.SimpleDateFormat -import java.util.Date import java.util.Locale import java.util.TimeZone -class DateUtil : JsonDeserializer { - - companion object { - private const val TAG = "DateDeserializer" - private val LOCALE = Locale.US - private const val PATTERN = "yyyy-MM-dd HH:mm:ss" - - fun format( - date: Long, - locale: Locale = LOCALE, - pattern: String = PATTERN, - timezone: TimeZone = TimeZone.getDefault() - ): String? { - val formatter = SimpleDateFormat(pattern, locale) - formatter.timeZone = timezone - return formatter.format(date) - } - } - - @TypeConverter - fun toDate(value: Long?): Date? { - return if (value == null) null else Date(value) - } - - @TypeConverter - fun toLong(value: Date?): Long? { - return value?.time - } - - @Throws(JsonParseException::class) - override fun deserialize( - element: JsonElement, - arg1: Type?, - arg2: JsonDeserializationContext? - ): Date? { - val date = element.asString - val formatter = SimpleDateFormat(PATTERN, LOCALE) - formatter.timeZone = TimeZone.getDefault() - - return try { - formatter.parse(date) - } catch (e: ParseException) { - Log.e(TAG, "Failed to parse date: $e") - null - } +object DateUtil { + private val LOCALE = Locale.US + private const val PATTERN = "yyyy-MM-dd HH:mm:ss" + + fun format( + date: Long, + locale: Locale = LOCALE, + pattern: String = PATTERN, + timezone: TimeZone = TimeZone.getDefault() + ): String? { + val formatter = SimpleDateFormat(pattern, locale) + formatter.timeZone = timezone + return formatter.format(date) } } diff --git a/app/src/main/java/bisq/android/util/MaskingUtil.kt b/app/src/main/java/bisq/android/util/MaskingUtil.kt new file mode 100644 index 00000000..f64011bf --- /dev/null +++ b/app/src/main/java/bisq/android/util/MaskingUtil.kt @@ -0,0 +1,37 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.android.util + +object MaskingUtil { + @Suppress("ReturnCount") + fun maskSensitive(value: String?, visibleChars: Int = 5, maskChar: Char = '*'): String { + if (value.isNullOrEmpty()) return value ?: "" + + // If the value is too short to be masked, mask the whole thing + val minVisible = visibleChars * 2 + if (value.length <= minVisible) { + return maskChar.toString().repeat(value.length) + } + + val firstPart = value.take(visibleChars) + val lastPart = value.takeLast(visibleChars) + val maskedMiddle = maskChar.toString().repeat(value.length - (visibleChars * 2)) + + return "$firstPart$maskedMiddle$lastPart" + } +} diff --git a/app/src/main/res/drawable/ic_debug_24.xml b/app/src/main/res/drawable/ic_debug_24.xml new file mode 100644 index 00000000..be93fc8d --- /dev/null +++ b/app/src/main/res/drawable/ic_debug_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml new file mode 100644 index 00000000..08ee8e19 --- /dev/null +++ b/app/src/main/res/layout/activity_debug.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + +