Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(android): Ti.App wrongly fires pause/resume events when opening/closing child windows #10634

Merged
merged 7 commits into from
Mar 21, 2019
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,82 @@
import com.appcelerator.aps.APSAnalytics;

import org.appcelerator.kroll.common.Log;
import org.appcelerator.kroll.KrollModule;

public class TiApplicationLifecycle implements Application.ActivityLifecycleCallbacks
{
private static final String TAG = "TiApplicationLifecycle";

private TiApplication tiApp = TiApplication.getInstance();
private static int activityCount = 0;
private int existingActivityCount;
private int visibleActivityCount;
private boolean wasPaused;

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState)
{
// Reset "wasPaused" state when creating the 1st activity in a UI task.
// Needed to detect if the app is resuming after being paused.
if (this.existingActivityCount <= 0) {
this.wasPaused = false;
}

// Increment count of all known activities.
this.existingActivityCount++;
}

@Override
public void onActivityStarted(Activity activity)
{
if (activityCount == 0 && tiApp != null && tiApp.isAnalyticsEnabled()) {
APSAnalytics.getInstance().sendAppForegroundEvent();
// If no activities have been started, then app is going to be put into the foreground.
if (this.visibleActivityCount == 0) {
// Fire Ti.App resume events.
// Note: The "resume" event should only be fired after a "pause" event and never on app startup.
KrollModule appModule = this.tiApp.getModuleByName("App");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also put appModule in TiApplicationLifecycle, so we don't have to grab it each time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that we add a new "App" module instance every time a new JavaScript runtime gets created (ie: back-out of the app and relaunch).

if (appModule != null) {
if (this.wasPaused) {
appModule.fireEvent(TiC.EVENT_RESUME, null);
}
appModule.fireEvent(TiC.EVENT_RESUMED, null);
}

// Post analytics for this event, if enabled.
if (this.tiApp.isAnalyticsEnabled()) {
APSAnalytics.getInstance().sendAppForegroundEvent();
}
}
activityCount++;

// Increment number of "started" activities. These are activities that are currently in the foreground.
// Note: Should never be more than 1, unless some of these activities are fragments.
this.visibleActivityCount++;
}

@Override
public void onActivityStopped(Activity activity)
{
if (activityCount == 1 && tiApp != null && tiApp.isAnalyticsEnabled()) {
APSAnalytics.getInstance().sendAppBackgroundEvent();
// If this is the last activity being stopped, then the app is going to be put into the background.
if (this.visibleActivityCount == 1) {
// Flag that we've been paused at least once for this UI task.
this.wasPaused = true;

// Fire Ti.App pause events.
KrollModule appModule = this.tiApp.getModuleByName("App");
if (appModule != null) {
appModule.fireEvent(TiC.EVENT_PAUSE, null);
appModule.fireEvent(TiC.EVENT_PAUSED, null);
}

// Post analytics for this event, if enabled.
if (this.tiApp.isAnalyticsEnabled()) {
APSAnalytics.getInstance().sendAppBackgroundEvent();
}
}

// Decrement count of started/visible activities.
this.visibleActivityCount--;
if (this.visibleActivityCount < 0) {
this.visibleActivityCount = 0;
}
activityCount--;
}

@Override
Expand All @@ -62,5 +109,10 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState)
@Override
public void onActivityDestroyed(Activity activity)
{
// Decrement total activity count.
this.existingActivityCount--;
if (this.existingActivityCount < 0) {
this.existingActivityCount = 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1375,11 +1375,6 @@ protected void onPause()
if (activityProxy != null) {
activityProxy.fireEvent(TiC.EVENT_PAUSE, null);
}
KrollModule appModule = tiApp.getModuleByName("App");
if (appModule != null) {
appModule.fireEvent(TiC.EVENT_PAUSE, null);
appModule.fireEvent(TiC.EVENT_PAUSED, null);
}

synchronized (lifecycleListeners.synchronizedList())
{
Expand Down Expand Up @@ -1421,11 +1416,6 @@ protected void onResume()
if (activityProxy != null) {
activityProxy.fireEvent(TiC.EVENT_RESUME, null);
}
KrollModule appModule = tiApp.getModuleByName("App");
if (appModule != null) {
appModule.fireEvent(TiC.EVENT_RESUME, null);
appModule.fireEvent(TiC.EVENT_RESUMED, null);
}

synchronized (lifecycleListeners.synchronizedList())
{
Expand Down
13 changes: 8 additions & 5 deletions apidoc/Titanium/App/App.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ events:
Note that calls to functions that modify the UI during this event may be partially executed,
**up to** the UI call before the suspension. See [paused](Titanium.App.paused) event. If this happens, the remainder of the code will
be executed after the application is resumed, but before the `resume` event is triggered.
platforms: [iphone, ipad]
platforms: [android, iphone, ipad]
since: {android: "7.5.0", iphone: "0.8", ipad: "0.8"}

- name: paused
summary: Fired when the application transitions to the background on a multitasked system.
Expand All @@ -214,8 +215,8 @@ events:
[Monitoring Application State Changes](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground?language=objc)
for the exact behavior that triggers this event.

platforms: [iphone, ipad]
since: '2.1.0'
platforms: [android, iphone, ipad]
since: {android: "7.5.0", iphone: "2.1.0", ipad: "2.1.0"}

- name: proximity
summary: Fired when the proximity sensor changes state.
Expand Down Expand Up @@ -280,7 +281,8 @@ events:
See the `applicationWillEnterForeground` section of the official Apple documentation about
[Monitoring Application State Changes](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground)
for the exact behavior that triggers this event.
platforms: [iphone, ipad]
platforms: [android, iphone, ipad]
since: {android: "7.5.0", iphone: "0.8", ipad: "0.8"}

- name: resumed
summary: Fired when the application returns to the foreground.
Expand All @@ -296,7 +298,8 @@ events:
Note: This event will not be fired for URL's in iOS 10+ that are handled by the <Modules.SafariDialog>
or <Titanium.UI.WebView>, because Apple does not call the `applicationDidBecomeActive` for
URL-handling anymore. Instead, use the `handleurl` event in <Titanium.App.iOS>.
platforms: [iphone, ipad]
platforms: [android, iphone, ipad]
since: {android: "7.5.0", iphone: "0.8", ipad: "0.8"}

- name: started
summary: Fired after the "app.js" or "alloy.js" gets executed during application startup.
Expand Down
54 changes: 54 additions & 0 deletions tests/Resources/ti.app.addontest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Appcelerator Titanium Mobile
* Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
/* eslint-env mocha */
/* global Ti */
/* eslint no-unused-expressions: "off" */
'use strict';
var should = require('./utilities/assertions');

describe('Titanium.App', function () {
it.android('pause/resume events', function (finish) {
this.timeout(5000);
let wasPauseEventReceived = false;
let wasResumeEventReceived = false;

// Handle Ti.App pause/resume events. They happen when app is sent to background/foreground.
// - "pause" event must be received before "paused" event.
// - "resume" event must be received before "resumed" event.
Ti.App.addEventListener('pause', function pauseEventHandler(e) {
Ti.API.info('Received event: ' + e.type);
Ti.App.removeEventListener(e.type, pauseEventHandler);
wasPauseEventReceived = true;
});
Ti.App.addEventListener('paused', function pausedEventHandler(e) {
Ti.API.info('Received event: ' + e.type);
Ti.App.removeEventListener(e.type, pausedEventHandler);
should(wasPauseEventReceived).be.true;
Ti.Android.currentActivity.startActivity(Ti.App.Android.launchIntent); // Resume this app.
});
Ti.App.addEventListener('resume', function resumeEventHandler(e) {
Ti.API.info('Received event: ' + e.type);
Ti.App.removeEventListener(e.type, resumeEventHandler);
wasResumeEventReceived = true;
});
Ti.App.addEventListener('resumed', function resumedEventHandler(e) {
Ti.API.info('Received event: ' + e.type);
Ti.App.removeEventListener(e.type, resumedEventHandler);
should(wasResumeEventReceived).be.true;
finish();
});

// Navigate to the device's home screen. Equivalent to pressing the "home" button.
// This should fire this app's "pause" and "paused" events.
const homeIntent = Ti.Android.createIntent({
action: Ti.Android.ACTION_MAIN,
});
homeIntent.addCategory(Ti.Android.CATEGORY_HOME);
homeIntent.setFlags(Ti.Android.FLAG_ACTIVITY_NEW_TASK);
Ti.Android.currentActivity.startActivity(homeIntent);
});
});