// 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;
import android.content.Context;
import android.os.Bundle;
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.GcmTaskService;
import com.google.android.gms.gcm.TaskParams;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.chrome.browser.download.DownloadResumptionScheduler;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
import org.chromium.chrome.browser.ntp.snippets.SnippetsLauncher;
import org.chromium.chrome.browser.offlinepages.BackgroundOfflinerTask;
import org.chromium.chrome.browser.offlinepages.BackgroundSchedulerProcessorImpl;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.precache.PrecacheController;
import org.chromium.chrome.browser.precache.PrecacheUMA;
/**
* {@link ChromeBackgroundService} is scheduled through the {@link GcmNetworkManager} when the
* browser needs to be launched for scheduled tasks, or in response to changing network or power
* conditions.
*
* If HOLD_WAKELOCK is set to true in a bundle in the task params, then the ChromeBackgroundService
* will wait until the task reports done before returning control to the {@link GcmNetworkManager}.
* This both guarantees that the wakelock keeps chrome awake and that the GcmNetworkManager does not
* start another task in place of the one just started. The GcmNetworkManager can start more than
* one task concurrently, thought, so it does not guarantee that a different task won't start.
*/
public class ChromeBackgroundService extends GcmTaskService {
private static final String TAG = "BackgroundService";
/** Bundle key to use to specify we should block the GcmNetworkManager thread until the task on
* the UI thread is done before returning to the GcmNetworkManager.
*/
public static final String HOLD_WAKELOCK = "HoldWakelock";
private static final int WAKELOCK_TIMEOUT_SECONDS = 4 * 60;
private BackgroundOfflinerTask mBackgroundOfflinerTask;
@Override
@VisibleForTesting
public int onRunTask(final TaskParams params) {
final String taskTag = params.getTag();
Log.i(TAG, "[" + taskTag + "] Woken up at " + new java.util.Date().toString());
final ChromeBackgroundServiceWaiter waiter = getWaiterIfNeeded(params.getExtras());
final Context context = this;
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
switch (taskTag) {
case BackgroundSyncLauncher.TASK_TAG:
handleBackgroundSyncEvent(context, taskTag);
break;
case OfflinePageUtils.TASK_TAG:
handleOfflinePageBackgroundLoad(
context, params.getExtras(), waiter, taskTag);
break;
case SnippetsLauncher.TASK_TAG_WIFI:
case SnippetsLauncher.TASK_TAG_FALLBACK:
handleFetchSnippets(context, taskTag);
break;
case PrecacheController.PERIODIC_TASK_TAG:
case PrecacheController.CONTINUATION_TASK_TAG:
handlePrecache(context, taskTag);
break;
case DownloadResumptionScheduler.TASK_TAG:
DownloadResumptionScheduler.getDownloadResumptionScheduler(
context.getApplicationContext()).handleDownloadResumption();
break;
default:
Log.i(TAG, "Unknown task tag " + taskTag);
break;
}
}
});
// If needed, block the GcmNetworkManager thread until the UI thread has finished its work.
waitForTaskIfNeeded(waiter);
return GcmNetworkManager.RESULT_SUCCESS;
}
private void handleBackgroundSyncEvent(Context context, String tag) {
if (!BackgroundSyncLauncher.hasInstance()) {
// Start the browser. The browser's BackgroundSyncManager (for the active profile) will
// start, check the network, and run any necessary sync events. This task runs with a
// wake lock, but has a three minute timeout, so we need to start the browser in its
// own task.
// TODO(jkarlin): Protect the browser sync event with a wake lock.
// See crbug.com/486020.
launchBrowser(context, tag);
}
}
private void handleFetchSnippets(Context context, String tag) {
if (!SnippetsLauncher.hasInstance()) {
launchBrowser(context, tag);
}
fetchSnippets();
}
@VisibleForTesting
protected void fetchSnippets() {
// Do not force regular background fetches.
SnippetsBridge.fetchSnippets(/*forceRequest=*/false);
}
@VisibleForTesting
protected void rescheduleFetching() {
SnippetsBridge.rescheduleFetching();
}
private void handlePrecache(Context context, String tag) {
if (!hasPrecacheInstance()) {
launchBrowser(context, tag);
}
precache(context, tag);
}
@VisibleForTesting
protected boolean hasPrecacheInstance() {
return PrecacheController.hasInstance();
}
@VisibleForTesting
protected void precache(Context context, String tag) {
PrecacheController.get(context).precache(tag);
}
private void handleOfflinePageBackgroundLoad(
Context context, Bundle bundle, ChromeBackgroundServiceWaiter waiter, String tag) {
if (!LibraryLoader.isInitialized()) {
launchBrowser(context, tag);
}
// Call BackgroundTask, provide context.
if (mBackgroundOfflinerTask == null) {
mBackgroundOfflinerTask =
new BackgroundOfflinerTask(new BackgroundSchedulerProcessorImpl());
}
mBackgroundOfflinerTask.startBackgroundRequests(context, bundle, waiter);
// TODO(petewil) if processBackgroundRequest returns false, return RESTART_RESCHEDULE
// to the GcmNetworkManager
}
/**
* If the bundle contains the special HOLD_WAKELOCK key set to true, then we create a
* CountDownLatch for use later in the wait step, and set its initial count to one.
*/
@VisibleForTesting
public ChromeBackgroundServiceWaiter getWaiterIfNeeded(Bundle bundle) {
// If wait_needed is set to true, wait.
if (bundle != null && bundle.getBoolean(HOLD_WAKELOCK, false)) {
return new ChromeBackgroundServiceWaiter(WAKELOCK_TIMEOUT_SECONDS);
}
return null;
}
/**
* Some tasks need to block the GcmNetworkManager thread (and thus hold the wake lock) until the
* task is done. If we have a waiter, then start waiting.
*/
@VisibleForTesting
public void waitForTaskIfNeeded(ChromeBackgroundServiceWaiter waiter) {
if (waiter != null) {
// Block current thread until the onWaitDone method is called, or a timeout occurs.
waiter.startWaiting();
}
}
@VisibleForTesting
@SuppressFBWarnings("DM_EXIT")
protected void launchBrowser(Context context, String tag) {
Log.i(TAG, "Launching browser");
try {
ChromeBrowserInitializer.getInstance(this).handleSynchronousStartup();
} catch (ProcessInitException e) {
Log.e(TAG, "ProcessInitException while starting the browser process");
switch (tag) {
case PrecacheController.PERIODIC_TASK_TAG:
case PrecacheController.CONTINUATION_TASK_TAG:
// Record the failure persistently, and upload to UMA when the library
// successfully loads next time.
PrecacheUMA.record(PrecacheUMA.Event.PRECACHE_TASK_LOAD_LIBRARY_FAIL);
break;
default:
break;
}
// Since the library failed to initialize nothing in the application
// can work, so kill the whole application not just the activity.
System.exit(-1);
}
}
@VisibleForTesting
protected void rescheduleBackgroundSyncTasksOnUpgrade() {
BackgroundSyncLauncher.rescheduleTasksOnUpgrade(this);
}
@VisibleForTesting
protected void reschedulePrecacheTasksOnUpgrade() {
PrecacheController.rescheduleTasksOnUpgrade(this);
}
private void rescheduleSnippetsTasksOnUpgrade() {
if (SnippetsLauncher.shouldRescheduleTasksOnUpgrade()) {
if (!SnippetsLauncher.hasInstance()) {
launchBrowser(this, /*tag=*/""); // The |tag| doesn't matter here.
}
rescheduleFetching();
}
}
@Override
public void onInitializeTasks() {
rescheduleBackgroundSyncTasksOnUpgrade();
reschedulePrecacheTasksOnUpgrade();
rescheduleSnippetsTasksOnUpgrade();
}
}