steveliles / Foredroid

Utility for detecting and notifying when your Android app goes background / becomes foreground
Other
153 stars 28 forks source link

This library is an error implementation. Here is a simple way to achieve. #15

Closed TakWolf closed 7 years ago

TakWolf commented 7 years ago
public class ApplicationListener implements Application.ActivityLifecycleCallbacks {

    private int foregroundCount = 0; 

    @Override
    public void onActivityStarted(Activity activity) {
        if (foregroundCount <= 0) {
            // TODO becomes foreground
        }
        foregroundCount++;
    }

    @Override
    public void onActivityStopped(Activity activity) {
        foregroundCount--;
        if (foregroundCount <= 0) {
            // TODO goes background
        }
    }

    /*
     * The following callbacks, we do not need
     */

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}

    @Override
    public void onActivityResumed(Activity activity) {}

    @Override
    public void onActivityPaused(Activity activity) {}

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

    @Override
    public void onActivityDestroyed(Activity activity) {}

}

That is.

If ActivityA go to ActivityB,the callbacks is :

A.onPause()
B.onStart()
B.onResume()
A.onStop()

If ActivityB return ActivityA, the callbacks is:

B.onPause()
A.onStart()
A.onResume()
B.onStop()

So, set a counter to statistics onStart() and onStop() callbacks. if the counter is > 0, Application is in foreground, opposite in background.

steveliles commented 7 years ago

There are so many edge cases and quirks, i doubt that the solution you've posted here (which I believe was suggested at Google IO) covers them all - it certainly did not when I originally wrote this lib, because that was exactly what I started out with.

TakWolf commented 7 years ago

Test for all scenes, there is only one scene need to special treatment: android.configChanges

So compat this scene, update the codes:

public class ApplicationListener implements Application.ActivityLifecycleCallbacks {

    private int foregroundCount = 0;
    private int bufferCount = 0;

    @Override
    public void onActivityStarted(Activity activity) {
        if (foregroundCount <= 0) {
            // TODO becomes foreground
        }
        if (bufferCount < 0) {
            bufferCount++;
        } else {
            foregroundCount++;
        }
    }

    @Override
    public void onActivityStopped(Activity activity) {
        if (!activity.isChangingConfigurations()) {
            foregroundCount--;
            if (foregroundCount <= 0) {
                // TODO goes background
            }
        } else {
            bufferCount--;
        }
    }

    /*
     * The following callbacks, we do not need
     */

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}

    @Override
    public void onActivityResumed(Activity activity) {}

    @Override
    public void onActivityPaused(Activity activity) {}

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

    @Override
    public void onActivityDestroyed(Activity activity) {}

}

I don't know what's the other edge cases and quirks. But make me puzzled at https://github.com/steveliles/Foredroid/blob/2eed0445f9f6e13258de759eeae0a0daad5d3520/Foredroid/src/main/java/com/sjl/foreground/Foreground.java#L179

and

https://github.com/steveliles/Foredroid/blob/2eed0445f9f6e13258de759eeae0a0daad5d3520/Foredroid/src/main/java/com/sjl/foreground/Foreground.java#L191

Why use handler delay check in onPaused ? This makes the logic complicated and leaving the hidden dangers (WeakReference).

Can provide an edge cases to let me make a test. Or give me some reason about that?

steveliles commented 7 years ago

Check the blog post and comments ...

http://steveliles.github.io/is_my_android_app_currently_foreground_or_background.html

Besides config changes, another significant problem is: if you receive a phone call onStop is not called.

TakWolf commented 7 years ago

About receive a phone call: In my test, that's really did not call onStop, only call onPause(System version 4.4, 5.1 and 7.0). But does in calling means application enter background?

In iOS, there is a class named AppDelegate (similar to Androdi.Application

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

}

In my understanding, iOS only have the application level callbacks. Android only have the controller level callbacks. But in concept, they are the same. applicationWillResignActive and applicationDidBecomeActive is similar to activity.onPause and activity.onResume, means hang up statius switch. applicationDidEnterBackground and applicationWillEnterForeground means iOS application's visibility switch. activity.onStop and activity.onStart means activity's visibility switch.

In iOS, if receive a phone call, it will only call applicationWillResignActive, but not call applicationDidEnterBackground. (This behavior can be simulated by a real machine.)

So, can i think that, Android 's behavior is same to iOS, although onStop not call?
And should this case really need to be handled specifically?

TakWolf commented 7 years ago

About request permissions, like case https://github.com/steveliles/Foredroid/issues/12 :

In Android, it will only call onPause, not call onStop (Because activity is still visiable); In iOS, it will only call applicationWillResignActive, not call applicationDidEnterBackground

So request permissions means application paused, but not enter background.

Android 's behavior is same to iOS.

TakWolf commented 7 years ago

So use the onPause hack way to handle this service may be not a good idea. More reasonable approach is: distinguish between two cases.

Use onStart and onStop to listen application switch foreground and background in a stable way. Use onResume and onPause to listen application level resumed or paused in a hack way. Choose one of it depending on your situation.

steveliles commented 7 years ago

but does in calling means application enter background?

Since API 11 Android does guarantee to call onStop before killing your application. However it does not guarantee to call it in a timely fashion if you are simply moved to the background. Phone calls are one case where this manifests in the real world - there may be others too, e.g. receiving calls from other apps such as skype, whatsapp, slack, etc. - I don't know.

The phone call case is very clear-cut. You are most definitely put in the background when a phone call is received, so the onStart/onStop method simply does not work as a mechanism for determining the foreground status of an app.

The permissions case is a much more grey area IMHO, but there are also ways around that because your app instigates the permission request, so you know when a permissions request is in flight.

More assistance for that case could perhaps be baked into the library (runtime permissions weren't a thing when I wrote Foredroid, and I've not worked with native Android much since then).

TakWolf commented 7 years ago

In Android, there is no definition about application enter background or application paused(For this reason, we have to simulate one). The problems is what is the reasonable behavior? Because there is no definition, I tend to find a reference, like iOS. What the behavior in iOS? According to the reasons I have said above, only use onStop() and onStart() can simulate a same behavior to iOS. The same behavior means, I can use the same logic and process to write the service for the two platforms. I think this is a better practice.

In iOS, request permissions and recived a calling means application paused, but not enter background. Android is the same behavior, so do not need to do any special treatment, it is great!

But there are always annoying demands that we need to achieve, like request permissions and recived a calling. The boss or the users wants these behaviors to behave as application enter background. Fortunately, these cases can be handle specially:

Recive a calling can be listen by using BroadcastReceiver and android.intent.action.PHONE_STATE; Request permissions can be listen by set flags in your own action and onRequestPermissionsResult.

So you can handle them using an elegant way. More importantly, you do not need anything hacking! All the code is clear and stable, users can combine solutions according to their own situation, rather than using a blurry logic of the boundary.