// Copyright 2016 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.ntp.snippets;
import android.content.Context;
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.PeriodicTask;
import com.google.android.gms.gcm.Task;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.ChromeBackgroundService;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalauth.UserRecoverableErrorHandler;
/**
* The {@link SnippetsLauncher} singleton is created and owned by the C++ browser.
*
* Thread model: This class is to be run on the UI thread only.
*/
public class SnippetsLauncher {
private static final String TAG = "SnippetsLauncher";
// Task tags for fetching snippets.
public static final String TASK_TAG_WIFI = "FetchSnippetsWifi";
public static final String TASK_TAG_FALLBACK = "FetchSnippetsFallback";
// TODO(treib): Remove this after M55.
private static final String OBSOLETE_TASK_TAG_WIFI_CHARGING = "FetchSnippetsWifiCharging";
// TODO(treib): Remove this after M55.
private static final String OBSOLETE_TASK_TAG_RESCHEDULE = "RescheduleSnippets";
// The amount of "flex" to add around the fetching periods, as a ratio of the period.
private static final double FLEX_FACTOR = 0.1;
@VisibleForTesting
public static final String PREF_IS_SCHEDULED = "ntp_snippets.is_scheduled";
// The instance of SnippetsLauncher currently owned by a C++ SnippetsLauncherAndroid, if any.
// If it is non-null then the browser is running.
private static SnippetsLauncher sInstance;
private GcmNetworkManager mScheduler;
private boolean mGCMEnabled = true;
/**
* Create a SnippetsLauncher object, which is owned by C++.
* @param context The app context.
*/
@VisibleForTesting
@CalledByNative
public static SnippetsLauncher create(Context context) {
if (sInstance != null) {
throw new IllegalStateException("Already instantiated");
}
sInstance = new SnippetsLauncher(context);
return sInstance;
}
/**
* Called when the C++ counterpart is deleted.
*/
@VisibleForTesting
@SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
@CalledByNative
public void destroy() {
assert sInstance == this;
sInstance = null;
}
/**
* Returns true if the native browser has started and created an instance of {@link
* SnippetsLauncher}.
*/
public static boolean hasInstance() {
return sInstance != null;
}
protected SnippetsLauncher(Context context) {
checkGCM(context);
mScheduler = GcmNetworkManager.getInstance(context);
}
private boolean canUseGooglePlayServices(Context context) {
return ExternalAuthUtils.getInstance().canUseGooglePlayServices(
context, new UserRecoverableErrorHandler.Silent());
}
private void checkGCM(Context context) {
// Check to see if Play Services is up to date, and disable GCM if not.
if (!canUseGooglePlayServices(context)) {
mGCMEnabled = false;
Log.i(TAG, "Disabling SnippetsLauncher because Play Services is not up to date.");
}
}
private static PeriodicTask buildFetchTask(
String tag, long periodSeconds, int requiredNetwork) {
// Add a bit of "flex" around the target period. This achieves the following:
// - It makes sure the task doesn't run (significantly) before its initial period has
// elapsed. In practice, the scheduler seems to behave like that anyway, but it doesn't
// guarantee that, so we shouldn't rely on it.
// - It gives the scheduler a bit of room to optimize for battery life.
long effectivePeriodSeconds = (long) (periodSeconds * (1.0 + FLEX_FACTOR));
long flexSeconds = (long) (periodSeconds * (2.0 * FLEX_FACTOR));
return new PeriodicTask.Builder()
.setService(ChromeBackgroundService.class)
.setTag(tag)
.setPeriod(effectivePeriodSeconds)
.setFlex(flexSeconds)
.setRequiredNetwork(requiredNetwork)
.setPersisted(true)
.setUpdateCurrent(true)
.build();
}
private void scheduleOrCancelFetchTask(String taskTag, long period, int requiredNetwork) {
if (period > 0) {
mScheduler.schedule(buildFetchTask(taskTag, period, requiredNetwork));
} else {
mScheduler.cancelTask(taskTag, ChromeBackgroundService.class);
}
}
@CalledByNative
private boolean schedule(long periodWifiSeconds, long periodFallbackSeconds) {
if (!mGCMEnabled) return false;
Log.i(TAG, "Scheduling: " + periodWifiSeconds + " " + periodFallbackSeconds);
boolean isScheduled = periodWifiSeconds != 0 || periodFallbackSeconds != 0;
ContextUtils.getAppSharedPreferences()
.edit()
.putBoolean(PREF_IS_SCHEDULED, isScheduled)
.apply();
// Google Play Services may not be up to date, if the application was not installed through
// the Play Store. In this case, scheduling the task will fail silently.
try {
mScheduler.cancelTask(OBSOLETE_TASK_TAG_WIFI_CHARGING, ChromeBackgroundService.class);
scheduleOrCancelFetchTask(
TASK_TAG_WIFI, periodWifiSeconds, Task.NETWORK_STATE_UNMETERED);
scheduleOrCancelFetchTask(
TASK_TAG_FALLBACK, periodFallbackSeconds, Task.NETWORK_STATE_CONNECTED);
mScheduler.cancelTask(OBSOLETE_TASK_TAG_RESCHEDULE, ChromeBackgroundService.class);
} catch (IllegalArgumentException e) {
// Disable GCM for the remainder of this session.
mGCMEnabled = false;
ContextUtils.getAppSharedPreferences().edit().remove(PREF_IS_SCHEDULED).apply();
// Return false so that the failure will be logged.
return false;
}
return true;
}
@CalledByNative
private boolean unschedule() {
if (!mGCMEnabled) return false;
Log.i(TAG, "Unscheduling");
return schedule(0, 0);
}
public static boolean shouldRescheduleTasksOnUpgrade() {
// Reschedule the periodic tasks if they were enabled previously.
return ContextUtils.getAppSharedPreferences().getBoolean(PREF_IS_SCHEDULED, false);
}
}