diff --git a/.gitignore b/.gitignore index 1ba74ba906efeef82dca9a1fc057124d168aafd1..7b3442280741b856b539b31c5ce3cc61c210532e 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,5 @@ proguard-project.txt # Android Studio/IDEA *.iml .idea -.kotlin \ No newline at end of file + +.kotlin diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 306ffa5c41a28d17fbb1135ee0430a8eea9b12c1..ff442f308e29802b6465beb25a3632439398342e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,11 @@ image: "registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:latest" stages: -- build + - quality + - build + +variables: + SENTRY_DSN: $SENTRY_DSN before_script: - export GRADLE_USER_HOME=$(pwd)/.gradle @@ -12,10 +16,33 @@ cache: paths: - .gradle/ +quality: + stage: quality + script: + - ./gradlew spotlessCheck + - ./gradlew lintFdroidRelease + - ./gradlew detekt + - ./gradlew :notificationsreceiver:testDebugUnitTest :notificationsreceiver-domain:test + - ./gradlew :app:assembleFdroidRelease + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: on_success + - if: $CI_COMMIT_BRANCH == "main" + when: on_success + - when: never + artifacts: + paths: + - ./**/build/reports/tests/testDebugUnitTest + - app/build/outputs/apk/ + buildRelease: stage: build script: - ./gradlew assembleFdroid + rules: + - if: $CI_COMMIT_BRANCH == "main" + when: manual + - when: never artifacts: paths: - app/build/outputs/apk/fdroid/release diff --git a/README.md b/README.md index 1cfe161b969b4c4c1e5c750014c308a715d56e2f..808b89be943c15b7d90a303e66061aec7b9cfeb3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ +# foundation.e.ntfy + +Fork of [ntfy-android](https://github.com/binwiederhier/ntfy-android) app. It is used in /e/OS as the default UnifiedPush distributor client, and to implement the /e/OS Broadcasting notification feature. It allows to /e/OS user to receive news and alerts from /e/OS team. + +## /e/OS Development + +Add quality hooks, to avoid quality job failure when creating MR. Create the .git/hooks/pre-push +file make it executable, and add the gradle tasks set in quality job in .gitlab.ci file: + +̀```shell +#!/bin/sh + +./gradlew spotlessCheck && ./gradlew lintFdroidRelease && ./gradlew detekt && ./gradlew :notificationsreceiver:testDebugUnitTest :notificationsreceiver-domain:test + +̀``` +## Software architecture and good practices for /e/OS related upgrades + +Feature addition are located in dedicated modules and follow a clean architecture to guide code split and simplify Unit tests writing and maintenance. + +### notificationsreceiver-domain +The domain module is a pure kotlin module, to simplify unit tests. UseCase can use Entities and Procedures to implement the fonctionnalities to the user. Procedures may implement pure technical routines to. + +The goal is to maximise the features related or delicate code in the domain module, and unit test it. Then other module host mostly interfaces adapters with boiler plate code, where unit test make less sense. + +### notificationsreceiver-bridges + +This module hold interfaces adapter between Android specific features and the domain module. + +### notificationsreceiver-UI + +Ui use compose + +### foundation.e.notificationsreceiver in app module + +Hold glue code, which has to belong in main app module. + + # ntfy Android App This is the Android app for [ntfy](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)). You can find the app in [F-Droid](https://f-droid.org/packages/io.heckel.ntfy/) or the [Play Store](https://play.google.com/store/apps/details?id=io.heckel.ntfy), or as .apk files on the [releases page](https://github.com/binwiederhier/ntfy-android/releases). diff --git a/app/build.gradle b/app/build.gradle index 275149cc718e0c96b9579dd9a4e462c8e3a13c87..7312a550aa083c48c58241e44e88308437c3b156 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,7 @@ plugins { id 'com.google.devtools.ksp' + alias(libs.plugins.hilt.android) + alias(libs.plugins.detekt) } repositories { @@ -20,7 +22,7 @@ android { defaultConfig { applicationId "foundation.e.ntfy" - minSdkVersion 21 + minSdkVersion 31 targetSdkVersion 35 versionCode versionMajor * 100000000 + versionMinor * 100000 + versionPatch * 100 + eOSPatch @@ -36,6 +38,22 @@ android { buildFeatures { buildConfig true } + + def sentryDSN = System.getenv("SENTRY_DSN") ?: "placeholdertoto" + buildConfigField("String", "SENTRY_DSN", "\"$sentryDSN\"") + + lint { + baseline = file("lint-baseline.xml") + } + } + + signingConfigs { + create("platform-test") { + storeFile = file("$rootDir/ci/platform.jks") + storePassword = "platform" + keyAlias = "platform" + keyPassword = "platform" + } } buildTypes { @@ -43,12 +61,14 @@ android { minifyEnabled false shrinkResources false debuggable false + signingConfig = null } debug { minifyEnabled false shrinkResources false debuggable true - applicationIdSuffix ".debug" + signingConfig = signingConfigs.getByName("platform-test") + versionNameSuffix "-debug" } } @@ -104,6 +124,13 @@ android.applicationVariants.all { variant -> } dependencies { + implementation project(":notificationsreceiver-domain") + implementation project(':notificationsreceiver') + + implementation(libs.dagger.hilt.android) + ksp(libs.dagger.hilt.compiler) + implementation(libs.eos.telemetry) + // AndroidX, The Basics implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.core:core-ktx:1.10.1" @@ -159,3 +186,21 @@ dependencies { implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.29" implementation "com.caverock:androidsvg:1.4" } + + +detekt { + toolVersion = libs.versions.detekt.get() + config.setFrom(file("../detekt.yml")) + buildUponDefaultConfig = true + autoCorrect = true + baseline = file("detekt-baseline.xml") +} + +tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach { + jvmTarget = "17" + // exclude("**/io/heckel/ntfy/**") +} +tasks.withType(io.gitlab.arturbosch.detekt.DetektCreateBaselineTask).configureEach { + jvmTarget = "17" + //exclude("**/io/heckel/ntfy/**") +} diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml new file mode 100644 index 0000000000000000000000000000000000000000..613b7f1c6ea767f0856e8aeec5c2eb953d44a851 --- /dev/null +++ b/app/detekt-baseline.xml @@ -0,0 +1,461 @@ + + + + + ComplexCondition:JsonConnection.kt$JsonConnection$!failed.get() && !call.isCanceled() && isActive && serviceActive() + ComplexCondition:ShareActivity.kt$ShareActivity$!this::sendItem.isInitialized || !this::useAnotherServerCheckbox.isInitialized || !this::contentText.isInitialized || !this::topicText.isInitialized + CyclomaticComplexMethod:AddFragment.kt$AddFragment$private fun validateInputSubscribeView() + CyclomaticComplexMethod:DetailAdapter.kt$DetailAdapter.DetailViewHolder$private fun formatAttachmentDetails(context: Context, attachment: Attachment, attachmentFileStat: FileInfo?): String + CyclomaticComplexMethod:DetailAdapter.kt$DetailAdapter.DetailViewHolder$private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, attachmentFileStat: FileInfo?): PopupMenu? + CyclomaticComplexMethod:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$private fun downloadAttachment(userAction: Boolean) + CyclomaticComplexMethod:JsonConnection.kt$JsonConnection$override fun start() + CyclomaticComplexMethod:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$fun bind(subscription: Subscription) + CyclomaticComplexMethod:SettingsActivity.kt$SettingsActivity.SettingsFragment$override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) + CyclomaticComplexMethod:ShareActivity.kt$ShareActivity$override fun onCreate(savedInstanceState: Bundle?) + CyclomaticComplexMethod:SubscriberService.kt$SubscriberService$private suspend fun reallyRefreshConnections(scope: CoroutineScope) + ExplicitItLambdaParameter:Util.kt${ str, it -> str + "%02x".format(it) } + ForEachOnRange:Util.kt$1..1000 + InstanceOfCheckForException:DetailActivity.kt$DetailActivity$e is ApiService.UnauthorizedException + InstanceOfCheckForException:DetailAdapter.kt$DetailAdapter.DetailViewHolder$e is ActivityNotFoundException + InstanceOfCheckForException:NotificationService.kt$NotificationService.ViewActionWithClearActivity$e is ActivityNotFoundException + InstanceOfCheckForException:ShareActivity.kt$ShareActivity$e is ApiService.EntityTooLargeException + InstanceOfCheckForException:ShareActivity.kt$ShareActivity$e is ApiService.UnauthorizedException + LongMethod:AddFragment.kt$AddFragment$override fun onCreateDialog(savedInstanceState: Bundle?): Dialog + LongMethod:Backuper.kt$Backuper$private suspend fun applyNotifications(notifications: List<Notification>?) + LongMethod:Backuper.kt$Backuper$private suspend fun createNotificationList(): List<Notification> + LongMethod:BroadcastReceiver.kt$BroadcastReceiver$private fun register(context: Context, intent: Intent) + LongMethod:DetailActivity.kt$DetailActivity$private fun maybeSubscribeAndLoadView(url: Uri) + LongMethod:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$private fun downloadAttachment(userAction: Boolean) + LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) + LongMethod:SettingsActivity.kt$SettingsActivity.SettingsFragment$override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) + LongMethod:ShareActivity.kt$ShareActivity$override fun onCreate(savedInstanceState: Bundle?) + LongMethod:SubscriberService.kt$SubscriberService$private suspend fun reallyRefreshConnections(scope: CoroutineScope) + LongMethod:UserFragment.kt$UserFragment$override fun onCreateDialog(savedInstanceState: Bundle?): Dialog + LongParameterList:ApiService.kt$ApiService$( baseUrl: String, topic: String, user: User? = null, message: String, title: String = "", priority: Int = PRIORITY_DEFAULT, tags: List<String> = emptyList(), delay: String = "", body: RequestBody? = null, filename: String = "" ) + LongParameterList:ApiService.kt$ApiService$( baseUrl: String, topics: String, since: String?, user: User?, notify: (topic: String, Notification) -> Unit, fail: (Exception) -> Unit ) + LongParameterList:DetailAdapter.kt$DetailAdapter.DetailViewHolder$( private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val markwon: Markwon, itemView: View, private val selected: Set<String>, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit ) + LongParameterList:JsonConnection.kt$JsonConnection$( private val connectionId: ConnectionId, private val scope: CoroutineScope, private val repository: Repository, private val api: ApiService, private val user: User?, private val sinceId: String?, private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit, private val notificationListener: (Subscription, Notification) -> Unit, private val serviceActive: () -> Boolean ) + LongParameterList:WsConnection.kt$WsConnection$( private val connectionId: ConnectionId, private val repository: Repository, private val user: User?, private val sinceId: String?, private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit, private val notificationListener: (Subscription, Notification) -> Unit, private val alarmManager: AlarmManager ) + MagicNumber:ApiService.kt$ApiService$15 + MagicNumber:ApiService.kt$ApiService$401 + MagicNumber:ApiService.kt$ApiService$403 + MagicNumber:ApiService.kt$ApiService$404 + MagicNumber:ApiService.kt$ApiService$413 + MagicNumber:ApiService.kt$ApiService$5 + MagicNumber:ApiService.kt$ApiService$77 + MagicNumber:BroadcastService.kt$BroadcastService.BroadcastReceiver$3 + MagicNumber:BroadcastService.kt$BroadcastService.BroadcastReceiver$4 + MagicNumber:BroadcastService.kt$BroadcastService.BroadcastReceiver$5 + MagicNumber:Database.kt$Database.Companion.<no name provided>$10 + MagicNumber:Database.kt$Database.Companion.<no name provided>$11 + MagicNumber:Database.kt$Database.Companion.<no name provided>$12 + MagicNumber:Database.kt$Database.Companion.<no name provided>$13 + MagicNumber:Database.kt$Database.Companion.<no name provided>$14 + MagicNumber:Database.kt$Database.Companion.<no name provided>$3 + MagicNumber:Database.kt$Database.Companion.<no name provided>$4 + MagicNumber:Database.kt$Database.Companion.<no name provided>$5 + MagicNumber:Database.kt$Database.Companion.<no name provided>$6 + MagicNumber:Database.kt$Database.Companion.<no name provided>$7 + MagicNumber:Database.kt$Database.Companion.<no name provided>$8 + MagicNumber:Database.kt$Database.Companion.<no name provided>$9 + MagicNumber:DeleteWorker.kt$DeleteWorker$1000 + MagicNumber:DetailActivity.kt$DetailActivity$1000 + MagicNumber:DetailActivity.kt$DetailActivity$4 + MagicNumber:DetailActivity.kt$DetailActivity$6 + MagicNumber:DetailActivity.kt$DetailActivity$60_000 + MagicNumber:DetailAdapter.kt$DetailAdapter.DetailViewHolder$1000 + MagicNumber:DetailAdapter.kt$DetailAdapter.DetailViewHolder$3 + MagicNumber:DetailAdapter.kt$DetailAdapter.DetailViewHolder$99 + MagicNumber:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$1024 + MagicNumber:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment.<no name provided>$1000 + MagicNumber:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment.<no name provided>$30 + MagicNumber:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment.<no name provided>$60 + MagicNumber:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment.<no name provided>$8 + MagicNumber:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$100 + MagicNumber:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$15 + MagicNumber:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$200 + MagicNumber:DownloadIconWorker.kt$DownloadIconWorker$15 + MagicNumber:JsonConnection.kt$JsonConnection$1000 + MagicNumber:MainActivity.kt$MainActivity$1000 + MagicNumber:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$1000 + MagicNumber:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$24 + MagicNumber:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$60 + MagicNumber:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$99 + MagicNumber:MarkwonFactory.kt$MarkwonFactory$.67f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$.7f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$.83f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$.8f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$0.5f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$1.17f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$1.2f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$1.5f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$1.7f + MagicNumber:MarkwonFactory.kt$MarkwonFactory$8 + MagicNumber:NotificationDispatcher.kt$NotificationDispatcher$1000 + MagicNumber:NotificationDispatcher.kt$NotificationDispatcher$3 + MagicNumber:NotificationFragment.kt$NotificationFragment$1000 + MagicNumber:NotificationFragment.kt$NotificationFragment$150 + MagicNumber:NotificationFragment.kt$NotificationFragment$30 + MagicNumber:NotificationFragment.kt$NotificationFragment$60 + MagicNumber:NotificationFragment.kt$NotificationFragment$8 + MagicNumber:NotificationService.kt$NotificationService$100 + MagicNumber:NotificationService.kt$NotificationService$2000 + MagicNumber:NotificationService.kt$NotificationService$300L + MagicNumber:NotificationService.kt$NotificationService$99 + MagicNumber:Repository.kt$Repository$1000 + MagicNumber:SettingsActivity.kt$SettingsActivity.SettingsFragment$15 + MagicNumber:SettingsActivity.kt$SettingsActivity.SettingsFragment.<no name provided>$1000 + MagicNumber:SettingsActivity.kt$SettingsActivity.SettingsFragment.<no name provided>$30 + MagicNumber:SettingsActivity.kt$SettingsActivity.SettingsFragment.<no name provided>$60 + MagicNumber:SettingsActivity.kt$SettingsActivity.SettingsFragment.<no name provided>$8 + MagicNumber:ShareActivity.kt$ShareActivity$130 + MagicNumber:ShareActivity.kt$ShareActivity$255 + MagicNumber:SubscriberService.kt$SubscriberService$1000 + MagicNumber:SubscriberService.kt$SubscriberService$3 + MagicNumber:SubscriberService.kt$SubscriberService$4 + MagicNumber:SubscriberService.kt$SubscriberService$5 + MagicNumber:SubscriberService.kt$SubscriberService$6 + MagicNumber:UserActionWorker.kt$UserActionWorker$15 + MagicNumber:UserActionWorker.kt$UserActionWorker$60 + MagicNumber:Util.kt$0xfffccccccccccccL + MagicNumber:Util.kt$10 + MagicNumber:Util.kt$100 + MagicNumber:Util.kt$1000 + MagicNumber:Util.kt$100_000_000 + MagicNumber:Util.kt$1024 + MagicNumber:Util.kt$1024.0 + MagicNumber:Util.kt$200 + MagicNumber:Util.kt$40 + MagicNumber:WsConnection.kt$WsConnection$10 + MaxLineLength:AddFragment.kt$AddFragment$Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}, adding to database") + MaxLineLength:AddFragment.kt$AddFragment$if + MaxLineLength:AddFragment.kt$AddFragment$showErrorAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized, user.username)) + MaxLineLength:AddFragment.kt$AddFragment$showErrorAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized, user.username)) + MaxLineLength:AddFragment.kt$AddFragment$subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl)) + MaxLineLength:AddFragment.kt$AddFragment$subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE + MaxLineLength:AddFragment.kt$AddFragment$subscribeUseAnotherServerDescription = view.findViewById(R.id.add_dialog_subscribe_use_another_server_description) + MaxLineLength:AddFragment.kt$AddFragment$val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked + MaxLineLength:ApiService.kt$ApiService$fun + MaxLineLength:ApiService.kt$ApiService.<no name provided>$val line = source.readUtf8Line() ?: throw Exception("Unexpected response for $url: line is null") + MaxLineLength:ApiService.kt$ApiService.<no name provided>$val notification = parser.parseWithTopic(line, notificationId = Random.nextInt(), subscriptionId = 0) // subscriptionId to be set downstream + MaxLineLength:ApiService.kt$ApiService.<no name provided>$val source = response.body?.source() ?: throw Exception("Unexpected response for $url: body is empty") + MaxLineLength:ApiService.kt$ApiService.Companion$val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" + MaxLineLength:Backuper.kt$Backuper$Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e) + MaxLineLength:Backuper.kt$Backuper$private suspend + MaxLineLength:Backuper.kt$Backuper$suspend + MaxLineLength:BroadcastReceiver.kt$BroadcastReceiver$Log.d(TAG, "Subscription with connectorToken $connectorToken exists for a different app. Refusing registration.") + MaxLineLength:BroadcastReceiver.kt$BroadcastReceiver$Log.d(TAG, "Subscription with connectorToken $connectorToken exists. Sending endpoint $endpoint.") + MaxLineLength:Database.kt$Action$@ColumnInfo(name = "id") val id: String + MaxLineLength:Database.kt$Database$@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 14) + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Notification ADD COLUMN attachment_name TEXT") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Notification ADD COLUMN attachment_progress INT") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Notification ADD COLUMN attachment_url TEXT") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Notification ADD COLUMN icon_url TEXT") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Subscription ADD COLUMN autoDelete INT NOT NULL DEFAULT (-1)") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Subscription ADD COLUMN insistent INTEGER NOT NULL DEFAULT (-1)") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("ALTER TABLE Subscription ADD COLUMN minPriority INT NOT NULL DEFAULT (0)") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("CREATE TABLE Log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, timestamp INT NOT NULL, tag TEXT NOT NULL, level INT NOT NULL, message TEXT NOT NULL, exception TEXT)") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("CREATE TABLE Notification_New (id TEXT NOT NULL, subscriptionId INTEGER NOT NULL, timestamp INTEGER NOT NULL, title TEXT NOT NULL, message TEXT NOT NULL, notificationId INTEGER NOT NULL, priority INTEGER NOT NULL DEFAULT(3), tags TEXT NOT NULL, deleted INTEGER NOT NULL, PRIMARY KEY(id, subscriptionId))") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("CREATE TABLE Subscription_New (id INTEGER NOT NULL, baseUrl TEXT NOT NULL, topic TEXT NOT NULL, instant INTEGER NOT NULL DEFAULT('0'), PRIMARY KEY(id))") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("CREATE TABLE User (baseUrl TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, PRIMARY KEY(baseUrl))") + MaxLineLength:Database.kt$Database.Companion.<no name provided>$db.execSQL("INSERT INTO Notification_New SELECT id, subscriptionId, timestamp, '', message, notificationId, 3, '', deleted FROM Notification") + MaxLineLength:Database.kt$NotificationDao$@Query("UPDATE notification SET deleted = 1 WHERE subscriptionId = :subscriptionId AND timestamp < :olderThanTimestamp") + MaxLineLength:Database.kt$Subscription$@ColumnInfo(name = "insistent") val insistent: Int + MaxLineLength:Database.kt$Subscription$@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true), Index(value = ["upConnectorToken"], unique = true)]) + MaxLineLength:DeleteWorker.kt$DeleteWorker$Log.d(TAG, "Deleting attachment for notification ${notification.id}: ${attachment.contentUri} (${attachment.name})") + MaxLineLength:DetailActivity.kt$DetailActivity$"warning" + MaxLineLength:DetailActivity.kt$DetailActivity$. + MaxLineLength:DetailActivity.kt$DetailActivity$// Do NOT remove the notificationId here, we need that for the UI indicators; we'll remove it in onPause() + MaxLineLength:DetailActivity.kt$DetailActivity$0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show() + MaxLineLength:DetailActivity.kt$DetailActivity$1L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show() + MaxLineLength:DetailActivity.kt$DetailActivity$Log.d(TAG, "Showing notification settings dialog for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}") + MaxLineLength:DetailActivity.kt$DetailActivity$Log.e(TAG, "Error fetching notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}: ${e.stackTrace}", e) + MaxLineLength:DetailActivity.kt$DetailActivity$Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_disabled), Toast.LENGTH_SHORT) + MaxLineLength:DetailActivity.kt$DetailActivity$Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_enabled), Toast.LENGTH_SHORT) + MaxLineLength:DetailActivity.kt$DetailActivity$Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show() + MaxLineLength:DetailActivity.kt$DetailActivity$notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate) + MaxLineLength:DetailActivity.kt$DetailActivity$val + MaxLineLength:DetailActivity.kt$DetailActivity$val message = getString(R.string.detail_deep_link_subscribed_toast_message, topicShortUrl(baseUrl, topic)) + MaxLineLength:DetailActivity.kt$DetailActivity$val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil + MaxLineLength:DetailActivity.kt$DetailActivity$val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId) + MaxLineLength:DetailActivity.kt$DetailActivity.<no name provided>$override + MaxLineLength:DetailAdapter.kt$DetailAdapter$class + MaxLineLength:DetailAdapter.kt$DetailAdapter.Companion$const val IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024 // Too large images crash the app with "Canvas: trying to draw too large(233280000bytes) bitmap." + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$. + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$infos.add(context.getString(R.string.detail_item_download_info_deleted_expires_x, formatDateShort(attachment.expires!!))) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$infos.add(context.getString(R.string.detail_item_download_info_download_failed_expires_x, formatDateShort(attachment.expires!!))) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$infos.add(context.getString(R.string.detail_item_download_info_downloading_x_percent, attachment.progress)) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$infos.add(context.getString(R.string.detail_item_download_info_not_downloaded_expires_x, formatDateShort(attachment.expires!!))) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$intent.setDataAndType(contentUri, attachment.type ?: "application/octet-stream") + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp)) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp)) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp)) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp)) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$private + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val actionsCount = Math.min(notification.actions.size, 3) // per documentation, only 3 actions are available + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, attachmentFileStat) // Heavy lifting not during on-click + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val file = ensureSafeNewFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), attachment.name) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val image = attachment.contentUri != null && supportedImage(attachment.type) && previewableImage(attachmentFileStat) + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentFileStat) // Heavy lifting not during on-click + MaxLineLength:DetailAdapter.kt$DetailAdapter.DetailViewHolder$val requiresPermission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$. + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_DAY_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_day) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_MONTH_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_month) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_WEEK_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_week) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Repository.AUTO_DELETE_THREE_DAYS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_days) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Repository.AUTO_DELETE_THREE_MONTHS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_months) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$Toast.makeText(context, getString(R.string.detail_settings_appearance_icon_error_saving, e.message), Toast.LENGTH_LONG).show() + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$else -> getString(R.string.settings_notifications_auto_delete_summary_one_month) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$getString(R.string.settings_notifications_min_priority_summary_x_or_higher, value, minPriorityString) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$if + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$openChannelsPref.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$pref?.dialogMessage = getString(R.string.detail_settings_appearance_display_name_message, topicShortUrl(subscription.baseUrl, subscription.topic)) + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$throw IOException("image exceeds max dimensions of ${SUBSCRIPTION_ICON_MAX_WIDTH}x${SUBSCRIPTION_ICON_MAX_HEIGHT}") + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$throw IOException("image too large, max supported is ${SUBSCRIPTION_ICON_MAX_SIZE_BYTES/1024/1024}MB") + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$val enabled = if (global) repository.getInsistentMaxPriorityEnabled() else value == Repository.INSISTENT_MAX_PRIORITY_ENABLED + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading") + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$val notificationsHeaderId = context?.getString(R.string.detail_settings_notifications_header_key) ?: return + MaxLineLength:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing") + MaxLineLength:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$. + MaxLineLength:DownloadIconWorker.kt$DownloadIconWorker$return + MaxLineLength:DownloadIconWorker.kt$DownloadIconWorker$val size = response.headers["Content-Length"]?.toLongOrNull() ?: return false // Don't abort here if size unknown + MaxLineLength:DownloadManager.kt$DownloadManager$Log.d(TAG,"Enqueuing work to download both attachment and icon for notification $notificationId, work: $workName") + MaxLineLength:JsonConnection.kt$JsonConnection$Log.d(TAG,"[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=${serviceActive()}") + MaxLineLength:Log.kt$Log$val maybeScrubLine = if (scrubLine) "Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.\n" else "" + MaxLineLength:MainActivity.kt$MainActivity$0L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show() + MaxLineLength:MainActivity.kt$MainActivity$1L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show() + MaxLineLength:MainActivity.kt$MainActivity$DownloadManager.enqueue(this@MainActivity, notification.id, userAction = false, DownloadType.ICON) + MaxLineLength:MainActivity.kt$MainActivity$Log.d(TAG, "Battery: ignoring optimizations = $ignoringOptimizations (we want this to be true); instant subscriptions = $hasInstantSubscriptions; remind time reached = $batteryRemindTimeReached; banner = $showBanner") + MaxLineLength:MainActivity.kt$MainActivity$Log.d(TAG, "hasSelfHostedSubscriptions: ${hasSelfHostedSubscriptions}, wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}") + MaxLineLength:MainActivity.kt$MainActivity$Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show() + MaxLineLength:MainActivity.kt$MainActivity$class + MaxLineLength:MainActivity.kt$MainActivity$if + MaxLineLength:MainActivity.kt$MainActivity$notificationsDisabledUntilItem?.title = getString(R.string.main_menu_notifications_disabled_until, formattedDate) + MaxLineLength:MainActivity.kt$MainActivity$startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))) + MaxLineLength:MainActivity.kt$MainActivity$val + MaxLineLength:MainActivity.kt$MainActivity$val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil + MaxLineLength:MainActivity.kt$MainActivity$val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId) + MaxLineLength:MainActivity.kt$MainActivity$val showBanner = hasSelfHostedSubscriptions && wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms + MaxLineLength:MainActivity.kt$MainActivity$workManager?.enqueueUniquePeriodicWork(SubscriberService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work) + MaxLineLength:MainAdapter.kt$MainAdapter$class + MaxLineLength:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$class + MaxLineLength:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$instantImageView.visibility = if (subscription.instant && BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE + MaxLineLength:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$private val notificationDisabledForeverImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_forever_image) + MaxLineLength:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$private val notificationDisabledUntilImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_until_image) + MaxLineLength:MainAdapter.kt$MainAdapter.SubscriptionViewHolder$val showMutedUntilIcon = !showMutedForeverIcon && (subscription.mutedUntil > 1L || globalMutedUntil > 1L) && !isUnifiedPush + MaxLineLength:NotificationDispatcher.kt$NotificationDispatcher$return subscription.mutedUntil == 1L || (subscription.mutedUntil > 1L && subscription.mutedUntil > System.currentTimeMillis()/1000) + MaxLineLength:NotificationService.kt$NotificationService$(repository.getInsistentMaxPriorityEnabled() || subscription.insistent == Repository.INSISTENT_MAX_PRIORITY_ENABLED) + MaxLineLength:NotificationService.kt$NotificationService$. + MaxLineLength:NotificationService.kt$NotificationService$// Hack: Action "view" with "clear=true" is a special case, because it's apparently impossible to start a + MaxLineLength:NotificationService.kt$NotificationService$PRIORITY_LOW -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW) + MaxLineLength:NotificationService.kt$NotificationService$PRIORITY_MIN -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN) + MaxLineLength:NotificationService.kt$NotificationService$addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + MaxLineLength:NotificationService.kt$NotificationService$builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build()) + MaxLineLength:NotificationService.kt$NotificationService$builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build()) + MaxLineLength:NotificationService.kt$NotificationService$builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build()) + MaxLineLength:NotificationService.kt$NotificationService$builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build()) + MaxLineLength:NotificationService.kt$NotificationService$else -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_default_name), NotificationManager.IMPORTANCE_DEFAULT) + MaxLineLength:NotificationService.kt$NotificationService$getPendingIntent(Random().nextInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:NotificationService.kt$NotificationService$if + MaxLineLength:NotificationService.kt$NotificationService$maybeCreateNotificationGroup(DEFAULT_GROUP, context.getString(R.string.channel_notifications_group_default_name)) + MaxLineLength:NotificationService.kt$NotificationService$private + MaxLineLength:NotificationService.kt$NotificationService$return context.getString(R.string.notification_popup_file_downloading, attachmentInfos, attachment.progress, message) + MaxLineLength:NotificationService.kt$NotificationService$setDataAndType(contentUri, notification.attachment.type ?: "application/octet-stream") + MaxLineLength:NotificationService.kt$NotificationService$val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH) + MaxLineLength:NotificationService.kt$NotificationService$val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_HIGH) // IMPORTANCE_MAX does not exist + MaxLineLength:NotificationService.kt$NotificationService$val notificationIcon = if (notification.icon != null) notification.icon.contentUri?.readBitmapFromUriOrNull(context) else null + MaxLineLength:NotificationService.kt$NotificationService$val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:NotificationService.kt$NotificationService$val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:NotificationService.kt$NotificationService$val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:NotificationService.kt$NotificationService$val subscriptionIcon = if (subscription.icon != null) subscription.icon.readBitmapFromUriOrNull(context) else null + MaxLineLength:NotificationService.kt$NotificationService$val viewIntent = PendingIntent.getActivity(context, Random().nextInt(), Intent(Intent.ACTION_VIEW, uri), PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:NotificationService.kt$NotificationService.UserActionBroadcastReceiver$BROADCAST_TYPE_DOWNLOAD_START -> DownloadManager.enqueue(context, notificationId, userAction = true, DownloadType.ATTACHMENT) + MaxLineLength:PollWorker.kt$PollWorker.Companion$const val WORK_NAME_ONCE_SINGE_PREFIX = "NtfyPollWorkerSingle" // e.g. NtfyPollWorkerSingle_https://ntfy.sh_mytopic + MaxLineLength:Repository.kt$Repository$return sharedPrefs.getLong(SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME, BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS) + MaxLineLength:Repository.kt$Repository$sharedPrefs.getString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, null) + MaxLineLength:Repository.kt$Repository.Companion$const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL + MaxLineLength:Repository.kt$Repository.Companion$const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner) + MaxLineLength:SettingsActivity.kt$SettingsActivity.Companion$private const val EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD = 100 * 1024 // Show "Uploading ..." if log larger than X + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$. + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_DAY_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_day) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_MONTH_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_month) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DELETE_ONE_WEEK_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_week) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DELETE_THREE_DAYS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_days) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DELETE_THREE_MONTHS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_months) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DOWNLOAD_ALWAYS -> getString(R.string.settings_notifications_auto_download_summary_always) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.AUTO_DOWNLOAD_NEVER -> getString(R.string.settings_notifications_auto_download_summary_never) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Repository.CONNECTION_PROTOCOL_WS -> getString(R.string.settings_advanced_connection_protocol_summary_ws) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Toast.makeText(context, getString(R.string.settings_backup_restore_backup_failed, e.message), Toast.LENGTH_LONG).show() + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Toast.makeText(context, getString(R.string.settings_backup_restore_backup_successful), Toast.LENGTH_LONG).show() + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Toast.makeText(context, getString(R.string.settings_backup_restore_restore_failed, e.message), Toast.LENGTH_LONG).show() + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$Toast.makeText(context, getString(R.string.settings_backup_restore_restore_successful), Toast.LENGTH_LONG).show() + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$autoDownloadSelection = repository.getAutoDownloadMaxSize() + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$backup?.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$channelPrefs?.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$clearLogs?.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$else -> getString(R.string.settings_notifications_auto_delete_summary_one_month) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$else -> getString(R.string.settings_notifications_auto_download_summary_smaller_than_x, formatBytes(maxSize, decimals = 0)) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$exactAlarmsPref?.summary = if (canScheduleExactAlarms) getString(R.string.settings_advanced_exact_alarms_true) else getString(R.string.settings_advanced_exact_alarms_false) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$exportLogs?.preferenceDataStore = object : PreferenceDataStore() { } + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString) + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$if + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return + MaxLineLength:SettingsActivity.kt$SettingsActivity.SettingsFragment$val version = getString(R.string.settings_about_version_format, BuildConfig.VERSION_NAME, BuildConfig.FLAVOR) + MaxLineLength:SettingsActivity.kt$SettingsActivity.UserSettingsFragment$getString(R.string.settings_general_users_prefs_user_used_by_many, user.topics.joinToString(", ")) + MaxLineLength:ShareActivity.kt$ShareActivity$contentText.text.isNotEmpty() && validTopic(topicText.text.toString()) && validUrl(baseUrlText.text.toString()) + MaxLineLength:ShareActivity.kt$ShareActivity$if + MaxLineLength:ShareActivity.kt$ShareActivity.TopicAdapter$class + MaxLineLength:SubscriberService.kt$SubscriberService$. + MaxLineLength:SubscriberService.kt$SubscriberService$JsonConnection(connectionId, scope, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, serviceActive) + MaxLineLength:SubscriberService.kt$SubscriberService$WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager) + MaxLineLength:SubscriberService.kt$SubscriberService$alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent) + MaxLineLength:SubscriberService.kt$SubscriberService$else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size) + MaxLineLength:SubscriberService.kt$SubscriberService$else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size) + MaxLineLength:SubscriberService.kt$SubscriberService$val + MaxLineLength:SubscriberService.kt$SubscriberService$val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE) + MaxLineLength:SubscriberServiceManager.kt$SubscriberServiceManager$workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) + MaxLineLength:SubscriberServiceManager.kt$SubscriberServiceManager.ServiceStartWorker$class + MaxLineLength:SubscriberServiceManager.kt$SubscriberServiceManager.ServiceStartWorker$val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP + MaxLineLength:SubscriberServiceManager.kt$SubscriberServiceManager.ServiceStartWorker$val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size + MaxLineLength:UserActionManager.kt$UserActionManager$Log.d(TAG,"Enqueuing work to execute user action for notification $notificationId, action $actionId, work: $workName") + MaxLineLength:UserFragment.kt$UserFragment$val positiveButtonTextResId = if (user == null) R.string.user_dialog_button_add else R.string.user_dialog_button_save + MaxLineLength:Util.kt$// Content provider URIs (e.g. content://io.heckel.ntfy.provider/cache_files/DQ4o7DitZAmw) return an entry, even + MaxLineLength:Util.kt$// when they do not exist, but with an empty size. This is a practical/fast way to weed out non-existing files. + MaxLineLength:Util.kt$fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) + MaxLineLength:WsConnection.kt$WsConnection$Log.d(TAG,"$shortUrl (gid=$globalId): Not (re-)starting, because connection is marked closed/connecting/connected") + MaxLineLength:WsConnection.kt$WsConnection$Log.d(TAG,"$shortUrl (gid=$globalId): Not closing existing connection, because there is no active web socket") + MaxLineLength:WsConnection.kt$WsConnection$Log.d(TAG,"$shortUrl (gid=$globalId): Not rescheduling connection, because connection is marked closed/connecting/connected") + MaxLineLength:WsConnection.kt$WsConnection.Listener$Log.e(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection failed (response code ${response.code}, message: ${response.message}): ${t.message}", t) + MaxLineLength:WsConnection.kt$WsConnection.Listener$Log.e(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection failed (response is null): ${t.message}", t) + MaxLineLength:WsConnection.kt$WsConnection.Listener$Log.w(TAG, "$shortUrl (gid=$globalId, lid=$id): Skipping synchronized block '$tag', because listener ID does not match ${listenerId.get()}") + MaxLineLength:WsConnection.kt$WsConnection.Listener$val notificationWithTopic = parser.parseWithTopic(text, subscriptionId = 0, notificationId = Random.nextInt()) + MemberNameEqualsClassName:Log.kt$Log$private fun log(level: Int, tag: String, message: String, exception: Throwable?) + NestedBlockDepth:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$private fun downloadAttachment(userAction: Boolean) + NestedBlockDepth:DownloadIconWorker.kt$DownloadIconWorker$private fun downloadIcon(iconFile: File) + ReturnCount:ApiService.kt$ApiService$fun checkAuth(baseUrl: String, topic: String, user: User?): Boolean + ReturnCount:BroadcastService.kt$BroadcastService.BroadcastReceiver$private fun getStringExtra(intent: Intent, name: String): String? + ReturnCount:DetailActivity.kt$DetailActivity$private fun loadView() + ReturnCount:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$override fun doWork(): Result + ReturnCount:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$private fun shouldAbortDownload(): Boolean + ReturnCount:DownloadIconWorker.kt$DownloadIconWorker$override fun doWork(): Result + ReturnCount:JsonConnection.kt$JsonConnection$private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long + ReturnCount:NotificationDispatcher.kt$NotificationDispatcher$private fun shouldDownloadAttachment(notification: Notification): Boolean + ReturnCount:NotificationService.kt$NotificationService$private fun formatMessageMaybeWithAttachmentInfos(notification: Notification): CharSequence + ReturnCount:SettingsActivity.kt$SettingsActivity.SettingsFragment$override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) + ReturnCount:ShareActivity.kt$ShareActivity$override fun onCreate(savedInstanceState: Bundle?) + ReturnCount:UserActionWorker.kt$UserActionWorker$override fun doWork(): Result + ReturnCount:Util.kt$fun isDarkThemeOn(context: Context): Boolean + SwallowedException:DetailAdapter.kt$DetailAdapter.DetailViewHolder$e: ActivityNotFoundException + SwallowedException:MainActivity.kt$MainActivity$e: ActivityNotFoundException + SwallowedException:NotificationService.kt$NotificationService$e: Exception + SwallowedException:Util.kt$e: IllegalArgumentException + ThrowsCount:ApiService.kt$ApiService$fun publish( baseUrl: String, topic: String, user: User? = null, message: String, title: String = "", priority: Int = PRIORITY_DEFAULT, tags: List<String> = emptyList(), delay: String = "", body: RequestBody? = null, filename: String = "" ) + ThrowsCount:DetailAdapter.kt$DetailAdapter.DetailViewHolder$private fun saveFile(context: Context, attachment: Attachment): Boolean + ThrowsCount:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$private fun createIconPickLauncher(): ActivityResultLauncher<String> + ThrowsCount:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$private fun downloadAttachment(userAction: Boolean) + ThrowsCount:DownloadIconWorker.kt$DownloadIconWorker$private fun downloadIcon(iconFile: File) + ThrowsCount:Util.kt$fun fileStat(context: Context, contentUri: Uri?): FileInfo + TooGenericExceptionCaught:AddFragment.kt$AddFragment$e: Exception + TooGenericExceptionCaught:ApiService.kt$ApiService.<no name provided>$e: Exception + TooGenericExceptionCaught:Backuper.kt$Backuper$e: Exception + TooGenericExceptionCaught:BroadcastReceiver.kt$BroadcastReceiver$e: Exception + TooGenericExceptionCaught:BroadcastService.kt$BroadcastService.BroadcastReceiver$e: Exception + TooGenericExceptionCaught:DeleteWorker.kt$DeleteWorker$e: Exception + TooGenericExceptionCaught:DetailActivity.kt$DetailActivity$e: Exception + TooGenericExceptionCaught:DetailAdapter.kt$DetailAdapter.DetailViewHolder$e: Exception + TooGenericExceptionCaught:DetailSettingsActivity.kt$DetailSettingsActivity.SettingsFragment$e: Exception + TooGenericExceptionCaught:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$e: Exception + TooGenericExceptionCaught:DownloadIconWorker.kt$DownloadIconWorker$e: Exception + TooGenericExceptionCaught:JsonConnection.kt$JsonConnection$e: Exception + TooGenericExceptionCaught:MainActivity.kt$MainActivity$e: Exception + TooGenericExceptionCaught:NotificationService.kt$NotificationService$e: Exception + TooGenericExceptionCaught:NotificationService.kt$NotificationService.ViewActionWithClearActivity$e: Exception + TooGenericExceptionCaught:PollWorker.kt$PollWorker$e: Exception + TooGenericExceptionCaught:SettingsActivity.kt$SettingsActivity.SettingsFragment$e: Exception + TooGenericExceptionCaught:ShareActivity.kt$ShareActivity$e: Exception + TooGenericExceptionCaught:SubscriberService.kt$SubscriberService$e: Exception + TooGenericExceptionCaught:UserActionWorker.kt$UserActionWorker$e: Exception + TooGenericExceptionThrown:ApiService.kt$ApiService$throw Exception("Unexpected response ${response.code} when polling topic $url") + TooGenericExceptionThrown:ApiService.kt$ApiService$throw Exception("Unexpected response ${response.code} when publishing to $url") + TooGenericExceptionThrown:ApiService.kt$ApiService$throw Exception("Unexpected server response ${response.code}") + TooGenericExceptionThrown:ApiService.kt$ApiService.<no name provided>$throw Exception("Unexpected response ${response.code} when subscribing to topic $url") + TooGenericExceptionThrown:ApiService.kt$ApiService.<no name provided>$throw Exception("Unexpected response for $url: body is empty") + TooGenericExceptionThrown:ApiService.kt$ApiService.<no name provided>$throw Exception("Unexpected response for $url: line is null") + TooGenericExceptionThrown:Backuper.kt$Backuper$throw Exception("Cannot open output stream") + TooGenericExceptionThrown:DetailAdapter.kt$DetailAdapter.DetailViewHolder$throw Exception("Cannot insert content") + TooGenericExceptionThrown:DetailAdapter.kt$DetailAdapter.DetailViewHolder$throw Exception("Cannot open input stream") + TooGenericExceptionThrown:DetailAdapter.kt$DetailAdapter.DetailViewHolder$throw Exception("Cannot open output stream") + TooGenericExceptionThrown:DetailAdapter.kt$DetailAdapter.DetailViewHolder$throw Exception("no rows deleted") + TooGenericExceptionThrown:DetailAdapter.kt$DetailAdapter.DetailViewHolder$throw Exception("uri empty") + TooGenericExceptionThrown:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$throw Exception("Attachment is longer than max download size.") + TooGenericExceptionThrown:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$throw Exception("Cannot create cache directory for attachments: $attachmentDir") + TooGenericExceptionThrown:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$throw Exception("Cannot open output stream") + TooGenericExceptionThrown:DownloadAttachmentWorker.kt$DownloadAttachmentWorker$throw Exception("Unexpected response: ${response.code}") + TooGenericExceptionThrown:DownloadIconWorker.kt$DownloadIconWorker$throw Exception("Cannot create cache directory for icons: $iconDir") + TooGenericExceptionThrown:DownloadIconWorker.kt$DownloadIconWorker$throw Exception("Cannot open output stream") + TooGenericExceptionThrown:DownloadIconWorker.kt$DownloadIconWorker$throw Exception("Icon is longer than max download size.") + TooGenericExceptionThrown:DownloadIconWorker.kt$DownloadIconWorker$throw Exception("Unexpected response: ${response.code}") + TooGenericExceptionThrown:SettingsActivity.kt$SettingsActivity.SettingsFragment$throw Exception("Return body is empty") + TooGenericExceptionThrown:SettingsActivity.kt$SettingsActivity.SettingsFragment$throw Exception("Unexpected response ${response.code}") + TooGenericExceptionThrown:UserActionWorker.kt$UserActionWorker$throw Exception("HTTP ${response.code}") + TooGenericExceptionThrown:Util.kt$throw Exception("Bitmap too large to draw on Canvas (${bitmap.byteCount} bytes)") + TooGenericExceptionThrown:Util.kt$throw Exception("Cannot find safe file") + TooGenericExceptionThrown:Util.kt$throw Exception("Invalid argument $topicUrl") + TooGenericExceptionThrown:Util.kt$throw Exception("Query returned null") + TooManyFunctions:Database.kt$NotificationDao + TooManyFunctions:Log.kt$Log$Companion + TooManyFunctions:Repository.kt$Repository + TooManyFunctions:Util.kt$io.heckel.ntfy.util.Util.kt + UnusedParameter:Util.kt$resources: Resources + UnusedPrivateMember:DetailAdapter.kt$DetailAdapter.DetailViewHolder$private fun maybeMarkdown(message: String, notification: Notification): CharSequence + UseCheckOrError:AddFragment.kt$AddFragment$throw IllegalStateException("Activity cannot be null") + UseCheckOrError:NotificationFragment.kt$NotificationFragment$throw IllegalStateException("Activity cannot be null") + UtilityClassWithPublicConstructor:Colors.kt$Colors + WildcardImport:AddFragment.kt$import android.widget.* + WildcardImport:AddFragment.kt$import io.heckel.ntfy.util.* + WildcardImport:ApiService.kt$import io.heckel.ntfy.util.* + WildcardImport:ApiService.kt$import okhttp3.* + WildcardImport:BroadcastReceiver.kt$import io.heckel.ntfy.util.* + WildcardImport:BroadcastService.kt$import io.heckel.ntfy.util.* + WildcardImport:Database.kt$import androidx.room.* + WildcardImport:DetailActivity.kt$import io.heckel.ntfy.util.* + WildcardImport:DetailActivity.kt$import kotlinx.coroutines.* + WildcardImport:DetailAdapter.kt$import android.content.* + WildcardImport:DetailAdapter.kt$import android.widget.* + WildcardImport:DetailAdapter.kt$import io.heckel.ntfy.db.* + WildcardImport:DetailAdapter.kt$import io.heckel.ntfy.util.* + WildcardImport:DetailSettingsActivity.kt$import androidx.preference.* + WildcardImport:DetailSettingsActivity.kt$import io.heckel.ntfy.util.* + WildcardImport:DetailSettingsActivity.kt$import kotlinx.coroutines.* + WildcardImport:DownloadAttachmentWorker.kt$import io.heckel.ntfy.db.* + WildcardImport:DownloadIconWorker.kt$import io.heckel.ntfy.db.* + WildcardImport:JsonConnection.kt$import io.heckel.ntfy.db.* + WildcardImport:JsonConnection.kt$import kotlinx.coroutines.* + WildcardImport:MainActivity.kt$import androidx.work.* + WildcardImport:MainActivity.kt$import io.heckel.ntfy.util.* + WildcardImport:MainViewModel.kt$import io.heckel.ntfy.db.* + WildcardImport:MarkwonFactory.kt$import android.text.style.* + WildcardImport:MarkwonFactory.kt$import io.noties.markwon.* + WildcardImport:MarkwonFactory.kt$import org.commonmark.node.* + WildcardImport:NotificationService.kt$import android.app.* + WildcardImport:NotificationService.kt$import io.heckel.ntfy.db.* + WildcardImport:NotificationService.kt$import io.heckel.ntfy.util.* + WildcardImport:Repository.kt$import androidx.lifecycle.* + WildcardImport:SettingsActivity.kt$import androidx.preference.* + WildcardImport:SettingsActivity.kt$import io.heckel.ntfy.util.* + WildcardImport:ShareActivity.kt$import android.view.* + WildcardImport:ShareActivity.kt$import android.widget.* + WildcardImport:ShareActivity.kt$import io.heckel.ntfy.util.* + WildcardImport:SubscriberService.kt$import android.app.* + WildcardImport:SubscriberServiceManager.kt$import androidx.work.* + WildcardImport:UserActionWorker.kt$import io.heckel.ntfy.db.* + WildcardImport:Util.kt$import io.heckel.ntfy.db.* + WildcardImport:WsConnection.kt$import io.heckel.ntfy.db.* + + diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000000000000000000000000000000000000..d58704e2ea2ea10574f42140c450e5269d77baa8 --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,19501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/debug/res/values/values.xml b/app/src/debug/res/values/values.xml index 674f6be304800ff82426fa929543de16f5b539cf..3f4d72bedfc46040f62df29006c578f6fb4eee3b 100644 --- a/app/src/debug/res/values/values.xml +++ b/app/src/debug/res/values/values.xml @@ -1,4 +1,5 @@ ntfy (debug) + https://push.murenatest.com diff --git a/app/src/main/java/foundation/e/notificationsreceiver/utils/ETelemetryLogger.kt b/app/src/main/java/foundation/e/notificationsreceiver/utils/ETelemetryLogger.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5cb67de31628408064ea9e0de6a275a2a8dc98c --- /dev/null +++ b/app/src/main/java/foundation/e/notificationsreceiver/utils/ETelemetryLogger.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.notificationsreceiver.utils + +import android.app.Application +import android.util.Log +import foundation.e.lib.telemetry.Telemetry +import foundation.e.notificationsreceiver.domain.utils.Logger +import foundation.e.notificationsreceiver.domain.utils.setCustomLoggerImplementation +import io.heckel.ntfy.BuildConfig +import io.sentry.SentryLevel + +class ETelemetryLogger : Logger { + + fun initializeAsLoggerImplementation(application: Application) { + Telemetry.init(BuildConfig.SENTRY_DSN, application, true) + + setCustomLoggerImplementation(this) + } + + override fun v(tag: String?, message: String?, throwable: Throwable?) { + Log.v(tag, message, throwable) + } + + override fun d(tag: String?, message: String?, throwable: Throwable?) { + Log.d(tag, message, throwable) + } + + override fun i(tag: String?, message: String?, throwable: Throwable?) { + Log.i(tag, message, throwable) + } + + override fun w(tag: String?, message: String?, throwable: Throwable?) { + message?.let { "$tag - $it" } + var sentryMessage = "$tag - $message" + throwable?.let { sentryMessage += " : ${throwable.message}" } + Telemetry.reportMessage(sentryMessage, SentryLevel.WARNING) + Log.w(tag, message, throwable) + } + + override fun e(tag: String?, message: String?, throwable: Throwable?) { + var sentryMessage = "$tag - $message" + throwable?.let { + sentryMessage += " : ${throwable.message}" + Telemetry.reportException(Exception(throwable)) + } + + Telemetry.reportMessage(sentryMessage, SentryLevel.ERROR) + Log.e(tag, message, throwable) + } + + override fun wtf(tag: String?, message: String?, throwable: Throwable?) { + var sentryMessage = "$tag - $message" + throwable?.let { + sentryMessage += " : ${throwable.message}" + Telemetry.reportException(Exception(throwable)) + } + + Telemetry.reportMessage(sentryMessage, SentryLevel.ERROR) + Log.wtf(tag, message, throwable) + } +} diff --git a/app/src/main/java/io/heckel/ntfy/app/Application.kt b/app/src/main/java/io/heckel/ntfy/app/Application.kt index f6cb30ce4f4a05b7efa48165ff3260b6d74407bd..8f146fa94cf002a925f6a362a0679f92410ab2bc 100644 --- a/app/src/main/java/io/heckel/ntfy/app/Application.kt +++ b/app/src/main/java/io/heckel/ntfy/app/Application.kt @@ -1,9 +1,12 @@ package io.heckel.ntfy.app import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import foundation.e.notificationsreceiver.utils.ETelemetryLogger import io.heckel.ntfy.db.Repository import io.heckel.ntfy.util.Log +@HiltAndroidApp class Application : Application() { val repository by lazy { val repository = Repository.getInstance(applicationContext) @@ -12,4 +15,9 @@ class Application : Application() { } repository } + + override fun onCreate() { + super.onCreate() + ETelemetryLogger().initializeAsLoggerImplementation(this) + } } diff --git a/build.gradle b/build.gradle index 45ae3c18a344e8fa6720c8999428e0c19c2babf2..4a76393971daf734f4f1382fdcf51170a767ba80 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,9 @@ buildscript { plugins { id 'com.google.devtools.ksp' version '2.0.21-1.0.27' + alias(libs.plugins.spotless) + alias(libs.plugins.hilt.android) apply false + alias(libs.plugins.detekt) apply false } allprojects { @@ -23,9 +26,26 @@ allprojects { google() mavenCentral() maven { url "https://jitpack.io" } // For StfalconImageViewer + maven { url = uri("https://gitlab.e.foundation/api/v4/groups/9/-/packages/maven") } } } -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file + +spotless { + kotlin { + ktlint(libs.versions.ktlint.get()).editorConfigOverride([ + "ktlint_standard_function-naming": "disabled", + "ktlint_standard_no-wildcard-imports": "disabled", + "ktlint_standard_comment-wrapping": "disabled", + "ktlint_standard_property-naming": "disabled", + "ktlint_standard_discouraged-comment-location": "disabled", + "ktlint_standard_function-expression-body": "disabled", + ]) + target( + "app/src/main/java/foundation/e/notificationsreceiver/**/*.kt", + "notificationsreceiver-bridges/src/*/java/**/*.kt", + "notificationsreceiver-domain/src/*/java/**/*.kt", + ) + endWithNewline() + } +} diff --git a/ci/platform.jks b/ci/platform.jks new file mode 100644 index 0000000000000000000000000000000000000000..b778840542e79c048bcf570aa960243eeb9b9d53 Binary files /dev/null and b/ci/platform.jks differ diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0a7c1924580b1ff71d5dfb25db45dc84851480d --- /dev/null +++ b/detekt.yml @@ -0,0 +1,24 @@ +# Naming rules +naming: + ConstructorParameterNaming: + active: false + VariableNaming: + active: false + FunctionNaming: + active: true + ignoreAnnotated: [ 'Composable' ] + +# Style rules +style: + ForbiddenComment: + active: false + ReturnCount: + excludeGuardClauses: true + UnusedPrivateMember: + ignoreAnnotated: ['Preview'] + +# Complexity rules +complexity: + TooManyFunctions: + ignorePrivate: true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000000000000000000000000000000000..f0c4975207278dbf6c7942f0fb05e43c7d04a5e5 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,81 @@ +[versions] +app-compileSdk = "35" +app-minSdk = "31" +agp = "8.8.0" +coreKtx = "1.16.0" +coroutines = "1.10.2" +hilt = "2.53.1" +hiltAndroidx = "1.2.0" +kotlin = "2.0.0" +ksp = "2.0.0-1.0.23" + +androidxLifecycle = "2.9.2" +activityCompose = "1.10.1" +composeBom = "2025.06.01" +jetbrainsKotlinJvm = "2.0.0" + +# Tests +mockk = "1.13.12" +junit = "1.2.1" +espressoCore = "3.6.1" +appcompat = "1.7.1" +material = "1.12.0" + +# Build +detekt = "1.23.8" +ktlint = "1.6.0" + +[libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version = "1.1.7" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltAndroidx" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltAndroidx" } +androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltAndroidx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version = "2.9.0" } +androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version = "2.10.2" } +dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android" , version.ref = "hilt"} +dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler" , version.ref = "hilt"} +dagger-hilt-core = { group = "com.google.dagger", name = "hilt-core" , version.ref = "hilt"} +eos-telemetry = {group = "foundation.e.lib", name = "telemetry", version = "1.0.1-release" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name ="kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.8.1" } +markwon = { group = "io.noties.markwon", name = "core", version = "4.6.2" } +markwon-strikethrough = {group = "io.noties.markwon", name = "ext-strikethrough", version = "4.6.2" } +markwon-html = {group = "io.noties.markwon", name = "html", version = "4.6.2" } +markwon-compose = { group = "com.github.jeziellago", name = "compose-markdown", version = "0.5.7" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version = "3.0.0" } +retrofit-json = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "1.0.0" } +timber = { group = "com.jakewharton.timber", name = "timber", version = "5.0.1" } +unifiedpush-connector = { group = "org.unifiedpush.android", name = "connector", version = "3.0.9" } + +# Test libraries +junit = { group = "junit", name = "junit", version = "4.13.2" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name ="kotlinx-coroutines-test", version.ref = "coroutines" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockk-agent = { group = "io.mockk", name = "mockk-agent", version.ref = "mockk" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +spotless = { id = "com.diffplug.spotless", version = "8.0.0" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref ="hilt" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.0" } +ksp = { id = "com.google.devtools.ksp", version.ref ="ksp" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" } +android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/lint.xml b/lint.xml new file mode 100644 index 0000000000000000000000000000000000000000..71167c0be77d13402782d7a5eda20da34c466680 --- /dev/null +++ b/lint.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/notificationsreceiver-domain/.gitignore b/notificationsreceiver-domain/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/notificationsreceiver-domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/notificationsreceiver-domain/build.gradle.kts b/notificationsreceiver-domain/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..e8aca71ffd2311338e494b562027c78c3630619c --- /dev/null +++ b/notificationsreceiver-domain/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("java-library") + id("com.google.devtools.ksp") + id("org.jetbrains.kotlin.jvm") + alias(libs.plugins.detekt) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + implementation(libs.dagger.hilt.core) + ksp(libs.dagger.hilt.compiler) + implementation(libs.kotlinx.coroutines.core) + + testImplementation(project(":notificationsreceiver-domain:entitiesfixtures")) + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) + testImplementation(libs.mockk.agent) +} + +detekt { + toolVersion = libs.versions.detekt.get() + config.setFrom(file("../detekt.yml")) + buildUponDefaultConfig = true + autoCorrect = true +} + +tasks.withType().configureEach { + jvmTarget = "17" +} +tasks.withType().configureEach { + jvmTarget = "17" +} diff --git a/notificationsreceiver-domain/entitiesfixtures/.gitignore b/notificationsreceiver-domain/entitiesfixtures/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/notificationsreceiver-domain/entitiesfixtures/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/notificationsreceiver-domain/entitiesfixtures/build.gradle.kts b/notificationsreceiver-domain/entitiesfixtures/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..9c6f6754e99f551c48db1ad9835cab17b3629238 --- /dev/null +++ b/notificationsreceiver-domain/entitiesfixtures/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") +} +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} +dependencies { + implementation(project(":notificationsreceiver-domain")) +} diff --git a/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/AppBackgroundScope.kt b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/AppBackgroundScope.kt new file mode 100644 index 0000000000000000000000000000000000000000..0caa199242ae0cff88f806d6b70bebb3f79fabd1 --- /dev/null +++ b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/AppBackgroundScope.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.notificationsreceiver.domain.utils + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class AppBackgroundScope diff --git a/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/Logger.kt b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/Logger.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c637a030b558f466889e270e8d8fdaedd9b6e70 --- /dev/null +++ b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/Logger.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.notificationsreceiver.domain.utils + +import kotlin.jvm.java +import kotlin.let + +interface Logger { + fun v(tag: String?, message: String?, throwable: Throwable? = null) + fun d(tag: String?, message: String?, throwable: Throwable? = null) + fun i(tag: String?, message: String?, throwable: Throwable? = null) + fun w(tag: String?, message: String?, throwable: Throwable? = null) + fun e(tag: String?, message: String?, throwable: Throwable? = null) + fun wtf(tag: String?, message: String?, throwable: Throwable? = null) +} + +var loggerImplementation: Logger = LoggerPrintln() + private set + +fun setCustomLoggerImplementation(logger: Logger) { + loggerImplementation = logger +} + +inline fun T.v(message: String, throwable: Throwable? = null) { + return loggerImplementation.v(T::class.java.simpleName, message, throwable) +} + +inline fun T.d(message: String, throwable: Throwable? = null) { + return loggerImplementation.d(T::class.java.simpleName, message, throwable) +} + +inline fun T.i(message: String, throwable: Throwable? = null) { + return loggerImplementation.i(T::class.java.simpleName, message, throwable) +} + +inline fun T.w(message: String, throwable: Throwable? = null) { + return loggerImplementation.w(T::class.java.simpleName, message, throwable) +} + +inline fun T.e(message: String, throwable: Throwable? = null) { + return loggerImplementation.e(T::class.java.simpleName, message, throwable) +} + +inline fun T.wtf(message: String, throwable: Throwable? = null) { + return loggerImplementation.wtf(T::class.java.simpleName, message, throwable) +} + +class StaticLogger(private val tag: String) { + fun v(message: String, throwable: Throwable? = null) = loggerImplementation.v(tag, message, throwable) + fun d(message: String, throwable: Throwable? = null) = loggerImplementation.d(tag, message, throwable) + fun i(message: String, throwable: Throwable? = null) = loggerImplementation.i(tag, message, throwable) + fun w(message: String, throwable: Throwable? = null) = loggerImplementation.w(tag, message, throwable) + fun e(message: String, throwable: Throwable? = null) = loggerImplementation.e(tag, message, throwable) + fun wtf(message: String, throwable: Throwable? = null) = loggerImplementation.wtf(tag, message, throwable) +} + +/** Basic println Logger implementation, to be used as dummy default implementation, of for testing + */ +class LoggerPrintln : Logger { + private fun printLog(verbosity: String, tag: String?, message: String?, throwable: Throwable?) { + println("$verbosity: $tag - $message${throwable?.let { " exception: ${it.message}" } ?: ""}") + } + + override fun v(tag: String?, message: String?, throwable: Throwable?) { + printLog("V", tag, message, throwable) + } + + override fun d(tag: String?, message: String?, throwable: Throwable?) { + printLog("D", tag, message, throwable) + } + + override fun i(tag: String?, message: String?, throwable: Throwable?) { + printLog("I", tag, message, throwable) + } + + override fun w(tag: String?, message: String?, throwable: Throwable?) { + printLog("W", tag, message, throwable) + } + + override fun e(tag: String?, message: String?, throwable: Throwable?) { + printLog("E", tag, message, throwable) + } + + override fun wtf(tag: String?, message: String?, throwable: Throwable?) { + printLog("WTF", tag, message, throwable) + } +} diff --git a/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/ResultExtensions.kt b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/ResultExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..1382b7cc13e66675acfe0bd4bc98eda817789fa8 --- /dev/null +++ b/notificationsreceiver-domain/src/main/java/foundation/e/notificationsreceiver/domain/utils/ResultExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.notificationsreceiver.domain.utils + +import kotlinx.coroutines.CancellationException +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +inline fun runSuspendCatching(block: () -> T): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + return runCatching(block).onFailure { + if (it is CancellationException) { + throw it + } + } +} diff --git a/notificationsreceiver-domain/src/test/java/foundation/e/notificationreceiver/domain/procedures/ExampleUnitTest.kt b/notificationsreceiver-domain/src/test/java/foundation/e/notificationreceiver/domain/procedures/ExampleUnitTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9afa6890bfd283f89bc71fab2145da8c9ea8efd --- /dev/null +++ b/notificationsreceiver-domain/src/test/java/foundation/e/notificationreceiver/domain/procedures/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package foundation.e.notificationreceiver.domain.procedures + +import junit.framework.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/notificationsreceiver/.gitignore b/notificationsreceiver/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/notificationsreceiver/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/notificationsreceiver/build.gradle.kts b/notificationsreceiver/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..a04e9b13efd344a1c7c783b186c78f8beb8a02e5 --- /dev/null +++ b/notificationsreceiver/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + alias(libs.plugins.hilt.android) + alias(libs.plugins.detekt) +} + +android { + namespace = "foundation.e.notificationsreceiver.bridges" + + compileSdk = libs.versions.app.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.app.minSdk.get().toInt() + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":notificationsreceiver-domain")) + + implementation(libs.dagger.hilt.android) + ksp(libs.dagger.hilt.compiler) + + testImplementation(libs.junit) +} + +detekt { + toolVersion = libs.versions.detekt.get() + config.setFrom(file("../detekt.yml")) + buildUponDefaultConfig = true + autoCorrect = true +} + +tasks.withType().configureEach { + jvmTarget = "17" +} +tasks.withType().configureEach { + jvmTarget = "17" +} diff --git a/notificationsreceiver/consumer-rules.pro b/notificationsreceiver/consumer-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notificationsreceiver/proguard-rules.pro b/notificationsreceiver/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /dev/null +++ b/notificationsreceiver/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/notificationsreceiver/src/main/AndroidManifest.xml b/notificationsreceiver/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a40236b94715132bb1e1fab5816fbcd58da432c --- /dev/null +++ b/notificationsreceiver/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/notificationsreceiver/src/main/java/foundation/e/notificationsreceiver/bridges/utils/BroadcastReceiverUtils.kt b/notificationsreceiver/src/main/java/foundation/e/notificationsreceiver/bridges/utils/BroadcastReceiverUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..8162cb6130b706a06b3a8d279dbddb96adeab629 --- /dev/null +++ b/notificationsreceiver/src/main/java/foundation/e/notificationsreceiver/bridges/utils/BroadcastReceiverUtils.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.notificationsreceiver.bridges.utils + +import android.content.BroadcastReceiver +import foundation.e.notificationsreceiver.domain.utils.e +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun BroadcastReceiver.goAsync(coroutineScope: CoroutineScope, block: suspend () -> Unit) { + val pendingResult = goAsync() + coroutineScope.launch { + @Suppress("TooGenericExceptionCaught") + try { + block() + } catch (ce: CancellationException) { + throw ce + } catch (e: Exception) { + e("Uncaught exception in BroadcastReceiver.goAsync bloc", e) + } finally { + pendingResult.finish() + } + } +} diff --git a/notificationsreceiver/src/test/java/foundation/e/notificationsreceiver/bridges/ExampleUnitTest.kt b/notificationsreceiver/src/test/java/foundation/e/notificationsreceiver/bridges/ExampleUnitTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cc3a135c7c4fef032795b4e5be6154335bc562cd --- /dev/null +++ b/notificationsreceiver/src/test/java/foundation/e/notificationsreceiver/bridges/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package foundation.e.notificationsreceiver.bridges + +import junit.framework.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle b/settings.gradle index 231bc04f38a0997f7e982aa5e667f8b537239c0e..b10c51f02e79e538a316da46e58f59acd3d56f52 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ rootProject.name='ntfy' include ':app' +include ':notificationsreceiver-domain' +include ":notificationsreceiver-domain:entitiesfixtures" +include ':notificationsreceiver'