Preferences dialog

This commit is contained in:
Philipp Heckel 2021-12-31 01:34:25 +01:00
parent 72393ec0af
commit 2bc87013d5
13 changed files with 291 additions and 32 deletions

View file

@ -67,6 +67,7 @@ dependencies {
// WorkManager
implementation "androidx.work:work-runtime-ktx:2.6.0"
implementation 'androidx.preference:preference:1.1.1'
// Room (SQLite)
def roomVersion = "2.3.0"

View file

@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:name=".app.Application"
@ -23,6 +23,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<!-- Main activity -->
<activity
android:name=".ui.MainActivity"
@ -43,32 +44,51 @@
android:value=".ui.MainActivity"/>
</activity>
<!-- Settings activity -->
<activity
android:name=".ui.SettingsActivity"
android:parentActivityName=".ui.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity"/>
</activity>
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
<service android:name=".service.SubscriberService"/>
<!-- Subscriber service restart on reboot -->
<receiver android:name=".service.SubscriberService$BootStartReceiver" android:enabled="true">
<receiver
android:name=".service.SubscriberService$BootStartReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<!-- Subscriber service restart on destruction -->
<receiver android:name=".service.SubscriberService$AutoRestartReceiver" android:enabled="true"
android:exported="false"/>
<receiver
android:name=".service.SubscriberService$AutoRestartReceiver"
android:enabled="true"
android:exported="false"/>
<!-- Broadcast receiver to send messages via intents -->
<receiver android:name=".msg.BroadcastService$BroadcastReceiver" android:enabled="true" android:exported="true">
<receiver
android:name=".msg.BroadcastService$BroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="io.heckel.ntfy.SEND_MESSAGE"/>
</intent-filter>
</receiver>
<!-- Broadcast receiver for UnifiedPush; must match https://github.com/UnifiedPush/UP-spec/blob/main/specifications.md -->
<receiver android:name=".up.BroadcastReceiver" android:enabled="true" android:exported="true">
<!-- Broadcast receiver for UnifiedPush; must match https://github.com/UnifiedPush/UP-spec/blob/main/specifications.md -->
<receiver
android:name=".up.BroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
<action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
<action android:name="org.unifiedpush.android.distributor.REGISTER"/>
<action android:name="org.unifiedpush.android.distributor.UNREGISTER"/>
</intent-filter>
</receiver>
@ -80,7 +100,6 @@
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false"/>
@ -88,5 +107,4 @@
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification"/>
</application>
</manifest>

View file

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.*
import androidx.preference.PreferenceManager
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
@ -142,12 +143,32 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
.apply()
}
private suspend fun isMuted(subscriptionId: Long): Boolean {
if (isGlobalMuted()) {
return true
fun getUnifiedPushEnabled(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default!
}
fun setUnifiedPushEnabled(enabled: Boolean) {
sharedPrefs.edit()
.putBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, enabled)
.apply()
}
fun getUnifiedPushBaseUrl(): String? {
return sharedPrefs.getString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, null)
}
fun setUnifiedPushBaseUrl(baseUrl: String) {
if (baseUrl == "") {
sharedPrefs
.edit()
.remove(SHARED_PREFS_UNIFIED_PUSH_BASE_URL)
.apply()
} else {
sharedPrefs.edit()
.putString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, baseUrl)
.apply()
}
val s = getSubscription(subscriptionId) ?: return true
return s.mutedUntil == 1L || (s.mutedUntil > 1L && s.mutedUntil > System.currentTimeMillis()/1000)
}
fun isGlobalMuted(): Boolean {
@ -242,6 +263,8 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"
const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
private const val TAG = "NtfyRepository"
private var instance: Repository? = null

View file

@ -420,7 +420,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val formattedDate = formatDateShort(subscriptionMutedUntil)
notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate)
}
}
}

View file

@ -232,6 +232,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
onNotificationSettingsClick(enable = true)
true
}
R.id.main_menu_settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
R.id.main_menu_source -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url))))
true

View file

@ -0,0 +1,88 @@
package io.heckel.ntfy.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.*
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Repository
class SettingsActivity : AppCompatActivity() {
private val repository by lazy { (application as Application).repository }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
Log.d(MainActivity.TAG, "Create $this")
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_layout, SettingsFragment(repository))
.commit()
}
// Action bar
title = getString(R.string.settings_title)
// Show 'Back' button
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment(val repository: Repository) : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_preferences, rootKey)
// UnifiedPush Enabled
val upEnabledPrefId = context?.getString(R.string.pref_unified_push_enabled) ?: return
val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId)
upEnabled?.isChecked = repository.getUnifiedPushEnabled()
upEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setUnifiedPushEnabled(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getUnifiedPushEnabled()
}
}
upEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_unified_push_enabled_summary_on)
} else {
getString(R.string.settings_unified_push_enabled_summary_off)
}
}
// UnifiedPush Base URL
val appBaseUrl = context?.getString(R.string.app_base_url) ?: return
val upBaseUrlPrefId = context?.getString(R.string.pref_unified_push_base_url) ?: return
val upBaseUrl: EditTextPreference? = findPreference(upBaseUrlPrefId)
upBaseUrl?.text = repository.getUnifiedPushBaseUrl() ?: ""
upBaseUrl?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String, value: String?) {
val baseUrl = value ?: return
repository.setUnifiedPushBaseUrl(baseUrl)
}
override fun getString(key: String, defValue: String?): String? {
return repository.getUnifiedPushBaseUrl()
}
}
upBaseUrl?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { pref ->
if (TextUtils.isEmpty(pref.text)) {
getString(R.string.settings_unified_push_base_url_default_summary, appBaseUrl)
} else {
pref.text
}
}
// Version
val versionPrefId = context?.getString(R.string.pref_version) ?: return
val versionPref: Preference? = findPreference(versionPrefId)
versionPref?.summary = getString(R.string.settings_about_version_format, BuildConfig.VERSION_NAME, BuildConfig.FLAVOR)
}
}
}

View file

@ -3,9 +3,9 @@ package io.heckel.ntfy.up
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.preference.PreferenceManager
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.randomString
@ -34,8 +34,8 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
val repository = app.repository
val distributor = Distributor(app)
Log.d(TAG, "REGISTER received for app $appId (connectorToken=$connectorToken)")
if (appId.isBlank()) {
Log.w(TAG, "Refusing registration: empty application")
if (!repository.getUnifiedPushEnabled() || appId.isBlank()) {
Log.w(TAG, "Refusing registration: UnifiedPush disabled or empty application")
distributor.sendRegistrationRefused(appId, connectorToken)
return
}
@ -54,8 +54,8 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
}
// Add subscription
val baseUrl = context.getString(R.string.app_base_url) // FIXME
val topic = UP_PREFIX + randomString(TOPIC_LENGTH)
val baseUrl = repository.getUnifiedPushBaseUrl() ?: context.getString(R.string.app_base_url)
val topic = UP_PREFIX + randomString(TOPIC_RANDOM_ID_LENGTH)
val endpoint = topicUrlUp(baseUrl, topic)
val subscription = Subscription(
id = Random.nextLong(),
@ -105,6 +105,6 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
companion object {
private const val TAG = "NtfyUpBroadcastRecv"
private const val UP_PREFIX = "up"
private const val TOPIC_LENGTH = 16
private const val TOPIC_RANDOM_ID_LENGTH = 12
}
}

View file

@ -0,0 +1,9 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View file

@ -24,12 +24,13 @@
android:textColor="@color/primaryTextColor" android:layout_marginTop="10dp"
app:layout_constraintEnd_toStartOf="@+id/main_item_instant_image"/>
<TextView
android:text="89 notifications"
android:layout_width="wrap_content"
android:text="89 notifications, reconnecting ... This may wrap in the case of UnifiedPush"
android:layout_width="0dp"
android:layout_height="wrap_content" android:id="@+id/main_item_status"
app:layout_constraintStart_toStartOf="@+id/main_item_text"
app:layout_constraintTop_toBottomOf="@+id/main_item_text" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="10dp"/>
android:layout_marginBottom="10dp" app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/main_item_new" android:layout_marginEnd="10dp"/>
<ImageView
android:layout_width="20dp"
android:layout_height="24dp" app:srcCompat="@drawable/ic_notifications_off_time_gray_outline_24dp"

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This is a slightly edited copy of the original Android project layout
to make wrapping the summary line work.
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?android:attr/selectableItemBackground"
android:baselineAligned="false"
android:layout_marginTop="16dp"
android:gravity="center_vertical">
<include layout="@layout/image_frame"/>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:textAlignment="viewStart"
style="@style/PreferenceCategoryTitleTextStyle"/>
<!-- EDITED singleLine -->
<TextView
android:id="@android:id/summary"
android:ellipsize="end"
android:singleLine="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignLeft="@android:id/title"
android:layout_alignStart="@android:id/title"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="10"
style="@style/PreferenceSummaryTextStyle"/>
</RelativeLayout>
</LinearLayout>

View file

@ -5,6 +5,7 @@
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_time_white_outline_24dp"/>
<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"/>
<item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/>
<item android:id="@+id/main_menu_source" android:title="@string/main_menu_source_title"/>
<item android:id="@+id/main_menu_website" android:title="@string/main_menu_website_title"/>
</menu>

View file

@ -16,7 +16,8 @@
<string name="channel_subscriber_notification_text">You are subscribed to instant delivery topics</string>
<string name="channel_subscriber_notification_text_one">You are subscribed to one instant delivery topic</string>
<string name="channel_subscriber_notification_text_two">You are subscribed to two instant delivery topics</string>
<string name="channel_subscriber_notification_text_three">You are subscribed to three instant delivery topics</string>
<string name="channel_subscriber_notification_text_three">You are subscribed to three instant delivery topics
</string>
<string name="channel_subscriber_notification_text_four">You are subscribed to four instant delivery topics</string>
<string name="channel_subscriber_notification_text_more">You are subscribed to %1$d instant delivery topics</string>
@ -30,6 +31,7 @@
<string name="main_menu_notifications_enabled">Notifications enabled</string>
<string name="main_menu_notifications_disabled_forever">Notifications disabled</string>
<string name="main_menu_notifications_disabled_until">Notifications disabled until %1$s</string>
<string name="main_menu_settings_title">Settings</string>
<string name="main_menu_source_title">Report a bug</string>
<string name="main_menu_source_url">https://heckel.io/ntfy-android</string>
<string name="main_menu_website_title">Visit ntfy.sh</string>
@ -55,7 +57,8 @@
Click the button below to create or subscribe to a topic. After that, you can send
messages via PUT or POST and you\'ll receive notifications on your phone.
</string>
<string name="main_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.</string>
<string name="main_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.
</string>
<string name="main_unified_push_toast">This subscription is managed by %1$s via UnifiedPush</string>
<!-- Add dialog -->
@ -81,10 +84,13 @@
<!-- Detail activity -->
<string name="detail_deep_link_subscribed_toast_message">Subscribed to topic %1$s</string>
<string name="detail_no_notifications_text">You haven\'t received any notifications for this topic yet.</string>
<string name="detail_how_to_intro">To send notifications to this topic, simply PUT or POST to the topic URL.</string>
<string name="detail_how_to_intro">To send notifications to this topic, simply PUT or POST to the topic URL.
</string>
<string name="detail_how_to_example"><![CDATA[ Example (using curl):<br/><tt>$ curl -d \"Hi\" %1$s</tt> ]]></string>
<string name="detail_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.</string>
<string name="detail_clear_dialog_message">Do you really want to delete all of the notifications in this topic?</string>
<string name="detail_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.
</string>
<string name="detail_clear_dialog_message">Do you really want to delete all of the notifications in this topic?
</string>
<string name="detail_clear_dialog_permanently_delete">Permanently delete</string>
<string name="detail_clear_dialog_cancel">Cancel</string>
<string name="detail_delete_dialog_message">
@ -94,7 +100,9 @@
<string name="detail_delete_dialog_permanently_delete">Permanently delete</string>
<string name="detail_delete_dialog_cancel">Cancel</string>
<string name="detail_test_title">Test: You can set a title if you like</string>
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It has a priority of %1$d. If you send another one, it may look different.</string>
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It has a priority of %1$d.
If you send another one, it may look different.
</string>
<string name="detail_test_message_error">Could not send test message: %1$s</string>
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string>
@ -136,4 +144,22 @@
<string name="notification_dialog_8h">8 hours</string>
<string name="notification_dialog_tomorrow">Until tomorrow</string>
<string name="notification_dialog_forever">Forever</string>
<!-- Settings -->
<string name="settings_title">Settings</string>
<string name="settings_unified_push_header">UnifiedPush</string>
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
<string name="settings_unified_push_enabled_title">Enabled</string>
<string name="settings_unified_push_enabled_summary_on">Apps can use ntfy as distributor</string>
<string name="settings_unified_push_enabled_summary_off">Apps cannot use ntfy as distributor</string>
<string name="settings_unified_push_base_url_title">Server URL</string>
<string name="settings_unified_push_base_url_default_summary">%1$s (default)</string>
<string name="settings_about_header">About</string>
<string name="settings_about_version">Version</string>
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
<!-- Preferences IDs -->
<string name="pref_unified_push_enabled">UnifiedPushEnabled</string>
<string name="pref_unified_push_base_url">UnifiedPushBaseURL</string>
<string name="pref_version">Version</string>
</resources>

View file

@ -0,0 +1,21 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
app:title="@string/settings_title">
<PreferenceCategory
app:title="@string/settings_unified_push_header"
app:summary="@string/settings_unified_push_header_summary"
app:layout="@layout/preference_category_material_edited">
<SwitchPreference
app:key="@string/pref_unified_push_enabled"
app:title="@string/settings_unified_push_enabled_title"
app:enabled="true"/>
<EditTextPreference
app:key="@string/pref_unified_push_base_url"
app:title="@string/settings_unified_push_base_url_title"
app:dependency="@string/pref_unified_push_enabled"/>
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_about_header">
<Preference
app:key="@string/pref_version"
app:title="@string/settings_about_version"/>
</PreferenceCategory>
</PreferenceScreen>