Skip to content
This repository was archived by the owner on Dec 18, 2022. It is now read-only.

Commit ae7cbc0

Browse files
authored
Merge pull request #62 from 05nelsonm/mn/feature/background-manager-additional-options
Add option to BackgroundManager to run service in the Foreground
2 parents 6fede46 + 4b14116 commit ae7cbc0

File tree

7 files changed

+140
-68
lines changed

7 files changed

+140
-68
lines changed

sampleapp/src/main/java/io/matthewnelson/sampleapp/App.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,10 @@ class App: Application() {
121121
private fun generateBackgroundManagerPolicy(): BackgroundManager.Builder.Policy {
122122
// private fun generateBackgroundManagerPolicy(): BackgroundManager.Builder.Policy {
123123
return BackgroundManager.Builder()
124-
.respectResourcesWhileInBackground(secondsFrom5To45 = 20)
125124

125+
// All available options present. Only 1 is able to be chosen.
126+
//.respectResourcesWhileInBackground(secondsFrom5To45 = 20)
127+
.runServiceInForeground(killAppIfTaskIsRemoved = true)
126128
// }
127129
}
128130

topl-service/src/main/java/io/matthewnelson/topl_service/lifecycle/BackgroundManager.kt

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
package io.matthewnelson.topl_service.lifecycle
6868

6969
import android.content.Context
70+
import android.os.Process
7071
import androidx.lifecycle.Lifecycle
7172
import androidx.lifecycle.LifecycleObserver
7273
import androidx.lifecycle.OnLifecycleEvent
@@ -79,20 +80,22 @@ import io.matthewnelson.topl_service.util.ServiceConsts
7980

8081
/**
8182
* When your application is sent to the background (the Recent App's tray or lock screen), the
82-
* chosen [BackgroundManager.Builder.Policy] will be executed after the number of seconds you've
83-
* declared.
83+
* chosen [BackgroundManager.Builder.Policy] will be triggered.
84+
*
85+
* Additionally, there are 2 values for you to query if needed to give you context surrounding
86+
* your application's background state; [taskIsInForeground] and [taskIsRemovedFromRecentApps].
8487
*
8588
* If brought back into the foreground by the user:
8689
*
8790
* - **Before Policy execution**: Execution is canceled. If [BaseService.lastAcceptedServiceAction]
8891
* was **not** [ServiceConsts.ServiceActionName.STOP], a startService call is made to ensure it's
8992
* started.
9093
*
91-
* - **After Policy execution**: If [BaseService.lastAcceptedServiceAction]
92-
* was **not** [ServiceConsts.ServiceActionName.STOP], a startService call is made to ensure it's
93-
* started.
94+
* - **After Policy execution**: If [BaseService.lastAcceptedServiceAction] was **not**
95+
* [ServiceConsts.ServiceActionName.STOP], a startService call is made to ensure it's started.
9496
*
95-
* - See [BaseService.updateLastAcceptedServiceAction] and [TorService.onTaskRemoved] for
97+
* - See [io.matthewnelson.topl_service.service.components.binding.BaseServiceBinder],
98+
* [BaseService.updateLastAcceptedServiceAction], and [TorService.onTaskRemoved] for
9699
* more information.
97100
*
98101
* While your application is in the foreground the only way to stop the service is by
@@ -103,14 +106,16 @@ import io.matthewnelson.topl_service.util.ServiceConsts
103106
*
104107
* When the user sends your application to the Recent App's tray though, to recoup resources
105108
* the OS will kill your app after being idle for a period of time (random AF... typically
106-
* 0.75m to 1.25m). This is not an issue if the user removes the task before the OS
107-
* kills the app, as Tor will be able to shutdown properly and the service will stop.
109+
* 0.75m to 1.25m if the device's memory is being used heavily) if the service is not moved to
110+
* the Foreground to inhibit this. This is not an issue if the user removes the task before the
111+
* OS kills the app, as Tor will be able to shutdown properly and the service will stop.
108112
*
109113
* This is where Services get sketchy (especially when trying to implement an always
110114
* running service for networking), and is the purpose of the [BackgroundManager] class.
111115
*
112116
* This class starts your chosen [BackgroundManager.Builder.Policy] as soon as your
113-
* application is sent to the background, waits for the time you declared, and then executes.
117+
* application is sent to the background. It facilitates a more declarative, flexible
118+
* operation to fit Library users' needs.
114119
*
115120
* See the [BackgroundManager.Builder] for more detail.
116121
*
@@ -126,7 +131,8 @@ class BackgroundManager internal constructor(
126131
@BackgroundPolicy private val policy: String,
127132
private val executionDelay: Long,
128133
private val serviceClass: Class<*>,
129-
private val bindServiceFlag: Int
134+
private val bindServiceFlag: Int,
135+
private val killAppIfTaskIsRemoved: Boolean
130136
): ServiceConsts(), LifecycleObserver {
131137

132138

@@ -147,29 +153,7 @@ class BackgroundManager internal constructor(
147153
@BackgroundPolicy
148154
private lateinit var chosenPolicy: String
149155
private var executionDelay: Long = 30_000L
150-
151-
// TODO: Needs more work... running in the foreground is inhibiting the Application from
152-
// performing it's normal lifecycle after user swipes it away such that it's not going
153-
// through Application.onCreate, but is holding onto references. (same problem when
154-
// starting the service using Context.startForegroundService), which is bullshit.
155-
// /**
156-
// * While your application is in the background (the Recent App's tray or lock screen),
157-
// * this [Policy] periodically switches [TorService] to the foreground then immediately
158-
// * back the background. Doing do prevents your application from going idle and being
159-
// * killed by the OS. It is much more resource intensive than choosing
160-
// * [respectResourcesWhileInBackground].
161-
// *
162-
// * @param [secondsFrom20To40]? Seconds between the events of cycling from background to
163-
// * foreground to background. Sending null will use the default (30s)
164-
// * @return [BackgroundManager.Builder.Policy] To use when initializing
165-
// * [io.matthewnelson.topl_service.TorServiceController.Builder]
166-
// * */
167-
// fun keepAliveWhileInBackground(secondsFrom20To40: Int? = null): Policy {
168-
// chosenPolicy = BackgroundPolicy.KEEP_ALIVE
169-
// if (secondsFrom20To40 != null && secondsFrom20To40 in 20..40)
170-
// executionDelay = (secondsFrom20To40 * 1000).toLong()
171-
// return Policy(this)
172-
// }
156+
private var killAppIfTaskIsRemoved = false
173157

174158
/**
175159
* Stops [TorService] after being in the background for the declared [secondsFrom5To45].
@@ -200,6 +184,37 @@ class BackgroundManager internal constructor(
200184
return Policy(this)
201185
}
202186

187+
/**
188+
* Electing this option will, when your application is sent to the background, immediately
189+
* move [TorService] to the Foreground. If the user returns to your application,
190+
* [TorService] will then be backgrounded.
191+
*
192+
* Some things to note about your application's behaviour with this option:
193+
*
194+
* - If the user sends your app to the recent App's tray and then swipes it away,
195+
* [TorService.onTaskRemoved] will stop Tor, and then [TorService].
196+
* - Because of how shitty the Service APIs are, your application will _not_ be
197+
* killed like one would expect, thus not going through `Application.onCreate` if the
198+
* user re-launches your application.
199+
* - In the event of being re-launched in the aforementioned state,
200+
* [applicationMovedToForeground] is called and Tor will be started again to match the
201+
* Service's State for which it was left, prior to "terminating" your application.
202+
* - Even while the Service has been properly stopped and everything cleaned up, your
203+
* application will continue running and not be killed (Again, Service APIs...).
204+
* - If [TorService] is stopped, and *then* your application is cleared from the
205+
* recent apps tray, your application will be killed.
206+
*
207+
* @param [killAppIfTaskIsRemoved] If set to `true`, your Application's Process will be
208+
* killed in [TorService.onDestroy] if the user removed the task from the recent app's
209+
* tray and has not returned to the application before [killAppProcess] is called.
210+
* */
211+
@JvmOverloads
212+
fun runServiceInForeground(killAppIfTaskIsRemoved: Boolean = false): Policy {
213+
chosenPolicy = BackgroundPolicy.RUN_IN_FOREGROUND
214+
this.killAppIfTaskIsRemoved = killAppIfTaskIsRemoved
215+
return Policy(this)
216+
}
217+
203218
/**
204219
* Holds the chosen policy to be built in
205220
* [io.matthewnelson.topl_service.TorServiceController.Builder.build].
@@ -229,25 +244,46 @@ class BackgroundManager internal constructor(
229244
policyBuilder.chosenPolicy,
230245
policyBuilder.executionDelay,
231246
serviceClass,
232-
bindServiceFlag
247+
bindServiceFlag,
248+
policyBuilder.killAppIfTaskIsRemoved
233249
)
234250
}
235251
}
236252
}
237253
}
238254

239-
internal companion object {
255+
companion object {
240256
private lateinit var backgroundManager: BackgroundManager
241257

242-
// TODO: re-implement in BaseService as a monitor for Tor's state so it can automatically
243-
// handle hiccups (such as network getting stuck b/c Android is sometimes unreliable,
244-
// or Bootstrapping stalling).
245-
// var heartbeatTime = 30_000L
246-
// private set
247-
//
248-
// fun initialize(milliseconds: Long) {
249-
// heartbeatTime = milliseconds
250-
// }
258+
@JvmStatic
259+
@Volatile
260+
var taskIsInForeground = true
261+
private set
262+
263+
@JvmStatic
264+
@Volatile
265+
var taskIsRemovedFromRecentApps = false
266+
private set
267+
268+
internal fun taskIsRemovedFromRecentApps(isRemoved: Boolean) {
269+
taskIsRemovedFromRecentApps = isRemoved
270+
}
271+
272+
/**
273+
* Called from [TorService.onDestroy].
274+
*
275+
* Will only kill the application process if [taskIsRemovedFromRecentApps] is `true`.
276+
* [taskIsRemovedFromRecentApps] will turn back to `false` when
277+
* [applicationMovedToForeground] is fired off, as to not kill the application in the
278+
* event the user quickly re-launches the application between the time
279+
* [TorService.onTaskRemoved] gets a callback, and this method is called from
280+
* [TorService.onDestroy].
281+
* */
282+
internal fun killAppProcess() {
283+
if (backgroundManager.killAppIfTaskIsRemoved && taskIsRemovedFromRecentApps) {
284+
Process.killProcess(Process.myPid())
285+
}
286+
}
251287
}
252288

253289
init {
@@ -256,6 +292,8 @@ class BackgroundManager internal constructor(
256292

257293
@OnLifecycleEvent(Lifecycle.Event.ON_START)
258294
private fun applicationMovedToForeground() {
295+
taskIsRemovedFromRecentApps(false)
296+
taskIsInForeground = true
259297
// if the last _accepted_ ServiceAction to be issued by the Application was not to STOP
260298
// the service, then we want to put it back in the state it was in
261299
if (!ServiceActionProcessor.wasLastAcceptedServiceActionStop()) {
@@ -271,7 +309,16 @@ class BackgroundManager internal constructor(
271309

272310
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
273311
private fun applicationMovedToBackground() {
274-
if (!ServiceActionProcessor.wasLastAcceptedServiceActionStop())
312+
taskIsInForeground = false
313+
if (!ServiceActionProcessor.wasLastAcceptedServiceActionStop()) {
314+
// System automatically unbinds when app is sent to the background. This prevents
315+
// it so that we maintain a started, bound service.
316+
BaseService.bindService(
317+
BaseService.getAppContext(),
318+
serviceClass,
319+
bindServiceFlag = bindServiceFlag
320+
)
275321
TorServiceConnection.serviceBinder?.executeBackgroundPolicyJob(policy, executionDelay)
322+
}
276323
}
277324
}

topl-service/src/main/java/io/matthewnelson/topl_service/notification/ServiceNotification.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,15 @@ class ServiceNotification internal constructor(
432432
if (notificationRefreshJob?.isActive == true)
433433
notificationRefreshJob?.cancel()
434434

435-
notificationRefreshJob = torService.getScopeMain().launch {
435+
if (inForeground)
436+
return
437+
438+
notificationRefreshJob = torService.getScopeIO().launch {
436439
delay(timeoutLength - 250L)
437-
notificationBuilder?.let {
438-
notify(torService, it)
439-
}
440+
if (!inForeground)
441+
notificationBuilder?.let {
442+
notify(torService, it)
443+
}
440444
}
441445
}
442446

@@ -492,6 +496,7 @@ class ServiceNotification internal constructor(
492496
if (inForeground) {
493497
torService.stopForeground(!showNotification)
494498
inForeground = false
499+
launchRefreshNotificationJob(torService)
495500
}
496501
return serviceNotification
497502
}

topl-service/src/main/java/io/matthewnelson/topl_service/service/BaseService.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import io.matthewnelson.topl_core.broadcaster.BroadcastLogger
7676
import io.matthewnelson.topl_core_base.TorConfigFiles
7777
import io.matthewnelson.topl_core_base.TorSettings
7878
import io.matthewnelson.topl_service.BuildConfig
79+
import io.matthewnelson.topl_service.lifecycle.BackgroundManager
7980
import io.matthewnelson.topl_service.notification.ServiceNotification
8081
import io.matthewnelson.topl_service.prefs.TorServicePrefsListener
8182
import io.matthewnelson.topl_service.service.components.actions.ServiceActionProcessor
@@ -194,17 +195,34 @@ internal abstract class BaseService: Service() {
194195
// the system via START_STICKY
195196
return try {
196197
context.applicationContext.startService(intent)
197-
context.applicationContext.bindService(
198-
intent,
199-
TorServiceConnection.torServiceConnection,
200-
bindServiceFlag
201-
)
198+
bindService(context, serviceClass, bindServiceFlag)
202199
true
203200
} catch (e: RuntimeException) {
204201
false
205202
}
206203
}
207204

205+
/**
206+
* Binds the Service.
207+
*
208+
* @param [context]
209+
* @param [serviceClass] The Service's class wanting to be started
210+
* @param [bindServiceFlag] The flag to use when binding to [TorService]
211+
* @return true if startService didn't throw an exception, false if it did.
212+
* */
213+
fun bindService(
214+
context: Context,
215+
serviceClass: Class<*>,
216+
bindServiceFlag: Int = Context.BIND_AUTO_CREATE
217+
) {
218+
context.applicationContext.bindService(
219+
Intent(context.applicationContext, serviceClass),
220+
TorServiceConnection.torServiceConnection,
221+
bindServiceFlag
222+
)
223+
}
224+
225+
208226
/**
209227
* Unbinds [TorService] from the Application and clears the reference to
210228
* [TorServiceConnection.serviceBinder].
@@ -360,6 +378,7 @@ internal abstract class BaseService: Service() {
360378

361379

362380
override fun onTaskRemoved(rootIntent: Intent?) {
381+
BackgroundManager.taskIsRemovedFromRecentApps(true)
363382
// Move to the foreground so we can properly shutdown w/o interrupting the
364383
// application's normal lifecycle (Context.startServiceForeground does... thus,
365384
// the complexity)

topl-service/src/main/java/io/matthewnelson/topl_service/service/TorService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import io.matthewnelson.topl_core.OnionProxyManager
7373
import io.matthewnelson.topl_core.broadcaster.BroadcastLogger
7474
import io.matthewnelson.topl_core.util.FileUtilities
7575
import io.matthewnelson.topl_service.TorServiceController
76+
import io.matthewnelson.topl_service.lifecycle.BackgroundManager
7677
import io.matthewnelson.topl_service.service.components.binding.TorServiceBinder
7778
import io.matthewnelson.topl_service.service.components.binding.TorServiceConnection
7879
import io.matthewnelson.topl_service.service.components.onionproxy.ServiceEventBroadcaster
@@ -258,6 +259,7 @@ internal class TorService: BaseService() {
258259
super.onDestroy()
259260
supervisorJob.cancel()
260261
removeNotification()
262+
BackgroundManager.killAppProcess()
261263
}
262264

263265
override fun onTaskRemoved(rootIntent: Intent?) {

topl-service/src/main/java/io/matthewnelson/topl_service/service/components/binding/BaseServiceBinder.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@
6666
* */
6767
package io.matthewnelson.topl_service.service.components.binding
6868

69+
import android.app.Activity
70+
import android.app.Application
6971
import android.os.Binder
72+
import android.os.Bundle
7073
import io.matthewnelson.topl_core.broadcaster.BroadcastLogger
7174
import io.matthewnelson.topl_service.service.BaseService
7275
import io.matthewnelson.topl_service.lifecycle.BackgroundManager
@@ -77,6 +80,7 @@ import kotlinx.coroutines.Job
7780
import kotlinx.coroutines.delay
7881
import kotlinx.coroutines.isActive
7982
import kotlinx.coroutines.launch
83+
import net.freehaven.tor.control.TorControlCommands
8084

8185

8286
internal abstract class BaseServiceBinder(private val torService: BaseService): Binder() {
@@ -113,16 +117,9 @@ internal abstract class BaseServiceBinder(private val torService: BaseService):
113117
cancelExecuteBackgroundPolicyJob()
114118
backgroundPolicyExecutionJob = torService.getScopeMain().launch {
115119
when (policy) {
116-
BackgroundPolicy.KEEP_ALIVE -> {
117-
while (isActive && TorServiceConnection.serviceBinder != null) {
118-
delay(executionDelay)
119-
if (isActive && TorServiceConnection.serviceBinder != null) {
120-
bgMgrBroadcastLogger.debug("Executing background management policy")
121-
torService.stopForegroundService()
122-
torService.startForegroundService()
123-
torService.stopForegroundService()
124-
}
125-
}
120+
BackgroundPolicy.RUN_IN_FOREGROUND -> {
121+
bgMgrBroadcastLogger.debug("Executing background management policy")
122+
torService.startForegroundService()
126123
}
127124
BackgroundPolicy.RESPECT_RESOURCES -> {
128125
delay(executionDelay)

topl-service/src/main/java/io/matthewnelson/topl_service/util/ServiceConsts.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ abstract class ServiceConsts: BaseConsts() {
8383
AnnotationTarget.TYPE
8484
)
8585
@StringDef(
86-
BackgroundPolicy.KEEP_ALIVE,
87-
BackgroundPolicy.RESPECT_RESOURCES
86+
BackgroundPolicy.RESPECT_RESOURCES,
87+
BackgroundPolicy.RUN_IN_FOREGROUND
8888
)
8989
@Retention(AnnotationRetention.SOURCE)
9090
internal annotation class BackgroundPolicy {
9191
companion object {
9292
private const val BACKGROUND_POLICY = "BackgroundPolicy_"
93-
const val KEEP_ALIVE = "${BACKGROUND_POLICY}KEEP_ALIVE"
9493
const val RESPECT_RESOURCES = "${BACKGROUND_POLICY}RESPECT_RESOURCES"
94+
const val RUN_IN_FOREGROUND = "${BACKGROUND_POLICY}RUN_IN_FOREGROUND"
9595
}
9696
}
9797

0 commit comments

Comments
 (0)