[go: up one dir, main page]

blob: 9ea47640e7a8096c2277defcde92e8b303189152 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.feed;
import android.app.Activity;
import android.support.annotation.IntDef;
import com.google.android.libraries.feed.api.client.lifecycle.AppLifecycleListener;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.signin.SigninManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Aggregation point for application lifecycle events that the Feed cares about. Events that
* originate in Java flow directly to FeedAppLifecycle, while native-originating events arrive
* via {@link FeedLifecycleBridge}.
*/
public class FeedAppLifecycle
implements SigninManager.SignInStateObserver, ApplicationStatus.ActivityStateListener {
// Intdef used to assign each event a number for metrics logging purposes. This maps directly to
// the AppLifecycleEvent enum defined in tools/metrics/enums.xml
@IntDef({AppLifecycleEvent.ENTER_FOREGROUND, AppLifecycleEvent.ENTER_BACKGROUND,
AppLifecycleEvent.CLEAR_ALL, AppLifecycleEvent.INITIALIZE, AppLifecycleEvent.SIGN_IN,
AppLifecycleEvent.SIGN_OUT, AppLifecycleEvent.HISTORY_DELETED,
AppLifecycleEvent.CACHED_DATA_CLEARED})
@Retention(RetentionPolicy.SOURCE)
public @interface AppLifecycleEvent {
int ENTER_FOREGROUND = 0;
int ENTER_BACKGROUND = 1;
int CLEAR_ALL = 2;
int INITIALIZE = 3;
int SIGN_IN = 4;
int SIGN_OUT = 5;
int HISTORY_DELETED = 6;
int CACHED_DATA_CLEARED = 7;
int NUM_ENTRIES = 8;
}
private AppLifecycleListener mAppLifecycleListener;
private FeedLifecycleBridge mLifecycleBridge;
private FeedScheduler mFeedScheduler;
private int mTabbedActivityCount;
private boolean mInitializeCalled;
private boolean mDelayedInitializeStarted;
/**
* Create a FeedAppLifecycle instance. In normal use, this should only be called by {@link
* FeedAppLifecycleFactory}.
* @param appLifecycleListener The Feed-side instance of the {@link AppLifecycleListener}
* interface that we will call into.
* @param lifecycleBridge FeedLifecycleBridge JNI bridge over which native lifecycle events are
* delivered.
* @param feedScheduler Scheduler to be notified of several events.
*/
public FeedAppLifecycle(AppLifecycleListener appLifecycleListener,
FeedLifecycleBridge lifecycleBridge, FeedScheduler feedScheduler) {
mAppLifecycleListener = appLifecycleListener;
mLifecycleBridge = lifecycleBridge;
mFeedScheduler = feedScheduler;
int resumedActivityCount = 0;
for (Activity activity : ApplicationStatus.getRunningActivities()) {
if (activity instanceof ChromeTabbedActivity) {
@ActivityState
int activityState = ApplicationStatus.getStateForActivity(activity);
if (activityState != ActivityState.STOPPED) {
++mTabbedActivityCount;
}
if (activityState == ActivityState.RESUMED) {
++resumedActivityCount;
}
}
}
if (mTabbedActivityCount > 0) {
onEnterForeground();
}
// The scheduler cares about Chrome entering the visual foreground, which corresponds to the
// RESUMED state. This state is entered regardless of whether or not the Activity was
// previously paused.
if (resumedActivityCount > 0) {
mFeedScheduler.onForegrounded();
}
ApplicationStatus.registerStateListenerForAllActivities(this);
SigninManager.get().addSignInStateObserver(this);
}
/**
* This is called when an NTP is shown.
*/
public void onNTPOpened() {
initialize();
}
/**
* This is called when the user has deleted some non-trivial number of history entries.
* We call onClearAll to avoid presenting personalized suggestions based on deleted history.
*/
public void onHistoryDeleted() {
reportEvent(AppLifecycleEvent.HISTORY_DELETED);
onClearAll(/*suppressRefreshes*/ true);
}
/**
* This is called when cached browsing data is cleared. We call onClearAll so that the
* Feed deletes its cached browsing data.
*/
public void onCachedDataCleared() {
reportEvent(AppLifecycleEvent.CACHED_DATA_CLEARED);
onClearAll(/*suppressRefreshes*/ false);
}
/**
* Unregisters listeners and cleans up any native resources held by FeedAppLifecycle.
*/
public void destroy() {
SigninManager.get().removeSignInStateObserver(this);
ApplicationStatus.unregisterActivityStateListener(this);
mLifecycleBridge.destroy();
mLifecycleBridge = null;
mAppLifecycleListener = null;
mFeedScheduler = null;
}
@Override
public void onActivityStateChange(Activity activity, @ActivityState int newState) {
// We only care about ChromeTabbedActivity since no other type of activity could potentially
// show the Feed.
if (activity != null && activity instanceof ChromeTabbedActivity) {
switch (newState) {
case ActivityState.STOPPED:
--mTabbedActivityCount;
if (mTabbedActivityCount == 0) {
onEnterBackground();
}
break;
case ActivityState.STARTED:
++mTabbedActivityCount;
if (mTabbedActivityCount == 1) {
onEnterForeground();
}
break;
case ActivityState.RESUMED:
mFeedScheduler.onForegrounded();
break;
}
}
}
@Override
public void onSignedIn() {
reportEvent(AppLifecycleEvent.SIGN_IN);
onClearAll(/*suppressRefreshes*/ false);
}
@Override
public void onSignedOut() {
reportEvent(AppLifecycleEvent.SIGN_OUT);
onClearAll(/*suppressRefreshes*/ false);
}
private void onEnterForeground() {
reportEvent(AppLifecycleEvent.ENTER_FOREGROUND);
mAppLifecycleListener.onEnterForeground();
if (!mDelayedInitializeStarted) {
mDelayedInitializeStarted = true;
boolean initFeed = ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.INTEREST_FEED_CONTENT_SUGGESTIONS, "init_feed_after_startup",
false);
if (initFeed) {
DeferredStartupHandler.getInstance().addDeferredTask(() -> {
// Since this is being run asynchronously, it's possible #destroy() is called
// before the delay finishes. Must guard against this.
if (mLifecycleBridge != null) {
initialize();
}
});
}
}
}
private void onEnterBackground() {
reportEvent(AppLifecycleEvent.ENTER_BACKGROUND);
mAppLifecycleListener.onEnterBackground();
}
private void onClearAll(boolean suppressRefreshes) {
reportEvent(AppLifecycleEvent.CLEAR_ALL);
// Clearing and triggering refreshes are both asynchronous operations. The Feed is able to
// better coordinate them if {@link AppLifecycleListener#onClearAllWithRefresh} is called.
// If the scheduler returns true from {@link FeedScheduler#onArticlesCleared}, this means
// that it did not trigger the refresh, but is allowing us to do so.
if (mFeedScheduler.onArticlesCleared(suppressRefreshes)) {
mAppLifecycleListener.onClearAllWithRefresh();
} else {
mAppLifecycleListener.onClearAll();
}
}
private void initialize() {
if (!mInitializeCalled) {
reportEvent(AppLifecycleEvent.INITIALIZE);
mAppLifecycleListener.initialize();
mInitializeCalled = true;
}
}
private void reportEvent(@AppLifecycleEvent int event) {
RecordHistogram.recordEnumeratedHistogram("ContentSuggestions.Feed.AppLifecycle.Events",
event, AppLifecycleEvent.NUM_ENTRIES);
}
}