Compare commits
19 commits
4-rename-a
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
3fb7e6427f | ||
|
afeebcc4d1 | ||
|
9add68ed1a | ||
|
18fb197507 | ||
|
7c00376ae1 | ||
|
db299383c8 | ||
|
e480230f27 | ||
|
92611d1e81 | ||
|
3b1581a2ba | ||
|
78bf83acf4 | ||
|
5684aba36d | ||
|
61dadcba85 | ||
|
35711e7c1b | ||
|
4f97fee55c | ||
|
fe86329af5 | ||
|
160c4cfd88 | ||
|
a4eb268aa0 | ||
|
95ab609d94 | ||
|
5aceba6474 |
194 changed files with 650 additions and 39 deletions
21
.gitlab-ci.yml
Normal file
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
|
||||
|
||||
defaultConfig {
|
||||
applicationId "io.heckel.ntfy"
|
||||
minSdkVersion 21
|
||||
applicationId "foundation.e.ntfy"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
|
||||
versionCode 33
|
||||
|
@ -27,6 +27,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
|
@ -41,7 +45,14 @@ android {
|
|||
|
||||
flavorDimensions "store"
|
||||
productFlavors {
|
||||
play {
|
||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
|
||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
|
||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
|
||||
}
|
||||
fdroid {
|
||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
|
||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
|
||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +68,7 @@ android {
|
|||
'-Xjvm-default=all-compatibility' // https://stackoverflow.com/a/71234042/1440785
|
||||
]
|
||||
}
|
||||
namespace "io.heckel.ntfy"
|
||||
}
|
||||
|
||||
// Disables GoogleServices tasks for F-Droid variant
|
||||
|
@ -103,6 +115,9 @@ dependencies {
|
|||
// OkHttp (HTTP library)
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
|
||||
// Firebase, sigh ... (only Google Play)
|
||||
playImplementation 'com.google.firebase:firebase-messaging:23.1.2'
|
||||
|
||||
// RecyclerView
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
||||
|
||||
|
@ -118,4 +133,6 @@ dependencies {
|
|||
|
||||
// Image viewer
|
||||
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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.heckel.ntfy">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
@ -25,6 +25,7 @@
|
|||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:persistent="true"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
|
@ -35,10 +36,10 @@
|
|||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true">
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
@ -175,5 +176,25 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</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>
|
||||
</manifest>
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
@ -8,6 +8,7 @@ import com.google.gson.stream.JsonReader
|
|||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
|
@ -17,6 +18,7 @@ class Backuper(val context: Context) {
|
|||
private val gson = Gson()
|
||||
private val resolver = context.applicationContext.contentResolver
|
||||
private val repository = (context.applicationContext as Application).repository
|
||||
private val messenger = FirebaseMessenger()
|
||||
private val notifier = NotificationService(context)
|
||||
|
||||
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)
|
||||
|
||||
// Subscribe to Firebase topics
|
||||
if (s.baseUrl == appBaseUrl) {
|
||||
messenger.subscribe(s.topic)
|
||||
}
|
||||
|
||||
// Create dedicated channels
|
||||
if (s.dedicatedChannels) {
|
||||
notifier.createSubscriptionNotificationChannels(subscription)
|
|
@ -10,6 +10,7 @@ import android.os.PowerManager
|
|||
import android.os.SystemClock
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.heckel.ntfy.BuildConfig
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.app.Application
|
||||
|
@ -88,19 +89,16 @@ class SubscriberService : Service() {
|
|||
|
||||
Log.init(this) // Init logs in all entry points
|
||||
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() {
|
||||
Log.d(TAG, "Subscriber service has been destroyed")
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -134,7 +132,6 @@ class SubscriberService : Service() {
|
|||
}
|
||||
}
|
||||
wakeLock = null
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Service stopped without being started: ${e.message}")
|
||||
|
@ -217,7 +214,18 @@ class SubscriberService : Service() {
|
|||
// Update foreground service notification popup
|
||||
if (connections.size > 0) {
|
||||
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)
|
||||
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
||||
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)
|
||||
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
||||
}
|
||||
}
|
||||
serviceNotification = createNotification(title, text)
|
||||
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
||||
}
|
|
@ -2,10 +2,11 @@ package io.heckel.ntfy.service
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.*
|
||||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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})")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val app = context.applicationContext as Application
|
||||
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
|
||||
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
|
||||
val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
val preferenceKey = context.getString(R.string.eos_preference_key_is_enabled)
|
||||
val action = if (sharedPreferences.getBoolean(preferenceKey, false)) {
|
||||
SubscriberService.Action.START
|
||||
} else {
|
||||
SubscriberService.Action.STOP
|
||||
}
|
||||
|
||||
val serviceState = SubscriberService.readServiceState(context)
|
||||
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) {
|
||||
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})")
|
||||
Intent(context, SubscriberService::class.java).also {
|
||||
it.action = action.name
|
||||
ContextCompat.startForegroundService(context, it)
|
||||
context.startService(it)
|
||||
}
|
||||
}
|
||||
return Result.success()
|
|
@ -114,7 +114,10 @@ class AddFragment : DialogFragment() {
|
|||
// Set foreground description text
|
||||
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
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -288,6 +291,16 @@ class AddFragment : DialogFragment() {
|
|||
private fun validateInputSubscribeView() {
|
||||
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) {
|
||||
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
||||
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
||||
|
@ -295,9 +308,15 @@ class AddFragment : DialogFragment() {
|
|||
subscribeUseAnotherServerDescription.visibility = View.GONE
|
||||
subscribeBaseUrlLayout.visibility = View.GONE
|
||||
}
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||
subscribeForegroundDescription.visibility = View.GONE
|
||||
if (instantToggleAllowed) {
|
||||
subscribeInstantDeliveryBox.visibility = View.VISIBLE
|
||||
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
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -337,7 +356,7 @@ class AddFragment : DialogFragment() {
|
|||
activity.runOnUiThread {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
val instant = baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||
dialog?.dismiss()
|
||||
}
|
|
@ -30,6 +30,7 @@ import io.heckel.ntfy.app.Application
|
|||
import io.heckel.ntfy.db.Notification
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.Subscription
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
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 api = ApiService()
|
||||
private val messenger = FirebaseMessenger()
|
||||
private var notifier: NotificationService? = null // Context-dependent
|
||||
private var appBaseUrl: String? = null // Context-dependent
|
||||
|
||||
|
@ -125,6 +127,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
)
|
||||
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
|
||||
try {
|
||||
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 enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
|
||||
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
|
||||
enableInstantItem?.isVisible = false
|
||||
disableInstantItem?.isVisible = false
|
||||
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
|
||||
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) {
|
||||
repository.removeAllNotifications(subscriptionId)
|
||||
repository.removeSubscription(subscriptionId)
|
||||
if (subscriptionBaseUrl == appBaseUrl) {
|
||||
messenger.unsubscribe(subscriptionTopic)
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
|
@ -114,6 +114,7 @@ class DetailSettingsActivity : AppCompatActivity() {
|
|||
|
||||
private fun loadView() {
|
||||
if (subscription.upAppId == null) {
|
||||
loadInstantPref()
|
||||
loadMutedUntilPref()
|
||||
loadMinPriorityPref()
|
||||
loadAutoDeletePref()
|
||||
|
@ -133,6 +134,29 @@ class DetailSettingsActivity : AppCompatActivity() {
|
|||
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() {
|
||||
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
||||
val pref: SwitchPreference? = findPreference(prefId)
|
|
@ -34,6 +34,7 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.Subscription
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.msg.DownloadManager
|
||||
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 api = ApiService()
|
||||
private val messenger = FirebaseMessenger()
|
||||
|
||||
// UI elements
|
||||
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
|
||||
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
|
||||
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
||||
|
||||
|
@ -344,6 +349,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
||||
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
|
||||
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
||||
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
||||
|
@ -376,7 +385,26 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -438,6 +466,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
)
|
||||
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
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
|
@ -105,7 +105,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
|||
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
||||
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) 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) {
|
||||
newItemsView.visibility = View.GONE
|
||||
} else {
|
71
app/src/main/java/io/heckel/ntfy/ui/MainSettingsActivity.kt
Normal file
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
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,31 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50">
|
||||
<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:strokeWidth="0.754022"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:strokeColor="#00000000"/>
|
||||
<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:strokeWidth="0.525121"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:strokeColor="#00000000"/>
|
||||
<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:strokeWidth="0.525121"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
|
||||
android:strokeWidth="0.525121"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:strokeColor="#00000000"/>
|
||||
<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:strokeWidth="0.525121"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue