request schedule_exact_alarm permissions
This commit is contained in:
parent
6f907a28cd
commit
5ecd84855f
9 changed files with 198 additions and 3 deletions
|
|
@ -110,6 +110,7 @@
|
|||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
|
|
|||
|
|
@ -342,6 +342,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
|||
.apply()
|
||||
}
|
||||
|
||||
fun getWebSocketReconnectRemindTime(): Long {
|
||||
return sharedPrefs.getLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS)
|
||||
}
|
||||
|
||||
fun setWebSocketReconnectRemindTime(timeMillis: Long) {
|
||||
sharedPrefs.edit()
|
||||
.putLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, timeMillis)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getDefaultBaseUrl(): String? {
|
||||
return sharedPrefs.getString(SHARED_PREFS_DEFAULT_BASE_URL, null) ?:
|
||||
sharedPrefs.getString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, null) // Fall back to UP URL, removed when default is set!
|
||||
|
|
@ -492,6 +502,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
|||
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
|
||||
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
|
||||
const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
|
||||
const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime"
|
||||
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
|
||||
const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
|
||||
const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
|
||||
|
|
@ -532,6 +543,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
|||
const val WEBSOCKET_REMIND_TIME_ALWAYS = 1L
|
||||
const val WEBSOCKET_REMIND_TIME_NEVER = Long.MAX_VALUE
|
||||
|
||||
const val WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS = 1L
|
||||
const val WEBSOCKET_RECONNECT_REMIND_TIME_NEVER = Long.MAX_VALUE
|
||||
|
||||
private const val TAG = "NtfyRepository"
|
||||
private var instance: Repository? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,27 @@ class WsConnection(
|
|||
Log.d(TAG,"$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via alarm manager)")
|
||||
val reconnectTime = Calendar.getInstance()
|
||||
reconnectTime.add(Calendar.SECOND, seconds)
|
||||
alarmManager.setExact(AlarmManager.RTC_WAKEUP, reconnectTime.timeInMillis, RECONNECT_TAG, { start() }, null)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
reconnectTime.timeInMillis,
|
||||
RECONNECT_TAG,
|
||||
{ start() },
|
||||
null
|
||||
)
|
||||
} else {
|
||||
Log.d(TAG, "SCHEDULE_EXACT_ALARM permission denied: Failed to reschedule websocket connection")
|
||||
}
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
reconnectTime.timeInMillis,
|
||||
RECONNECT_TAG,
|
||||
{ start() },
|
||||
null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via handler)")
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package io.heckel.ntfy.ui
|
|||
import android.Manifest
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.app.AlarmManager
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
|
|
@ -11,6 +12,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
|
|
@ -125,9 +127,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
Log.addScrubTerm(s.topic)
|
||||
}
|
||||
|
||||
// Update banner + WebSocket banner
|
||||
// Update battery banner + WebSocket banner + websocket reconnect banner
|
||||
showHideBatteryBanner(subscriptions)
|
||||
showHideWebSocketBanner(subscriptions)
|
||||
showHideWebSocketReconnectBanner(subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +197,38 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS)
|
||||
SubscriberServiceManager(this).restart()
|
||||
wsBanner.visibility = View.GONE
|
||||
|
||||
// maybe show WebSocketReconnectBanner
|
||||
viewModel.list().observe(this) {
|
||||
it?.let { subscriptions ->
|
||||
showHideWebSocketReconnectBanner(subscriptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket Reconnect banner
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val wsReconnectBanner = findViewById<View>(R.id.main_banner_websocket_reconnect)
|
||||
val wsReconnectText = findViewById<TextView>(R.id.main_banner_websocket_reconnect_text)
|
||||
val wsReconnectDismissButton =
|
||||
findViewById<Button>(R.id.main_banner_websocket_reconnect_dontaskagain)
|
||||
val wsReconnectRemindButton =
|
||||
findViewById<Button>(R.id.main_banner_websocket_reconnect_remind_later)
|
||||
val wsReconnectEnableButton =
|
||||
findViewById<Button>(R.id.main_banner_websocket_reconnect_enable)
|
||||
wsReconnectText.movementMethod =
|
||||
LinkMovementMethod.getInstance() // Make links clickable
|
||||
wsReconnectDismissButton.setOnClickListener {
|
||||
wsReconnectBanner.visibility = View.GONE
|
||||
repository.setWebSocketReconnectRemindTime(Repository.WEBSOCKET_RECONNECT_REMIND_TIME_NEVER)
|
||||
}
|
||||
wsReconnectRemindButton.setOnClickListener {
|
||||
wsReconnectBanner.visibility = View.GONE
|
||||
repository.setWebSocketReconnectRemindTime(System.currentTimeMillis() + ONE_DAY_MILLIS)
|
||||
}
|
||||
wsReconnectEnableButton.setOnClickListener {
|
||||
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
|
||||
}
|
||||
}
|
||||
|
||||
// Create notification channels right away, so we can configure them immediately after installing the app
|
||||
|
|
@ -248,6 +283,24 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
wsBanner.visibility = if (showBanner) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showHideWebSocketReconnectBanner(subscriptions: List<Subscription>) {
|
||||
val wsReconnectBanner = findViewById<View>(R.id.main_banner_websocket_reconnect)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val hasSelfHostedSubscriptions = subscriptions.count { it.baseUrl != appBaseUrl } > 0
|
||||
val usingWebSockets =
|
||||
repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS
|
||||
val wsReconnectRemindTimeReached =
|
||||
repository.getWebSocketReconnectRemindTime() < System.currentTimeMillis()
|
||||
val canScheduleExactAlarms =
|
||||
(getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()
|
||||
val showBanner = hasSelfHostedSubscriptions && wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms
|
||||
Log.d(TAG, "hasSelfHostedSubscriptions: ${hasSelfHostedSubscriptions}, wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}")
|
||||
wsReconnectBanner.visibility = if (showBanner) View.VISIBLE else View.GONE
|
||||
} else {
|
||||
wsReconnectBanner.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun schedulePeriodicPollWorker() {
|
||||
val workerVersion = repository.getPollWorkerVersion()
|
||||
val workPolicy = if (workerVersion == PollWorker.VERSION) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.app.AlertDialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
|
|
@ -10,6 +11,7 @@ import android.content.pm.PackageManager
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
import android.text.TextUtils
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
|
|
@ -119,6 +121,11 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
return true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
settingsFragment.updateExactAlarmsPref()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private lateinit var repository: Repository
|
||||
private lateinit var serviceManager: SubscriberServiceManager
|
||||
|
|
@ -545,6 +552,9 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
}
|
||||
}
|
||||
|
||||
// CanScheduleExactAlarms
|
||||
updateExactAlarmsPref()
|
||||
|
||||
// Version
|
||||
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
|
||||
val versionPref: Preference? = findPreference(versionPrefId)
|
||||
|
|
@ -678,6 +688,23 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
}
|
||||
}
|
||||
|
||||
fun updateExactAlarmsPref() {
|
||||
val exactAlarmsPrefId = context?.getString(R.string.settings_advanced_exact_alarms_key) ?: return
|
||||
val exactAlarmsPref: Preference? = findPreference(exactAlarmsPrefId)
|
||||
val canScheduleExactAlarms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
(activity?.getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
exactAlarmsPref?.summary = if (canScheduleExactAlarms) getString(R.string.settings_advanced_exact_alarms_true) else getString(R.string.settings_advanced_exact_alarms_false)
|
||||
exactAlarmsPref?.onPreferenceClickListener = OnPreferenceClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !canScheduleExactAlarms) {
|
||||
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
data class NopasteResponse(val url: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,73 @@
|
|||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:shapeAppearance="?shapeAppearanceLargeComponent" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/main_banner_websocket"
|
||||
android:id="@+id/main_banner_websocket_reconnect" android:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/main_banner_websocket_reconnect_constraint" android:elevation="5dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp" app:srcCompat="@drawable/ic_announcement_orange_24dp"
|
||||
android:id="@+id/main_banner_websocket_reconnect_image"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_banner_websocket_reconnect_text"
|
||||
app:layout_constraintEnd_toStartOf="@id/main_banner_websocket_reconnect_text"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/main_banner_websocket_reconnect_text"
|
||||
android:layout_marginStart="15dp"/>
|
||||
<TextView
|
||||
android:id="@+id/main_banner_websocket_reconnect_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_banner_websocket_reconnect_text"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginEnd="15dp" android:layout_marginTop="15dp"
|
||||
app:layout_constraintStart_toEndOf="@+id/main_banner_websocket_reconnect_image"
|
||||
android:layout_marginStart="10dp"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:constraint_referenced_ids="main_banner_websocket_reconnect_remind_later,main_banner_websocket_reconnect_dontaskagain,main_banner_websocket_reconnect_enable" app:layout_constraintTop_toBottomOf="@id/main_banner_websocket_reconnect_text" app:flow_horizontalAlign="end" app:flow_wrapMode="chain" app:flow_horizontalStyle="packed" android:layout_marginEnd="15dp" android:id="@+id/flow_reconnect" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="15dp" app:flow_horizontalBias="1"
|
||||
app:flow_verticalGap="0dp" app:flow_horizontalGap="0dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/main_banner_websocket_reconnect_remind_later"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_banner_websocket_reconnect_button_remind_later"
|
||||
tools:layout_editor_absoluteX="86dp" tools:layout_editor_absoluteY="83dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/main_banner_websocket_reconnect_dontaskagain"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_banner_websocket_reconnect_button_dismiss"
|
||||
tools:layout_editor_absoluteX="260dp" tools:layout_editor_absoluteY="83dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/main_banner_websocket_reconnect_enable"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_banner_websocket_reconnect_button_enable_now"
|
||||
tools:layout_editor_absoluteX="253dp" tools:layout_editor_absoluteY="131dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/main_subscriptions_list_container"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -155,7 +222,7 @@
|
|||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_banner_websocket">
|
||||
app:layout_constraintTop_toBottomOf="@id/main_banner_websocket_reconnect">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/main_subscriptions_list"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@
|
|||
<string name="main_banner_websocket_button_dismiss">Dismiss</string>
|
||||
<string name="main_banner_websocket_button_enable_now">Enable now</string>
|
||||
|
||||
<!-- Main activity: WebSocket Reconnect banner -->
|
||||
<string name="main_banner_websocket_reconnect_text">To ensure WebSockets reconnect in the background, grant the "Alarm & Reminders" permission to ntfy</string>
|
||||
<string name="main_banner_websocket_reconnect_button_remind_later">Ask later</string>
|
||||
<string name="main_banner_websocket_reconnect_button_dismiss">Dismiss</string>
|
||||
<string name="main_banner_websocket_reconnect_button_enable_now">Grant now</string>
|
||||
|
||||
<!-- Add dialog -->
|
||||
<string name="add_dialog_title">Subscribe to topic</string>
|
||||
<string name="add_dialog_description_below">
|
||||
|
|
@ -347,6 +353,9 @@
|
|||
<string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets to connect to the server. This is the recommended method, but may require additional config in your proxy.</string>
|
||||
<string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON stream over HTTP</string>
|
||||
<string name="settings_advanced_connection_protocol_entry_ws">WebSockets</string>
|
||||
<string name="settings_advanced_exact_alarms_title">Exact Alarms</string>
|
||||
<string name="settings_advanced_exact_alarms_true">ntfy can schedule exact alarms (used to reconnect WebSockets in the background)</string>
|
||||
<string name="settings_advanced_exact_alarms_false">ntfy CANNOT schedule exact alarms. Click to grant the permission now</string>
|
||||
<string name="settings_about_header">About</string>
|
||||
<string name="settings_about_version_title">Version</string>
|
||||
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="settings_advanced_export_logs_key" translatable="false">ExportLogs</string>
|
||||
<string name="settings_advanced_clear_logs_key" translatable="false">ClearLogs</string>
|
||||
<string name="settings_advanced_connection_protocol_key" translatable="false">ConnectionProtocol</string>
|
||||
<string name="settings_advanced_exact_alarms_key" translatable="false">ExactAlarms</string>
|
||||
<string name="settings_about_version_key" translatable="false">Version</string>
|
||||
|
||||
<!-- Detail settings constants -->
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@
|
|||
app:entries="@array/settings_advanced_connection_protocol_entries"
|
||||
app:entryValues="@array/settings_advanced_connection_protocol_values"
|
||||
app:defaultValue="jsonhttp"/>
|
||||
<Preference
|
||||
app:key="@string/settings_advanced_exact_alarms_key"
|
||||
app:title="@string/settings_advanced_exact_alarms_title"/>
|
||||
<SwitchPreference
|
||||
app:key="@string/settings_advanced_broadcast_key"
|
||||
app:title="@string/settings_advanced_broadcast_title"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue