// Copyright 2015 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;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.StrictMode;
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.OneoffTask;
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.metrics.RecordHistogram;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalauth.UserRecoverableErrorHandler;
/**
* The {@link BackgroundSyncLauncher} singleton is created and owned by the C++ browser. It
* registers interest in waking up the browser the next time the device goes online after the
* browser closes via the {@link #setLaunchWhenNextOnline} method.
*
* Thread model: This class is to be run on the UI thread only.
*/
public class BackgroundSyncLauncher {
private static final String TAG = "BgSyncLauncher";
public static final String TASK_TAG = "BackgroundSync Event";
static final String PREF_BACKGROUND_SYNC_LAUNCH_NEXT_ONLINE = "bgsync_launch_next_online";
// The instance of BackgroundSyncLauncher currently owned by a C++
// BackgroundSyncLauncherAndroid, if any. If it is non-null then the browser is running.
private static BackgroundSyncLauncher sInstance;
private GcmNetworkManager mScheduler;
/**
* Disables the automatic use of the GCMNetworkManager. When disabled, the methods which
* interact with GCM can still be used, but will not be called automatically on creation, or by
* {@link #launchBrowserIfStopped}.
*
* Automatic GCM use is disabled by tests, and also by this class if it is determined on
* creation that the installed Play Services library is out of date.
*/
private static boolean sGCMEnabled = true;
/**
* Create a BackgroundSyncLauncher object, which is owned by C++.
* @param context The app context.
*/
@VisibleForTesting
@CalledByNative
protected static BackgroundSyncLauncher create(Context context) {
if (sInstance != null) {
throw new IllegalStateException("Already instantiated");
}
sInstance = new BackgroundSyncLauncher(context);
return sInstance;
}
/**
* Called when the C++ counterpart is deleted.
*/
@VisibleForTesting
@CalledByNative
protected void destroy() {
assert sInstance == this;
sInstance = null;
}
/**
* Callback for {@link #shouldLaunchBrowserIfStopped}. The run method is invoked on the UI
* thread.
*/
public static interface ShouldLaunchCallback { public void run(Boolean shouldLaunch); }
/**
* Returns whether the browser should be launched when the device next goes online.
* This is set by C++ and reset to false each time {@link BackgroundSyncLauncher}'s singleton is
* created (the native browser is started). This call is asynchronous and will run the callback
* on the UI thread when complete.
* @param context The application context.
* @param sharedPreferences The shared preferences.
*/
protected static void shouldLaunchBrowserIfStopped(
final Context context, final ShouldLaunchCallback callback) {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
return prefs.getBoolean(PREF_BACKGROUND_SYNC_LAUNCH_NEXT_ONLINE, false);
}
@Override
protected void onPostExecute(Boolean shouldLaunch) {
callback.run(shouldLaunch);
}
}.execute();
}
/**
* Manages the scheduled tasks which re-launch the browser when the device next goes online
* after at least {@code minDelayMs} milliseconds.
* This method is called by C++ as background sync registrations are added and removed. When the
* {@link BackgroundSyncLauncher} singleton is created (on browser start), this is called to
* remove any pre-existing scheduled tasks.
* @param context The application context.
* @param shouldLaunch Whether or not to launch the browser in the background.
* @param minDelayMs The minimum time to wait before checking on the browser process.
*/
@VisibleForTesting
@CalledByNative
protected void launchBrowserIfStopped(
final Context context, final boolean shouldLaunch, final long minDelayMs) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
prefs.edit()
.putBoolean(PREF_BACKGROUND_SYNC_LAUNCH_NEXT_ONLINE, shouldLaunch)
.apply();
return null;
}
@Override
protected void onPostExecute(Void params) {
if (sGCMEnabled) {
if (shouldLaunch) {
RecordHistogram.recordBooleanHistogram(
"BackgroundSync.LaunchTask.ScheduleSuccess",
scheduleLaunchTask(context, mScheduler, minDelayMs));
} else {
RecordHistogram.recordBooleanHistogram(
"BackgroundSync.LaunchTask.CancelSuccess",
removeScheduledTasks(mScheduler));
}
}
}
}.execute();
}
/**
* Returns true if the native browser has started and created an instance of {@link
* BackgroundSyncLauncher}.
*/
protected static boolean hasInstance() {
return sInstance != null;
}
protected BackgroundSyncLauncher(Context context) {
mScheduler = GcmNetworkManager.getInstance(context);
launchBrowserIfStopped(context, false, 0);
}
private static boolean canUseGooglePlayServices(Context context) {
return ExternalAuthUtils.getInstance().canUseGooglePlayServices(
context, new UserRecoverableErrorHandler.Silent());
}
/**
* Returns true if the Background Sync Manager should be automatically disabled on startup.
* This is currently only the case if Play Services is not up to date, since any sync attempts
* which fail cannot be reregistered. Better to wait until Play Services is updated before
* attempting them.
*
* @param context The application context.
*/
@CalledByNative
private static boolean shouldDisableBackgroundSync(Context context) {
// Check to see if Play Services is up to date, and disable GCM if not.
// This will not automatically set {@link sGCMEnabled} to true, in case it has been
// disabled in tests.
if (sGCMEnabled) {
boolean isAvailable = true;
if (!canUseGooglePlayServices(context)) {
setGCMEnabled(false);
Log.i(TAG, "Disabling Background Sync because Play Services is not up to date.");
isAvailable = false;
}
RecordHistogram.recordBooleanHistogram(
"BackgroundSync.LaunchTask.PlayServicesAvailable", isAvailable);
}
return !sGCMEnabled;
}
private static boolean scheduleLaunchTask(
Context context, GcmNetworkManager scheduler, long minDelayMs) {
// 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.
final long minDelaySecs = minDelayMs / 1000;
OneoffTask oneoff = new OneoffTask.Builder()
.setService(ChromeBackgroundService.class)
.setTag(TASK_TAG)
// We have to set a non-zero execution window here
.setExecutionWindow(minDelaySecs, minDelaySecs + 1)
.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
.setPersisted(true)
.setUpdateCurrent(true)
.build();
try {
scheduler.schedule(oneoff);
} catch (IllegalArgumentException e) {
// Disable GCM for the remainder of this session.
setGCMEnabled(false);
// Return false so that the failure will be logged.
return false;
}
return true;
}
private static boolean removeScheduledTasks(GcmNetworkManager scheduler) {
// Third-party code causes broadcast to touch disk. http://crbug.com/614679
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
scheduler.cancelTask(TASK_TAG, ChromeBackgroundService.class);
} catch (IllegalArgumentException e) {
// This occurs when BackgroundSyncLauncherService is not found in the application
// manifest. This should not happen in code that reaches here, but has been seen in
// the past. See https://crbug.com/548314
// Disable GCM for the remainder of this session.
setGCMEnabled(false);
// Return false so that the failure will be logged.
return false;
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
return true;
}
/**
* Reschedule any required background sync tasks, if they have been removed due to an
* application upgrade.
*
* This method checks the saved preferences, and reschedules the sync tasks as appropriate
* to match the preferences.
* This method is static so that it can be run without actually instantiating a
* BackgroundSyncLauncher.
*/
protected static void rescheduleTasksOnUpgrade(final Context context) {
final GcmNetworkManager scheduler = GcmNetworkManager.getInstance(context);
BackgroundSyncLauncher.ShouldLaunchCallback callback =
new BackgroundSyncLauncher.ShouldLaunchCallback() {
@Override
public void run(Boolean shouldLaunch) {
if (shouldLaunch) {
// It's unclear what time the sync event was supposed to fire, so fire
// without delay and let the browser reschedule if necessary.
// TODO(iclelland): If this fails, report the failure via UMA (not now,
// since the browser is not running, but on next startup.)
scheduleLaunchTask(context, scheduler, 0);
}
}
};
BackgroundSyncLauncher.shouldLaunchBrowserIfStopped(context, callback);
}
@VisibleForTesting
static void setGCMEnabled(boolean enabled) {
sGCMEnabled = enabled;
}
}