Optimize background Workers, remove redundant JobServices

1. Adapt the location callback in WeatherWorker to coroutines
2. Merge EventListenerJob into UpdateCalendarWorker
3. Remove BatteryListenerJob, as it's impossible to use Jobs to detect battery status
This commit is contained in:
azuo 2021-10-07 16:03:07 +08:00
parent 388653f62b
commit d8e204c5d9
13 changed files with 123 additions and 168 deletions

View File

@ -108,10 +108,6 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".services.EventListenerJob" android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".services.BatteryListenerJob" android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".receivers.NotificationListener" android:exported="true" <service android:name=".receivers.NotificationListener" android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter> <intent-filter>

View File

@ -4,7 +4,6 @@ import android.Manifest
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.provider.CalendarContract import android.provider.CalendarContract
import com.tommasoberlose.anotherwidget.services.EventListenerJob
import com.tommasoberlose.anotherwidget.models.Event import com.tommasoberlose.anotherwidget.models.Event
import com.tommasoberlose.anotherwidget.global.Preferences import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.services.UpdateCalendarWorker import com.tommasoberlose.anotherwidget.services.UpdateCalendarWorker
@ -50,11 +49,11 @@ object CalendarHelper {
} }
fun setEventUpdatesAndroidN(context: Context) { fun setEventUpdatesAndroidN(context: Context) {
EventListenerJob.schedule(context) UpdateCalendarWorker.enqueueTrigger(context)
} }
fun removeEventUpdatesAndroidN(context: Context) { fun removeEventUpdatesAndroidN(context: Context) {
EventListenerJob.remove(context) UpdateCalendarWorker.cancelTrigger(context)
} }
fun List<Event>.applyFilters() : List<Event> { fun List<Event>.applyFilters() : List<Event> {

View File

@ -324,7 +324,9 @@ object WeatherHelper {
} }
} }
fun getWeatherGovIcon(iconString: String, isDaytime: Boolean): String = when (iconString.substringBefore('?').substringAfterLast('/')) { fun getWeatherGovIcon(iconString: String, isDaytime: Boolean): String = when (
iconString.substringBefore('?').substringAfterLast('/').substringBefore(',')
) {
"skc" -> "01" "skc" -> "01"
"few" -> "02" "few" -> "02"
"sct" -> "02" "sct" -> "02"

View File

@ -42,12 +42,12 @@ class WeatherReceiver : BroadcastReceiver() {
5 -> 60L * 24 5 -> 60L * 24
else -> 60 else -> 60
} }
WeatherWorker.enqueue(context, interval, TimeUnit.MINUTES) WeatherWorker.enqueuePeriodic(context, interval, TimeUnit.MINUTES)
} }
} }
fun removeUpdates(context: Context) { fun removeUpdates(context: Context) {
WeatherWorker.cancel(context) WeatherWorker.cancelPeriodic(context)
} }
} }
} }

View File

@ -1,61 +0,0 @@
package com.tommasoberlose.anotherwidget.services
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.provider.CalendarContract
import com.tommasoberlose.anotherwidget.helpers.CalendarHelper
import com.tommasoberlose.anotherwidget.ui.widgets.MainWidget
class BatteryListenerJob : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
MainWidget.updateWidget(this)
schedule(
this
)
return false
}
@Synchronized
override fun onStopJob(params: JobParameters): Boolean {
return false
}
companion object {
private const val chargingJobId = 1006
private const val notChargingJobId = 1007
fun schedule(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
remove(context)
val componentName = ComponentName(
context,
EventListenerJob::class.java
)
with(context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler) {
schedule(
JobInfo.Builder(chargingJobId, componentName)
.setRequiresCharging(true)
.setPersisted(true)
.build()
)
schedule(
JobInfo.Builder(notChargingJobId, componentName)
.setRequiresCharging(false)
.setPersisted(true)
.build()
)
}
}
}
private fun remove(context: Context) {
val js = context.getSystemService(JobScheduler::class.java)
js?.cancel(chargingJobId)
js?.cancel(notChargingJobId)
}
}
}

View File

@ -1,55 +0,0 @@
package com.tommasoberlose.anotherwidget.services
import android.app.job.JobInfo
import android.app.job.JobInfo.TriggerContentUri
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.provider.CalendarContract
import com.tommasoberlose.anotherwidget.helpers.CalendarHelper
class EventListenerJob : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
CalendarHelper.updateEventList(this)
schedule(
this
)
return false
}
@Synchronized
override fun onStopJob(params: JobParameters): Boolean {
return false
}
companion object {
private const val jobId = 1005
fun schedule(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val componentName = ComponentName(
context,
EventListenerJob::class.java
)
with(context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler) {
schedule(
JobInfo.Builder(jobId, componentName)
.addTriggerContentUri(TriggerContentUri(
CalendarContract.CONTENT_URI,
TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
))
.build()
)
}
}
}
fun remove(context: Context) {
val js = context.getSystemService(JobScheduler::class.java)
js?.cancel(jobId)
}
}
}

View File

@ -2,6 +2,9 @@ package com.tommasoberlose.anotherwidget.services
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.os.Build
import android.provider.CalendarContract
import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
@ -17,12 +20,12 @@ import com.tommasoberlose.anotherwidget.receivers.UpdatesReceiver
import com.tommasoberlose.anotherwidget.ui.fragments.MainFragment import com.tommasoberlose.anotherwidget.ui.fragments.MainFragment
import com.tommasoberlose.anotherwidget.ui.widgets.MainWidget import com.tommasoberlose.anotherwidget.ui.widgets.MainWidget
import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.everything.providers.android.calendar.CalendarProvider import me.everything.providers.android.calendar.CalendarProvider
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UpdateCalendarWorker(private val context: Context, params: WorkerParameters) : class UpdateCalendarWorker(private val context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) { CoroutineWorker(context, params) {
@ -36,7 +39,6 @@ class UpdateCalendarWorker(private val context: Context, params: WorkerParameter
if (!context.checkGrantedPermission(Manifest.permission.READ_CALENDAR)) { if (!context.checkGrantedPermission(Manifest.permission.READ_CALENDAR)) {
eventRepository.resetNextEventData() eventRepository.resetNextEventData()
eventRepository.clearEvents() eventRepository.clearEvents()
Preferences.showEvents = false
} else { } else {
// fetch all events from now to next ACTION_CALENDAR_UPDATE + limit // fetch all events from now to next ACTION_CALENDAR_UPDATE + limit
val now = Calendar.getInstance() val now = Calendar.getInstance()
@ -139,6 +141,7 @@ class UpdateCalendarWorker(private val context: Context, params: WorkerParameter
} catch (ignored: java.lang.Exception) { } catch (ignored: java.lang.Exception) {
} }
} }
enqueueTrigger(context)
} else { } else {
eventRepository.resetNextEventData() eventRepository.resetNextEventData()
eventRepository.clearEvents() eventRepository.clearEvents()
@ -156,11 +159,34 @@ class UpdateCalendarWorker(private val context: Context, params: WorkerParameter
companion object { companion object {
fun enqueue(context: Context) { fun enqueue(context: Context) {
WorkManager.getInstance(context.applicationContext).enqueueUniqueWork( WorkManager.getInstance(context).enqueueUniqueWork(
"UpdateCalendarWorker", "updateEventList",
ExistingWorkPolicy.REPLACE, ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<UpdateCalendarWorker>().build() OneTimeWorkRequestBuilder<UpdateCalendarWorker>().build()
) )
} }
fun enqueueTrigger(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
WorkManager.getInstance(context).enqueueUniqueWork(
"updateEventListByTrigger",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<UpdateCalendarWorker>().setConstraints(
Constraints.Builder().addContentUriTrigger(
CalendarContract.CONTENT_URI,
true
).build()
).build()
)
}
}
fun cancelTrigger(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
WorkManager.getInstance(context).cancelUniqueWork(
"updateEventListByTrigger"
)
}
}
} }
} }

View File

@ -14,19 +14,18 @@ import androidx.work.WorkerParameters
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.google.android.gms.tasks.Tasks
import com.tommasoberlose.anotherwidget.R import com.tommasoberlose.anotherwidget.R
import com.tommasoberlose.anotherwidget.global.Preferences import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.helpers.WeatherHelper import com.tommasoberlose.anotherwidget.helpers.WeatherHelper
import com.tommasoberlose.anotherwidget.network.WeatherNetworkApi import com.tommasoberlose.anotherwidget.network.WeatherNetworkApi
import com.tommasoberlose.anotherwidget.ui.fragments.MainFragment import com.tommasoberlose.anotherwidget.ui.fragments.MainFragment
import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
class WeatherWorker(private val context: Context, params: WorkerParameters) : class WeatherWorker(private val context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) { CoroutineWorker(context, params) {
@ -42,7 +41,11 @@ class WeatherWorker(private val context: Context, params: WorkerParameters) :
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
== ConnectionResult.SUCCESS == ConnectionResult.SUCCESS
) { ) {
LocationServices.getFusedLocationProviderClient(context).lastLocation suspendCancellableCoroutine { continuation ->
LocationServices.getFusedLocationProviderClient(context).lastLocation.addOnCompleteListener {
continuation.resume(if (it.isSuccessful) it.result else null)
}
}
} else { } else {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
var location: Location? = null var location: Location? = null
@ -54,21 +57,15 @@ class WeatherWorker(private val context: Context, params: WorkerParameters) :
location = it location = it
} }
} }
Tasks.forResult(location) location
}.addOnCompleteListener { task -> }.let { location ->
val networkApi = WeatherNetworkApi(context) if (location != null) {
if (task.isSuccessful) { Preferences.customLocationLat = location.latitude.toString()
val location = task.result Preferences.customLocationLon = location.longitude.toString()
if (location != null) {
Preferences.customLocationLat = location.latitude.toString()
Preferences.customLocationLon = location.longitude.toString()
}
} }
withContext(Dispatchers.IO) {
CoroutineScope(Dispatchers.IO).launch { WeatherNetworkApi(context).updateWeather()
networkApi.updateWeather()
} }
EventBus.getDefault().post(MainFragment.UpdateUiMessageEvent())
} }
} }
else -> { else -> {
@ -83,24 +80,24 @@ class WeatherWorker(private val context: Context, params: WorkerParameters) :
companion object { companion object {
fun enqueue(context: Context) { fun enqueue(context: Context) {
WorkManager.getInstance(context.applicationContext).enqueueUniqueWork( WorkManager.getInstance(context).enqueueUniqueWork(
"OneTimeWeatherWorker", "updateWeather",
ExistingWorkPolicy.REPLACE, ExistingWorkPolicy.REPLACE,
OneTimeWorkRequestBuilder<WeatherWorker>().build() OneTimeWorkRequestBuilder<WeatherWorker>().build()
) )
} }
fun enqueue(context: Context, interval: Long, unit: TimeUnit) { fun enqueuePeriodic(context: Context, interval: Long, unit: TimeUnit) {
WorkManager.getInstance(context.applicationContext).enqueueUniquePeriodicWork( WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"WeatherWorker", "updateWeatherPeriodically",
ExistingPeriodicWorkPolicy.REPLACE, ExistingPeriodicWorkPolicy.REPLACE,
PeriodicWorkRequestBuilder<WeatherWorker>(interval, unit).build() PeriodicWorkRequestBuilder<WeatherWorker>(interval, unit).build()
) )
} }
fun cancel(context: Context) { fun cancelPeriodic(context: Context) {
WorkManager.getInstance(context.applicationContext).cancelUniqueWork( WorkManager.getInstance(context).cancelUniqueWork(
"WeatherWorker" "updateWeatherPeriodically"
) )
} }
} }

View File

@ -141,7 +141,6 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh
if (Preferences.showEvents && !checkGrantedPermission(Manifest.permission.READ_CALENDAR)) { if (Preferences.showEvents && !checkGrantedPermission(Manifest.permission.READ_CALENDAR)) {
Preferences.showEvents = false Preferences.showEvents = false
com.tommasoberlose.anotherwidget.receivers.UpdatesReceiver.removeUpdates(this)
} }
} }

View File

@ -88,6 +88,7 @@ class PreferencesFragment : Fragment() {
CalendarHelper.setEventUpdatesAndroidN(requireContext()) CalendarHelper.setEventUpdatesAndroidN(requireContext())
} else { } else {
CalendarHelper.removeEventUpdatesAndroidN(requireContext()) CalendarHelper.removeEventUpdatesAndroidN(requireContext())
UpdatesReceiver.removeUpdates(requireContext())
} }
} }
} }
@ -124,7 +125,6 @@ class PreferencesFragment : Fragment() {
requireCalendarPermission() requireCalendarPermission()
} else { } else {
Preferences.showEvents = enabled Preferences.showEvents = enabled
UpdatesReceiver.removeUpdates(requireContext())
} }
} }
@ -165,6 +165,7 @@ class PreferencesFragment : Fragment() {
.withPermissions( .withPermissions(
Manifest.permission.READ_CALENDAR Manifest.permission.READ_CALENDAR
).withListener(object: MultiplePermissionsListener { ).withListener(object: MultiplePermissionsListener {
private var shouldShowRationale = false
override fun onPermissionsChecked(report: MultiplePermissionsReport?) { override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
report?.let { report?.let {
val granted = report.areAllPermissionsGranted() val granted = report.areAllPermissionsGranted()
@ -172,12 +173,33 @@ class PreferencesFragment : Fragment() {
if (granted) { if (granted) {
CalendarHelper.updateEventList(requireContext()) CalendarHelper.updateEventList(requireContext())
} }
else if (!shouldShowRationale && report.isAnyPermissionPermanentlyDenied) {
MaterialBottomSheetDialog(
requireContext(),
getString(R.string.title_permission_calendar),
getString(R.string.description_permission_calendar)
).setNegativeButton(getString(R.string.action_ignore))
.setPositiveButton(getString(R.string.action_grant_permission)) {
startActivity(
android.content.Intent(
android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
).apply {
data = android.net.Uri.fromParts(
"package",
requireContext().packageName,
null
)
}
)
}.show()
}
} }
} }
override fun onPermissionRationaleShouldBeShown( override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?, permissions: MutableList<PermissionRequest>?,
token: PermissionToken? token: PermissionToken?
) { ) {
shouldShowRationale = true
// Remember to invoke this method when the custom rationale is closed // Remember to invoke this method when the custom rationale is closed
// or just by default if you don't want to use any custom rationale. // or just by default if you don't want to use any custom rationale.
token?.continuePermissionRequest() token?.continuePermissionRequest()

View File

@ -221,18 +221,40 @@ class WeatherFragment : Fragment() {
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION Manifest.permission.ACCESS_COARSE_LOCATION
).withListener(object: MultiplePermissionsListener { ).withListener(object: MultiplePermissionsListener {
private var shouldShowRationale = false
override fun onPermissionsChecked(report: MultiplePermissionsReport?) { override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
report?.let { report?.let {
if (report.grantedPermissionResponses.isNotEmpty()) { if (report.grantedPermissionResponses.isNotEmpty()) {
checkLocationPermission() checkLocationPermission()
WeatherHelper.updateWeather(requireContext()) WeatherHelper.updateWeather(requireContext())
} }
else if (!shouldShowRationale && report.isAnyPermissionPermanentlyDenied) {
MaterialBottomSheetDialog(
requireContext(),
getString(R.string.title_permission_location),
getString(R.string.description_permission_location)
).setNegativeButton(getString(R.string.action_ignore))
.setPositiveButton(getString(R.string.action_grant_permission)) {
startActivity(
Intent(
android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
).apply {
data = android.net.Uri.fromParts(
"package",
requireContext().packageName,
null
)
}
)
}.show()
}
} }
} }
override fun onPermissionRationaleShouldBeShown( override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?, permissions: MutableList<PermissionRequest>?,
token: PermissionToken? token: PermissionToken?
) { ) {
shouldShowRationale = true
// Remember to invoke this method when the custom rationale is closed // Remember to invoke this method when the custom rationale is closed
// or just by default if you don't want to use any custom rationale. // or just by default if you don't want to use any custom rationale.
token?.continuePermissionRequest() token?.continuePermissionRequest()
@ -241,6 +263,12 @@ class WeatherFragment : Fragment() {
.check() .check()
} }
override fun onResume() {
super.onResume()
checkWeatherProviderConfig()
checkLocationPermission()
}
private fun maintainScrollPosition(callback: () -> Unit) { private fun maintainScrollPosition(callback: () -> Unit) {
binding.scrollView.isScrollable = false binding.scrollView.isScrollable = false
callback.invoke() callback.invoke()

View File

@ -155,6 +155,7 @@ class MainViewModel(context: Application) : AndroidViewModel(context) {
addSource(Preferences.asLiveData(Preferences::secondRowInformation)) { value = true } addSource(Preferences.asLiveData(Preferences::secondRowInformation)) { value = true }
addSource(Preferences.asLiveData(Preferences::showWeather)) { value = true } addSource(Preferences.asLiveData(Preferences::showWeather)) { value = true }
addSource(Preferences.asLiveData(Preferences::weatherProvider)) { value = true }
addSource(Preferences.asLiveData(Preferences::weatherTempUnit)) { value = true } addSource(Preferences.asLiveData(Preferences::weatherTempUnit)) { value = true }
addSource(Preferences.asLiveData(Preferences::weatherIconPack)) { value = true } addSource(Preferences.asLiveData(Preferences::weatherIconPack)) { value = true }
addSource(Preferences.asLiveData(Preferences::customLocationLat)) { value = true } addSource(Preferences.asLiveData(Preferences::customLocationLat)) { value = true }

View File

@ -34,25 +34,26 @@ class MainWidget : AppWidgetProvider() {
CalendarHelper.updateEventList(context) CalendarHelper.updateEventList(context)
WeatherReceiver.setUpdates(context) WeatherReceiver.setUpdates(context)
MediaPlayerHelper.updatePlayingMediaInfo(context) MediaPlayerHelper.updatePlayingMediaInfo(context)
if (Preferences.showEvents) {
CalendarHelper.setEventUpdatesAndroidN(context)
} else {
CalendarHelper.removeEventUpdatesAndroidN(context)
}
} }
override fun onDisabled(context: Context) { override fun onDisabled(context: Context) {
if (getWidgetCount(context) == 0) { if (getWidgetCount(context) == 0) {
CalendarHelper.removeEventUpdatesAndroidN(context)
UpdatesReceiver.removeUpdates(context) UpdatesReceiver.removeUpdates(context)
WeatherReceiver.removeUpdates(context) WeatherReceiver.removeUpdates(context)
} }
} }
companion object { companion object {
private val handler by lazy { android.os.Handler(android.os.Looper.getMainLooper()) }
fun updateWidget(context: Context) { fun updateWidget(context: Context) {
context.sendBroadcast(IntentHelper.getWidgetUpdateIntent(context)) handler.run {
removeCallbacksAndMessages(null)
postDelayed ({
context.sendBroadcast(IntentHelper.getWidgetUpdateIntent(context))
}, 100)
}
} }
fun getWidgetCount(context: Context): Int { fun getWidgetCount(context: Context): Int {