Compare commits
19 commits
7-develop-
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
3fb7e6427f | ||
|
afeebcc4d1 | ||
|
9add68ed1a | ||
|
18fb197507 | ||
|
7c00376ae1 | ||
|
db299383c8 | ||
|
e480230f27 | ||
|
92611d1e81 | ||
|
3b1581a2ba | ||
|
78bf83acf4 | ||
|
5684aba36d | ||
|
61dadcba85 | ||
|
35711e7c1b | ||
|
4f97fee55c | ||
|
fe86329af5 | ||
|
160c4cfd88 | ||
|
a4eb268aa0 | ||
|
95ab609d94 | ||
|
5aceba6474 |
21
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
image: "registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:latest"
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export GRADLE_USER_HOME=$(pwd)/.gradle
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: ${CI_PROJECT_ID}
|
||||||
|
paths:
|
||||||
|
- .gradle/
|
||||||
|
|
||||||
|
buildRelease:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- ./gradlew assembleFdroid
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- app/build/outputs/apk/fdroid/release
|
|
@ -10,8 +10,8 @@ android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "io.heckel.ntfy"
|
applicationId "foundation.e.ntfy"
|
||||||
minSdkVersion 21
|
minSdkVersion 23
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
|
|
||||||
versionCode 33
|
versionCode 33
|
||||||
|
@ -27,6 +27,10 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
|
@ -41,7 +45,14 @@ android {
|
||||||
|
|
||||||
flavorDimensions "store"
|
flavorDimensions "store"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
|
play {
|
||||||
|
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
|
||||||
|
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
|
||||||
|
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
|
||||||
|
}
|
||||||
fdroid {
|
fdroid {
|
||||||
|
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
|
||||||
|
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
|
||||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +68,7 @@ android {
|
||||||
'-Xjvm-default=all-compatibility' // https://stackoverflow.com/a/71234042/1440785
|
'-Xjvm-default=all-compatibility' // https://stackoverflow.com/a/71234042/1440785
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
namespace "io.heckel.ntfy"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disables GoogleServices tasks for F-Droid variant
|
// Disables GoogleServices tasks for F-Droid variant
|
||||||
|
@ -103,6 +115,9 @@ dependencies {
|
||||||
// OkHttp (HTTP library)
|
// OkHttp (HTTP library)
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||||
|
|
||||||
|
// Firebase, sigh ... (only Google Play)
|
||||||
|
playImplementation 'com.google.firebase:firebase-messaging:23.1.2'
|
||||||
|
|
||||||
// RecyclerView
|
// RecyclerView
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
||||||
|
|
||||||
|
@ -118,4 +133,6 @@ dependencies {
|
||||||
|
|
||||||
// Image viewer
|
// Image viewer
|
||||||
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
|
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
|
||||||
|
|
||||||
|
implementation 'foundation.e:elib:0.0.1-alpha11'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
class FirebaseMessenger {
|
||||||
|
fun subscribe(topic: String) {
|
||||||
|
// Dummy to keep F-Droid flavor happy
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsubscribe(topic: String) {
|
||||||
|
// Dummy to keep F-Droid flavor happy
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
// Dummy to keep F-Droid flavor happy
|
||||||
|
class FirebaseService : Service() {
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="io.heckel.ntfy">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:persistent="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
@ -35,10 +36,10 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:exported="true">
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
@ -175,5 +176,25 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths"/>
|
android:resource="@xml/file_paths"/>
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.MainSettingsActivity"
|
||||||
|
android:theme="@style/PreferenceTheme"/>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".ui.SettingsActivityLink"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/eos_settings_title"
|
||||||
|
android:targetActivity=".ui.MainSettingsActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.android.settings.action.EXTRA_SETTINGS" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="com.android.settings.category"
|
||||||
|
android:value="com.android.settings.category.device" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.android.settings.icon"
|
||||||
|
android:resource="@drawable/ic_notification" />
|
||||||
|
</activity-alias>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.google.gson.stream.JsonReader
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
|
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||||
import io.heckel.ntfy.msg.NotificationService
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
|
@ -17,6 +18,7 @@ class Backuper(val context: Context) {
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val resolver = context.applicationContext.contentResolver
|
private val resolver = context.applicationContext.contentResolver
|
||||||
private val repository = (context.applicationContext as Application).repository
|
private val repository = (context.applicationContext as Application).repository
|
||||||
|
private val messenger = FirebaseMessenger()
|
||||||
private val notifier = NotificationService(context)
|
private val notifier = NotificationService(context)
|
||||||
|
|
||||||
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
|
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
|
||||||
|
@ -112,6 +114,11 @@ class Backuper(val context: Context) {
|
||||||
)
|
)
|
||||||
repository.addSubscription(subscription)
|
repository.addSubscription(subscription)
|
||||||
|
|
||||||
|
// Subscribe to Firebase topics
|
||||||
|
if (s.baseUrl == appBaseUrl) {
|
||||||
|
messenger.subscribe(s.topic)
|
||||||
|
}
|
||||||
|
|
||||||
// Create dedicated channels
|
// Create dedicated channels
|
||||||
if (s.dedicatedChannels) {
|
if (s.dedicatedChannels) {
|
||||||
notifier.createSubscriptionNotificationChannels(subscription)
|
notifier.createSubscriptionNotificationChannels(subscription)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
|
@ -88,19 +89,16 @@ class SubscriberService : Service() {
|
||||||
|
|
||||||
Log.init(this) // Init logs in all entry points
|
Log.init(this) // Init logs in all entry points
|
||||||
Log.d(TAG, "Subscriber service has been created")
|
Log.d(TAG, "Subscriber service has been created")
|
||||||
|
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
|
||||||
val text = getString(R.string.channel_subscriber_notification_noinstant_text)
|
|
||||||
notificationManager = createNotificationChannel()
|
|
||||||
serviceNotification = createNotification(title, text)
|
|
||||||
|
|
||||||
startForeground(NOTIFICATION_SERVICE_ID, serviceNotification)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d(TAG, "Subscriber service has been destroyed")
|
Log.d(TAG, "Subscriber service has been destroyed")
|
||||||
stopService()
|
stopService()
|
||||||
sendBroadcast(Intent(this, AutoRestartReceiver::class.java)) // Restart it if necessary!
|
val preferenceKey = getString(R.string.eos_preference_key_is_enabled)
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(preferenceKey, false)) {
|
||||||
|
sendBroadcast(Intent(this, AutoRestartReceiver::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +132,6 @@ class SubscriberService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wakeLock = null
|
wakeLock = null
|
||||||
stopForeground(true)
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, "Service stopped without being started: ${e.message}")
|
Log.d(TAG, "Service stopped without being started: ${e.message}")
|
||||||
|
@ -217,7 +214,18 @@ class SubscriberService : Service() {
|
||||||
// Update foreground service notification popup
|
// Update foreground service notification popup
|
||||||
if (connections.size > 0) {
|
if (connections.size > 0) {
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
val title = getString(R.string.channel_subscriber_notification_title)
|
||||||
val text = when (instantSubscriptions.size) {
|
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
||||||
|
when (instantSubscriptions.size) {
|
||||||
|
1 -> getString(R.string.channel_subscriber_notification_instant_text_one)
|
||||||
|
2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
|
||||||
|
3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
|
||||||
|
4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
|
||||||
|
5 -> getString(R.string.channel_subscriber_notification_instant_text_five)
|
||||||
|
6 -> getString(R.string.channel_subscriber_notification_instant_text_six)
|
||||||
|
else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (instantSubscriptions.size) {
|
||||||
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
||||||
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
||||||
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
||||||
|
@ -226,6 +234,7 @@ class SubscriberService : Service() {
|
||||||
6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
|
6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
|
||||||
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
serviceNotification = createNotification(title, text)
|
serviceNotification = createNotification(title, text)
|
||||||
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@ package io.heckel.ntfy.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
|
import io.heckel.ntfy.R
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ -43,11 +44,17 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${id})")
|
Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${id})")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val app = context.applicationContext as Application
|
val app = context.applicationContext as Application
|
||||||
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(app)
|
||||||
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
|
val preferenceKey = context.getString(R.string.eos_preference_key_is_enabled)
|
||||||
val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP
|
val action = if (sharedPreferences.getBoolean(preferenceKey, false)) {
|
||||||
|
SubscriberService.Action.START
|
||||||
|
} else {
|
||||||
|
SubscriberService.Action.STOP
|
||||||
|
}
|
||||||
|
|
||||||
val serviceState = SubscriberService.readServiceState(context)
|
val serviceState = SubscriberService.readServiceState(context)
|
||||||
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) {
|
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) {
|
||||||
return@withContext Result.success()
|
return@withContext Result.success()
|
||||||
|
@ -55,7 +62,7 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${id})")
|
Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${id})")
|
||||||
Intent(context, SubscriberService::class.java).also {
|
Intent(context, SubscriberService::class.java).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
ContextCompat.startForegroundService(context, it)
|
context.startService(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
|
|
|
@ -114,7 +114,10 @@ class AddFragment : DialogFragment() {
|
||||||
// Set foreground description text
|
// Set foreground description text
|
||||||
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
||||||
|
|
||||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
|
||||||
|
if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||||
|
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Add baseUrl auto-complete behavior
|
// Add baseUrl auto-complete behavior
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -288,6 +291,16 @@ class AddFragment : DialogFragment() {
|
||||||
private fun validateInputSubscribeView() {
|
private fun validateInputSubscribeView() {
|
||||||
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
|
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
|
||||||
|
|
||||||
|
// Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
|
||||||
|
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||||
|
false
|
||||||
|
} else if (subscribeUseAnotherServerCheckbox.isChecked && subscribeBaseUrlText.text.toString() == appBaseUrl) {
|
||||||
|
true
|
||||||
|
} else if (!subscribeUseAnotherServerCheckbox.isChecked && defaultBaseUrl == null) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
if (subscribeUseAnotherServerCheckbox.isChecked) {
|
if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||||
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
||||||
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
||||||
|
@ -295,9 +308,15 @@ class AddFragment : DialogFragment() {
|
||||||
subscribeUseAnotherServerDescription.visibility = View.GONE
|
subscribeUseAnotherServerDescription.visibility = View.GONE
|
||||||
subscribeBaseUrlLayout.visibility = View.GONE
|
subscribeBaseUrlLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
if (instantToggleAllowed) {
|
||||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
subscribeInstantDeliveryBox.visibility = View.VISIBLE
|
||||||
subscribeForegroundDescription.visibility = View.GONE
|
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
|
||||||
|
subscribeForegroundDescription.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||||
|
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||||
|
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Enable/disable "Subscribe" button
|
// Enable/disable "Subscribe" button
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -337,7 +356,7 @@ class AddFragment : DialogFragment() {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
val topic = subscribeTopicText.text.toString()
|
val topic = subscribeTopicText.text.toString()
|
||||||
val baseUrl = getBaseUrl()
|
val baseUrl = getBaseUrl()
|
||||||
val instant = baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
|
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.NotificationService
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
|
@ -46,6 +47,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
}
|
}
|
||||||
private val repository by lazy { (application as Application).repository }
|
private val repository by lazy { (application as Application).repository }
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
|
private val messenger = FirebaseMessenger()
|
||||||
private var notifier: NotificationService? = null // Context-dependent
|
private var notifier: NotificationService? = null // Context-dependent
|
||||||
private var appBaseUrl: String? = null // Context-dependent
|
private var appBaseUrl: String? = null // Context-dependent
|
||||||
|
|
||||||
|
@ -125,6 +127,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
)
|
)
|
||||||
repository.addSubscription(subscription)
|
repository.addSubscription(subscription)
|
||||||
|
|
||||||
|
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
|
||||||
|
if (baseUrl == appBaseUrl) {
|
||||||
|
Log.d(TAG, "Subscribing to Firebase topic $topic")
|
||||||
|
messenger.subscribe(topic)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch cached messages
|
// Fetch cached messages
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
|
@ -521,8 +529,14 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
val appBaseUrl = getString(R.string.app_base_url)
|
val appBaseUrl = getString(R.string.app_base_url)
|
||||||
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
|
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
|
||||||
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
|
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
|
||||||
enableInstantItem?.isVisible = false
|
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
|
||||||
disableInstantItem?.isVisible = false
|
if (allowToggleInstant) {
|
||||||
|
enableInstantItem?.isVisible = !subscriptionInstant
|
||||||
|
disableInstantItem?.isVisible = subscriptionInstant
|
||||||
|
} else {
|
||||||
|
enableInstantItem?.isVisible = false
|
||||||
|
disableInstantItem?.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,6 +608,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
repository.removeAllNotifications(subscriptionId)
|
repository.removeAllNotifications(subscriptionId)
|
||||||
repository.removeSubscription(subscriptionId)
|
repository.removeSubscription(subscriptionId)
|
||||||
|
if (subscriptionBaseUrl == appBaseUrl) {
|
||||||
|
messenger.unsubscribe(subscriptionTopic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,7 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun loadView() {
|
private fun loadView() {
|
||||||
if (subscription.upAppId == null) {
|
if (subscription.upAppId == null) {
|
||||||
|
loadInstantPref()
|
||||||
loadMutedUntilPref()
|
loadMutedUntilPref()
|
||||||
loadMinPriorityPref()
|
loadMinPriorityPref()
|
||||||
loadAutoDeletePref()
|
loadAutoDeletePref()
|
||||||
|
@ -133,6 +134,29 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
loadTopicUrlPref()
|
loadTopicUrlPref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadInstantPref() {
|
||||||
|
val appBaseUrl = getString(R.string.app_base_url)
|
||||||
|
val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return
|
||||||
|
val pref: SwitchPreference? = findPreference(prefId)
|
||||||
|
pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl
|
||||||
|
pref?.isChecked = subscription.instant
|
||||||
|
pref?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
save(subscription.copy(instant = value), refresh = true)
|
||||||
|
}
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return subscription.instant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference ->
|
||||||
|
if (preference.isChecked) {
|
||||||
|
getString(R.string.detail_settings_notifications_instant_summary_on)
|
||||||
|
} else {
|
||||||
|
getString(R.string.detail_settings_notifications_instant_summary_off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadDedicatedChannelsPrefs() {
|
private fun loadDedicatedChannelsPrefs() {
|
||||||
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
||||||
val pref: SwitchPreference? = findPreference(prefId)
|
val pref: SwitchPreference? = findPreference(prefId)
|
||||||
|
|
|
@ -34,6 +34,7 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
|
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.DownloadManager
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
import io.heckel.ntfy.msg.DownloadType
|
import io.heckel.ntfy.msg.DownloadType
|
||||||
|
@ -56,6 +57,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
}
|
}
|
||||||
private val repository by lazy { (application as Application).repository }
|
private val repository by lazy { (application as Application).repository }
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
|
private val messenger = FirebaseMessenger()
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
private lateinit var menu: Menu
|
private lateinit var menu: Menu
|
||||||
|
@ -197,6 +199,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
// Create notification channels right away, so we can configure them immediately after installing the app
|
// Create notification channels right away, so we can configure them immediately after installing the app
|
||||||
dispatcher?.init()
|
dispatcher?.init()
|
||||||
|
|
||||||
|
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
|
||||||
|
messenger.subscribe(ApiService.CONTROL_TOPIC)
|
||||||
|
|
||||||
// Darrkkkk mode
|
// Darrkkkk mode
|
||||||
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
||||||
|
|
||||||
|
@ -207,36 +212,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
maybeRequestNotificationPermission()
|
maybeRequestNotificationPermission()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val subscriptions = repository.getSubscriptions()
|
|
||||||
val defaultTopic = "murena_notification"
|
|
||||||
val hasTestTopic = subscriptions.any { it.topic == defaultTopic }
|
|
||||||
if (hasTestTopic) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val subscription = Subscription(
|
|
||||||
id = randomSubscriptionId(),
|
|
||||||
baseUrl = appBaseUrl!!,
|
|
||||||
topic = defaultTopic,
|
|
||||||
instant = true,
|
|
||||||
dedicatedChannels = false,
|
|
||||||
mutedUntil = 0,
|
|
||||||
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
|
|
||||||
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
|
|
||||||
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
|
|
||||||
lastNotificationId = null,
|
|
||||||
icon = null,
|
|
||||||
upAppId = null,
|
|
||||||
upConnectorToken = null,
|
|
||||||
displayName = null,
|
|
||||||
totalCount = 0,
|
|
||||||
newCount = 0,
|
|
||||||
lastActive = Date().time / 1000
|
|
||||||
)
|
|
||||||
viewModel.add(subscription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRequestNotificationPermission() {
|
private fun maybeRequestNotificationPermission() {
|
||||||
|
@ -374,6 +349,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
}
|
}
|
||||||
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
// Show/hide in-app rate widget
|
||||||
|
val rateAppItem = menu.findItem(R.id.main_menu_rate)
|
||||||
|
rateAppItem.isVisible = BuildConfig.RATE_APP_AVAILABLE
|
||||||
|
|
||||||
// Pause notification icons
|
// Pause notification icons
|
||||||
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
||||||
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
||||||
|
@ -406,7 +385,26 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
startActivity(Intent(this, SettingsActivity::class.java))
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.main_menu_report_bug -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_report_bug_url))))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.main_menu_rate -> {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")))
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")))
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.main_menu_donate -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url))))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.main_menu_docs -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_docs_url))))
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -468,6 +466,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
)
|
)
|
||||||
viewModel.add(subscription)
|
viewModel.add(subscription)
|
||||||
|
|
||||||
|
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
|
||||||
|
if (baseUrl == appBaseUrl) {
|
||||||
|
Log.d(TAG, "Subscribing to Firebase topic $topic")
|
||||||
|
messenger.subscribe(topic)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch cached messages
|
// Fetch cached messages
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -105,7 +105,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
||||||
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
||||||
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
|
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
|
||||||
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
|
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
|
||||||
instantImageView.visibility = View.GONE
|
instantImageView.visibility = if (subscription.instant && BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||||
if (isUnifiedPush || subscription.newCount == 0) {
|
if (isUnifiedPush || subscription.newCount == 0) {
|
||||||
newItemsView.visibility = View.GONE
|
newItemsView.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|
71
app/src/main/java/io/heckel/ntfy/ui/MainSettingsActivity.kt
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowInsetsController
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.databinding.MainSettingsActivityBinding
|
||||||
|
|
||||||
|
class MainSettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var mBinding: MainSettingsActivityBinding
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mBinding = MainSettingsActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(mBinding.root)
|
||||||
|
|
||||||
|
setupToolbar()
|
||||||
|
setSystemBarsAppearance()
|
||||||
|
showPreferencesFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
|
mBinding.toolbar.setNavigationOnClickListener {
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun setSystemBarsAppearance() {
|
||||||
|
val insetsController = window.insetsController ?: return
|
||||||
|
|
||||||
|
val isLightMode = isSystemInLightMode()
|
||||||
|
if (isLightMode) {
|
||||||
|
insetsController.setSystemBarsAppearance(
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||||
|
)
|
||||||
|
insetsController.setSystemBarsAppearance(
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
insetsController.setSystemBarsAppearance(
|
||||||
|
0,
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||||
|
)
|
||||||
|
insetsController.setSystemBarsAppearance(
|
||||||
|
0,
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSystemInLightMode(): Boolean {
|
||||||
|
val nightModeFlags = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
return nightModeFlags != Configuration.UI_MODE_NIGHT_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPreferencesFragment() {
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.fragment_container, PreferencesFragment())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
37
app/src/main/java/io/heckel/ntfy/ui/PreferencesFragment.kt
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.Toolbar
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
|
import io.heckel.ntfy.util.Log
|
||||||
|
|
||||||
|
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.settings_preferences, rootKey)
|
||||||
|
|
||||||
|
val preference: SwitchPreferenceCompat? =
|
||||||
|
findPreference(getString(R.string.eos_preference_key_is_enabled))
|
||||||
|
|
||||||
|
preference?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val isChecked = newValue as Boolean
|
||||||
|
val intent = Intent(context, SubscriberService::class.java)
|
||||||
|
intent.action = if (isChecked) {
|
||||||
|
SubscriberService.Action.START.name
|
||||||
|
} else {
|
||||||
|
SubscriberService.Action.STOP.name
|
||||||
|
}
|
||||||
|
|
||||||
|
requireContext().startService(intent)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
||||||
|
|
||||||
// Add subscription
|
// Add subscription
|
||||||
val baseUrl = repository.getDefaultBaseUrl() ?: context.getString(R.string.app_base_url)
|
val baseUrl = repository.getDefaultBaseUrl() ?: context.getString(R.string.app_base_url)
|
||||||
val topic = "murena_notification"
|
val topic = UP_PREFIX + randomString(TOPIC_RANDOM_ID_LENGTH)
|
||||||
val endpoint = topicUrlUp(baseUrl, topic)
|
val endpoint = topicUrlUp(baseUrl, topic)
|
||||||
val subscription = Subscription(
|
val subscription = Subscription(
|
||||||
id = randomSubscriptionId(),
|
id = randomSubscriptionId(),
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="50dp"
|
android:width="22dp"
|
||||||
android:height="50dp"
|
android:height="22dp"
|
||||||
android:viewportWidth="50"
|
android:viewportWidth="50"
|
||||||
android:viewportHeight="50">
|
android:viewportHeight="50">
|
||||||
<path
|
<path
|
||||||
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
|
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
|
||||||
android:strokeWidth="0.754022"
|
android:strokeWidth="0.754022"
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="?android:attr/colorControlNormal"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
|
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
|
||||||
android:strokeWidth="0.525121"
|
android:strokeWidth="0.525121"
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="?android:attr/colorControlNormal"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
|
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
|
||||||
android:strokeWidth="0.525121"
|
android:strokeWidth="0.525121"
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="?android:attr/colorControlNormal"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
|
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
|
||||||
android:strokeWidth="0.525121"
|
android:strokeWidth="0.525121"
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="?android:attr/colorControlNormal"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
|
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
|
||||||
android:strokeWidth="0.525121"
|
android:strokeWidth="0.525121"
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="?android:attr/colorControlNormal"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
49
app/src/main/res/layout/main_settings_activity.xml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/collapsing_toolbar"
|
||||||
|
style="?attr/collapsingToolbarLayoutLargeStyle"
|
||||||
|
android:background="@color/e_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||||
|
android:layout_height="?attr/collapsingToolbarLayoutLargeSize">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
app:navigationIcon="@drawable/e_ic_back"
|
||||||
|
app:title="@string/eos_settings_title"
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:background="@color/e_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragment_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -6,4 +6,8 @@
|
||||||
<item android:id="@+id/main_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
<item android:id="@+id/main_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
||||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
||||||
<item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/>
|
<item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/>
|
||||||
|
<item android:id="@+id/main_menu_docs" android:title="@string/main_menu_docs_title"/>
|
||||||
|
<item android:id="@+id/main_menu_rate" android:title="@string/main_menu_rate_title"/>
|
||||||
|
<item android:id="@+id/main_menu_donate" android:title="@string/main_menu_donate_title"/>
|
||||||
|
<item android:id="@+id/main_menu_report_bug" android:title="@string/main_menu_report_bug_title"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -343,4 +343,6 @@
|
||||||
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy arbeitet nicht als UnifiedPush-Distributor</string>
|
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy arbeitet nicht als UnifiedPush-Distributor</string>
|
||||||
<string name="settings_advanced_unifiedpush_title">UnifiedPush aktivieren</string>
|
<string name="settings_advanced_unifiedpush_title">UnifiedPush aktivieren</string>
|
||||||
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy arbeitet als UnifiedPush-Distributor</string>
|
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy arbeitet als UnifiedPush-Distributor</string>
|
||||||
|
<string name="eos_settings_enable_title">Den Verteiler aktivieren</string>
|
||||||
|
<string name="eos_settings_enable_description">Es ermöglicht Drittanbieteranwendungen, UnifiedPush-Benachrichtigungen zu empfangen</string>
|
||||||
</resources>
|
</resources>
|
|
@ -343,4 +343,6 @@
|
||||||
<string name="settings_advanced_unifiedpush_title">Activar UnifiedPush</string>
|
<string name="settings_advanced_unifiedpush_title">Activar UnifiedPush</string>
|
||||||
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy actuará como distribuidor UnifiedPush</string>
|
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy actuará como distribuidor UnifiedPush</string>
|
||||||
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy no actuará como distribuidor UnifiedPush</string>
|
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy no actuará como distribuidor UnifiedPush</string>
|
||||||
|
<string name="eos_settings_enable_title">Habilitar el distribuidor</string>
|
||||||
|
<string name="eos_settings_enable_description">Permite a las aplicaciones de terceros recibir notificaciones de UnifiedPush</string>
|
||||||
</resources>
|
</resources>
|
|
@ -343,4 +343,6 @@
|
||||||
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy agira comme un distributeur UnifiedPush</string>
|
<string name="settings_advanced_unifiedpush_summary_enabled">ntfy agira comme un distributeur UnifiedPush</string>
|
||||||
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy n\'agira pas comme un distributeur UnifiedPush</string>
|
<string name="settings_advanced_unifiedpush_summary_disabled">ntfy n\'agira pas comme un distributeur UnifiedPush</string>
|
||||||
<string name="settings_advanced_unifiedpush_title">Activer le \"UnifiedPush\"</string>
|
<string name="settings_advanced_unifiedpush_title">Activer le \"UnifiedPush\"</string>
|
||||||
|
<string name="eos_settings_enable_title">Activer le distributeur</string>
|
||||||
|
<string name="eos_settings_enable_description">Cela permet aux applications tierces de recevoir des notifications UnifiedPush</string>
|
||||||
</resources>
|
</resources>
|
|
@ -330,4 +330,6 @@
|
||||||
<string name="channel_notifications_group_default_name">Default</string>
|
<string name="channel_notifications_group_default_name">Default</string>
|
||||||
<string name="main_menu_donate_title">Dona 💸</string>
|
<string name="main_menu_donate_title">Dona 💸</string>
|
||||||
<string name="detail_item_cannot_open_apk">Le app non possono più essere installate: devono essere scaricate via browser. Vedi l\'issue #531 per dettagli.</string>
|
<string name="detail_item_cannot_open_apk">Le app non possono più essere installate: devono essere scaricate via browser. Vedi l\'issue #531 per dettagli.</string>
|
||||||
|
<string name="eos_settings_enable_title">Abilitare il distributore</string>
|
||||||
|
<string name="eos_settings_enable_description">Consente alle applicazioni di terze parti di ricevere notifiche UnifiedPush</string>
|
||||||
</resources>
|
</resources>
|
|
@ -391,4 +391,10 @@
|
||||||
<string name="user_dialog_button_cancel">Cancel</string>
|
<string name="user_dialog_button_cancel">Cancel</string>
|
||||||
<string name="user_dialog_button_delete">Delete user</string>
|
<string name="user_dialog_button_delete">Delete user</string>
|
||||||
<string name="user_dialog_button_save">Save</string>
|
<string name="user_dialog_button_save">Save</string>
|
||||||
|
|
||||||
|
<!-- /e/OS integration preferences -->
|
||||||
|
<string name="eos_preference_key_is_enabled" translatable="false">isEnabled</string>
|
||||||
|
<string name="eos_settings_title" translatable="false">UnifiedPush</string>
|
||||||
|
<string name="eos_settings_enable_title" translatable="true">Enable the distributor</string>
|
||||||
|
<string name="eos_settings_enable_description" translatable="true">It allows 3rd party applications to receive UnifiedPush notifications</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -30,4 +30,26 @@
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
<item name="cornerSize">5dp</item>
|
<item name="cornerSize">5dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PreferenceTheme" parent="PreferenceTheme.Base"/>
|
||||||
|
<style name="PreferenceTheme.Base" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/e_action_bar</item>
|
||||||
|
<item name="colorPrimaryDark">@color/e_action_bar</item>
|
||||||
|
<item name="colorAccent">@color/e_accent</item>
|
||||||
|
<item name="android:textColorPrimary">@color/e_primary_text_color</item>
|
||||||
|
<item name="android:textColorSecondary">@color/e_secondary_text_color</item>
|
||||||
|
<item name="android:textColorPrimaryInverse">@color/e_background</item>
|
||||||
|
<item name="android:windowBackground">@color/e_background</item>
|
||||||
|
<item name="colorControlActivated">@color/e_accent</item>
|
||||||
|
<item name="colorButtonNormal">@color/e_icon_color</item>
|
||||||
|
<item name="colorControlHighlight">@color/e_icon_color</item>
|
||||||
|
<item name="homeAsUpIndicator">@drawable/e_ic_back</item>
|
||||||
|
<item name="android:homeAsUpIndicator">@drawable/e_ic_back</item>
|
||||||
|
<item name="android:popupBackground">@color/e_floating_background</item>
|
||||||
|
<item name="android:divider">@color/e_divider_color</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:statusBarColor">@color/e_background</item>
|
||||||
|
<item name="switchStyle">@style/ETheme.Switch</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
app:entryValues="@array/settings_general_dark_mode_values"
|
app:entryValues="@array/settings_general_dark_mode_values"
|
||||||
app:defaultValue="-1"/>
|
app:defaultValue="-1"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_backup_restore_header" app:isPreferenceVisible="false">
|
<PreferenceCategory app:title="@string/settings_backup_restore_header">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:key="@string/settings_backup_restore_backup_key"
|
app:key="@string/settings_backup_restore_backup_key"
|
||||||
app:title="@string/settings_backup_restore_backup_title"
|
app:title="@string/settings_backup_restore_backup_title"
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
app:title="@string/settings_advanced_clear_logs_title"
|
app:title="@string/settings_advanced_clear_logs_title"
|
||||||
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_about_header" app:isPreferenceVisible="false">
|
<PreferenceCategory app:title="@string/settings_about_header">
|
||||||
<Preference
|
<Preference
|
||||||
app:key="@string/settings_about_version_key"
|
app:key="@string/settings_about_version_key"
|
||||||
app:title="@string/settings_about_version_title"/>
|
app:title="@string/settings_about_version_title"/>
|
||||||
|
|
11
app/src/main/res/xml/settings_preferences.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<PreferenceScreen
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:icon="@drawable/ic_notification"
|
||||||
|
app:key="@string/eos_preference_key_is_enabled"
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:title="@string/eos_settings_enable_title"
|
||||||
|
app:summary="@string/eos_settings_enable_description" />
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
|
@ -0,0 +1,36 @@
|
||||||
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import io.heckel.ntfy.util.Log
|
||||||
|
|
||||||
|
class FirebaseMessenger {
|
||||||
|
fun subscribe(topic: String) {
|
||||||
|
val firebase = maybeInstance() ?: return
|
||||||
|
firebase
|
||||||
|
.subscribeToTopic(topic)
|
||||||
|
.addOnCompleteListener {
|
||||||
|
Log.d(TAG, "Subscribing to topic $topic complete: result=${it.result}, exception=${it.exception}, successful=${it.isSuccessful}")
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Subscribing to topic $topic failed: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsubscribe(topic: String) {
|
||||||
|
val firebase = maybeInstance() ?: return
|
||||||
|
firebase.unsubscribeFromTopic(topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeInstance(): FirebaseMessaging? {
|
||||||
|
return try {
|
||||||
|
FirebaseMessaging.getInstance()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Firebase instance unavailable: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NtfyFirebase"
|
||||||
|
}
|
||||||
|
}
|
166
app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.work.*
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.app.Application
|
||||||
|
import io.heckel.ntfy.db.Attachment
|
||||||
|
import io.heckel.ntfy.db.Icon
|
||||||
|
import io.heckel.ntfy.db.Notification
|
||||||
|
import io.heckel.ntfy.util.Log
|
||||||
|
import io.heckel.ntfy.msg.ApiService
|
||||||
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
|
import io.heckel.ntfy.util.nullIfZero
|
||||||
|
import io.heckel.ntfy.util.toPriority
|
||||||
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
|
import io.heckel.ntfy.work.PollWorker
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class FirebaseService : FirebaseMessagingService() {
|
||||||
|
private val repository by lazy { (application as Application).repository }
|
||||||
|
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val messenger = FirebaseMessenger()
|
||||||
|
private val parser = NotificationParser()
|
||||||
|
|
||||||
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
|
// Init log (this is done in all entrypoints)
|
||||||
|
Log.init(this)
|
||||||
|
|
||||||
|
// We only process data messages
|
||||||
|
if (remoteMessage.data.isEmpty()) {
|
||||||
|
Log.d(TAG, "Discarding unexpected message (1): from=${remoteMessage.from}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch event
|
||||||
|
val data = remoteMessage.data
|
||||||
|
when (data["event"]) {
|
||||||
|
ApiService.EVENT_MESSAGE -> handleMessage(remoteMessage)
|
||||||
|
ApiService.EVENT_KEEPALIVE -> handleKeepalive(remoteMessage)
|
||||||
|
ApiService.EVENT_POLL_REQUEST -> handlePollRequest(remoteMessage)
|
||||||
|
else -> Log.d(TAG, "Discarding unexpected message (2): from=${remoteMessage.from}, data=${data}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleKeepalive(remoteMessage: RemoteMessage) {
|
||||||
|
Log.d(TAG, "Keepalive received, sending auto restart broadcast for foregrounds service")
|
||||||
|
sendBroadcast(Intent(this, SubscriberService.AutoRestartReceiver::class.java)) // Restart it if necessary!
|
||||||
|
val topic = remoteMessage.data["topic"]
|
||||||
|
if (topic != ApiService.CONTROL_TOPIC) {
|
||||||
|
Log.d(TAG, "Keepalive on non-control topic $topic received, subscribing to control topic ${ApiService.CONTROL_TOPIC}")
|
||||||
|
messenger.subscribe(ApiService.CONTROL_TOPIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePollRequest(remoteMessage: RemoteMessage) {
|
||||||
|
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
|
||||||
|
val topic = remoteMessage.data["topic"] ?: return
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}"
|
||||||
|
val workManager = WorkManager.getInstance(this)
|
||||||
|
val workRequest = OneTimeWorkRequest.Builder(PollWorker::class.java)
|
||||||
|
.setInputData(workDataOf(
|
||||||
|
PollWorker.INPUT_DATA_BASE_URL to baseUrl,
|
||||||
|
PollWorker.INPUT_DATA_TOPIC to topic
|
||||||
|
))
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
Log.d(TAG, "Poll request for ${topicShortUrl(baseUrl, topic)} received, scheduling unique poll worker with name $workName")
|
||||||
|
|
||||||
|
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMessage(remoteMessage: RemoteMessage) {
|
||||||
|
val data = remoteMessage.data
|
||||||
|
val id = data["id"]
|
||||||
|
val timestamp = data["time"]?.toLongOrNull()
|
||||||
|
val topic = data["topic"]
|
||||||
|
val title = data["title"]
|
||||||
|
val message = data["message"]
|
||||||
|
val priority = data["priority"]?.toIntOrNull()
|
||||||
|
val tags = data["tags"]
|
||||||
|
val click = data["click"]
|
||||||
|
val iconUrl = data["icon"]
|
||||||
|
val actions = data["actions"] // JSON array as string, sigh ...
|
||||||
|
val encoding = data["encoding"]
|
||||||
|
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
||||||
|
val attachmentType = data["attachment_type"]
|
||||||
|
val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero()
|
||||||
|
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero()
|
||||||
|
val attachmentUrl = data["attachment_url"]
|
||||||
|
val truncated = (data["truncated"] ?: "") == "1"
|
||||||
|
if (id == null || topic == null || message == null || timestamp == null) {
|
||||||
|
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Received message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
|
|
||||||
|
CoroutineScope(job).launch {
|
||||||
|
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
|
||||||
|
|
||||||
|
// Check if notification was truncated and discard if it will (or likely already did) arrive via instant delivery
|
||||||
|
val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch
|
||||||
|
if (truncated && subscription.instant) {
|
||||||
|
Log.d(TAG, "Discarding truncated message that did/will arrive via instant delivery: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add notification
|
||||||
|
val attachment = if (attachmentUrl != null) {
|
||||||
|
Attachment(
|
||||||
|
name = attachmentName,
|
||||||
|
type = attachmentType,
|
||||||
|
size = attachmentSize,
|
||||||
|
expires = attachmentExpires,
|
||||||
|
url = attachmentUrl,
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
val icon: Icon? = if (iconUrl != null && iconUrl != "") Icon(url = iconUrl) else null
|
||||||
|
val notification = Notification(
|
||||||
|
id = id,
|
||||||
|
subscriptionId = subscription.id,
|
||||||
|
timestamp = timestamp,
|
||||||
|
title = title ?: "",
|
||||||
|
message = message,
|
||||||
|
encoding = encoding ?: "",
|
||||||
|
priority = toPriority(priority),
|
||||||
|
tags = tags ?: "",
|
||||||
|
click = click ?: "",
|
||||||
|
icon = icon,
|
||||||
|
actions = parser.parseActions(actions),
|
||||||
|
attachment = attachment,
|
||||||
|
notificationId = Random.nextInt(),
|
||||||
|
deleted = false
|
||||||
|
)
|
||||||
|
if (repository.addNotification(notification)) {
|
||||||
|
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
|
dispatcher.dispatch(subscription, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
// Called if the FCM registration token is updated
|
||||||
|
// We don't actually use or care about the token, since we're using topics
|
||||||
|
Log.d(TAG, "Registration token was updated: $token")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NtfyFirebase"
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://gitlab.e.foundation/api/v4/groups/9/-/packages/maven'}
|
||||||
maven { url "https://jitpack.io" } // For StfalconImageViewer
|
maven { url "https://jitpack.io" } // For StfalconImageViewer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
rootProject.name='ntfy'
|
rootProject.name='ntfy'
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':ui'
|
|
||||||
|
|
1
ui/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/build
|
|
|
@ -1,45 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'foundation.e.ntefy.ui'
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "foundation.e.ntefy.ui"
|
|
||||||
minSdk 29
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
|
||||||
|
|
||||||
implementation("com.github.UnifiedPush:android-connector:2.2.0")
|
|
||||||
}
|
|
21
ui/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,24 +0,0 @@
|
||||||
package foundation.e.ntefy.ui
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("foundation.e.ntefy.ui", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.Ntfy">
|
|
||||||
<activity android:exported="true" android:name=".MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPocReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
|
||||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
|
||||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
|
||||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,14 +0,0 @@
|
||||||
package foundation.e.ntefy.ui
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
|
||||||
import org.unifiedpush.android.connector.UnifiedPush
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
UnifiedPush.registerAppWithDialog(this)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package foundation.e.ntefy.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import org.unifiedpush.android.connector.MessagingReceiver
|
|
||||||
|
|
||||||
class UnifiedPocReceiver: MessagingReceiver() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "UnifiedPocReceiver"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
|
||||||
super.onMessage(context, message, instance)
|
|
||||||
val utf8Message = String(message)
|
|
||||||
|
|
||||||
Log.i(TAG, "onMessage $utf8Message")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
|
||||||
super.onNewEndpoint(context, endpoint, instance)
|
|
||||||
Log.i(TAG, "onNewEndpoint $endpoint")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity"/>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 7.6 KiB |
|
@ -1,16 +0,0 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.Ntfy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<resources>
|
|
||||||
<string name="app_name">UI</string>
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.Ntfy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
|
@ -1,17 +0,0 @@
|
||||||
package foundation.e.ntefy.ui
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|