6767package io.matthewnelson.topl_service.lifecycle
6868
6969import android.content.Context
70+ import android.os.Process
7071import androidx.lifecycle.Lifecycle
7172import androidx.lifecycle.LifecycleObserver
7273import 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}
0 commit comments