Battery optimization banner

This commit is contained in:
Philipp Heckel 2022-01-18 16:49:00 -05:00
parent 8fcef8ddee
commit 9c616d3b7d
7 changed files with 160 additions and 8 deletions

View file

@ -225,6 +225,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
.apply() .apply()
} }
fun getBatteryOptimizationsRemindTime(): Long {
return sharedPrefs.getLong(SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME, BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS)
}
fun setBatteryOptimizationsRemindTime(timeMillis: Long) {
sharedPrefs.edit()
.putLong(SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME, timeMillis)
.apply()
}
fun getUnifiedPushEnabled(): Boolean { fun getUnifiedPushEnabled(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default
} }
@ -350,6 +360,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol" const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs" const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled" const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
@ -360,6 +371,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp" const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp"
const val CONNECTION_PROTOCOL_WS = "ws" const val CONNECTION_PROTOCOL_WS = "ws"
const val BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS = 1L
const val BATTERY_OPTIMIZATIONS_REMIND_TIME_NEVER = Long.MAX_VALUE
private const val TAG = "NtfyRepository" private const val TAG = "NtfyRepository"
private var instance: Repository? = null private var instance: Repository? = null

View file

@ -5,11 +5,14 @@ import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.Button
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -20,6 +23,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.work.* import androidx.work.*
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.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.firebase.FirebaseMessenger import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.log.Log import io.heckel.ntfy.log.Log
@ -27,10 +31,7 @@ import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.service.SubscriberService import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.fadeStatusBarColor import io.heckel.ntfy.util.*
import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.shortUrl
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -120,6 +121,34 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
SubscriberServiceManager.refresh(this) SubscriberServiceManager.refresh(this)
} }
// Battery banner
val batteryRemindTimeReached = repository.getBatteryOptimizationsRemindTime() < System.currentTimeMillis()
val ignoringBatteryOptimizations = isIgnoringBatteryOptimizations(this)
val showBatteryBanner = batteryRemindTimeReached && !ignoringBatteryOptimizations
val batteryBanner = findViewById<View>(R.id.main_banner_battery)
batteryBanner.visibility = if (showBatteryBanner) View.VISIBLE else View.GONE
Log.d(TAG, "Battery: ignoring optimizations = $ignoringBatteryOptimizations (we want this to be true); remind time reached = $batteryRemindTimeReached")
if (showBatteryBanner) {
val dismissButton = findViewById<Button>(R.id.main_banner_battery_dismiss)
val askLaterButton = findViewById<Button>(R.id.main_banner_battery_ask_later)
val fixNowButton = findViewById<Button>(R.id.main_banner_battery_fix_now)
dismissButton.setOnClickListener {
batteryBanner.visibility = View.GONE
repository.setBatteryOptimizationsRemindTime(Repository.BATTERY_OPTIMIZATIONS_REMIND_TIME_NEVER)
}
askLaterButton.setOnClickListener {
batteryBanner.visibility = View.GONE
repository.setBatteryOptimizationsRemindTime(System.currentTimeMillis() + ONE_DAY_MILLIS)
}
fixNowButton.setOnClickListener {
batteryBanner.visibility = View.GONE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
startActivity(intent)
}
}
}
// 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()
@ -535,6 +564,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant" const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant"
const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil" const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil"
const val ANIMATION_DURATION = 80L const val ANIMATION_DURATION = 80L
const val ONE_DAY_MILLIS = 86400000L
// As per documentation: The minimum repeat interval that can be defined is 15 minutes // As per documentation: The minimum repeat interval that can be defined is 15 minutes
// (same as the JobScheduler API), but in practice 15 doesn't work. Using 16 here. // (same as the JobScheduler API), but in practice 15 doesn't work. Using 16 here.

View file

@ -3,9 +3,14 @@ package io.heckel.ntfy.util
import android.animation.ArgbEvaluator import android.animation.ArgbEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.provider.Settings
import android.view.Window import android.view.Window
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import java.security.SecureRandom import java.security.SecureRandom
@ -185,3 +190,13 @@ fun supportedImage(mimeType: String?): Boolean {
return listOf("image/jpeg", "image/png").contains(mimeType) return listOf("image/jpeg", "image/png").contains(mimeType)
} }
// Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val appName = context.applicationContext.packageName
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return powerManager.isIgnoringBatteryOptimizations(appName)
}
return true
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.67,4L14,4L14,2h-4v2L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM13,18h-2v-2h2v2zM13,14h-2L11,9h2v5z"
android:fillColor="#F44336"/>
</vector>

View file

@ -1,15 +1,91 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<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_toTopOf="parent"
android:id="@+id/main_banner_battery" android:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/main_banner_battery_constraint" android:elevation="5dp">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp" app:srcCompat="@drawable/ic_battery_alert_red_24dp"
android:id="@+id/main_banner_battery_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/main_banner_battery_text"
app:layout_constraintEnd_toStartOf="@id/main_banner_battery_text"
app:layout_constraintBottom_toBottomOf="@+id/main_banner_battery_text"
android:layout_marginStart="15dp"/>
<TextView
android:id="@+id/main_banner_battery_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/main_banner_battery_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_battery_image"
android:layout_marginStart="10dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/main_banner_battery_ask_later"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:text="@string/main_banner_battery_button_ask_later"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/main_banner_battery_fix_now"
app:layout_constraintTop_toBottomOf="@+id/main_banner_battery_text"
android:layout_marginBottom="5dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/main_banner_battery_dismiss"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:text="@string/main_banner_battery_button_dismiss"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/main_banner_battery_ask_later"
app:layout_constraintTop_toBottomOf="@+id/main_banner_battery_text"
android:layout_marginBottom="5dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/main_banner_battery_fix_now"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="15dp"
android:text="@string/main_banner_battery_button_fix_now"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_banner_battery_text"
android:layout_marginBottom="5dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/main_subscriptions_list_container" android:id="@+id/main_subscriptions_list_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:visibility="gone"> 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_battery">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/main_subscriptions_list" android:id="@+id/main_subscriptions_list"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -19,13 +95,14 @@
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
app:layoutManager="LinearLayoutManager"/> app:layoutManager="LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/fab" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@+id/fab" app:layout_constraintStart_toStartOf="parent"
android:id="@+id/main_no_subscriptions"> android:id="@+id/main_no_subscriptions" android:visibility="gone">
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_sms_gray_48dp" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_sms_gray_48dp"

View file

@ -67,6 +67,12 @@
</string> </string>
<string name="main_unified_push_toast">This subscription is managed by %1$s via UnifiedPush</string> <string name="main_unified_push_toast">This subscription is managed by %1$s via UnifiedPush</string>
<!-- Main activity: Battery banner -->
<string name="main_banner_battery_text">Battery optimization should be disabled to avoid issues with notification delivery.</string>
<string name="main_banner_battery_button_ask_later">Ask later</string>
<string name="main_banner_battery_button_dismiss">Dismiss</string>
<string name="main_banner_battery_button_fix_now">Fix now</string>
<!-- Add dialog --> <!-- Add dialog -->
<string name="add_dialog_title">Subscribe to topic</string> <string name="add_dialog_title">Subscribe to topic</string>
<string name="add_dialog_description_below"> <string name="add_dialog_description_below">

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.67 4H14V2h-4v2H8.33C7.6 4 7 4.6 7 5.33v15.33C7 21.4 7.6 22 8.33 22h7.33c.74 0 1.34-.6 1.34-1.33V5.33C17 4.6 16.4 4 15.67 4zM13 18h-2v-2h2v2zm0-4h-2V9h2v5z"/></svg>

After

Width:  |  Height:  |  Size: 317 B