blob: 4638653df417bc62c1c83a5252e5fbded6fb0ee0 [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.android_webview.services;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import androidx.annotation.VisibleForTesting;
import org.chromium.android_webview.common.variations.VariationsUtils;
import org.chromium.base.Log;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.text.ParseException;
import java.util.Date;
/**
* VariationsSeedHolder is a singleton which manages the local copy of the variations seed - both
* the file and the SeedInfo object containing the data loaded from that file - in WebView's
* variations service process. VariationsSeedHolder is used by VariationsSeedServer (to serve the
* seed to apps) and AwVariationsSeedFetcher (to update the seed). VariationsSeedHolder guards
* concurrent access to the seed by serializing all operations onto mSeedThread.
* VariationsSeedHolder is not meant to be used outside the variations service.
*/
@VisibleForTesting
public class VariationsSeedHolder {
private static final String TAG = "VariationsSeedHolder";
private static final VariationsSeedHolder sInstance = new VariationsSeedHolder();
private static void writeSeedWithoutClosing(SeedInfo seed, ParcelFileDescriptor destination) {
// writeSeed() will close "out", but closing "out" will not close "destination".
FileOutputStream out = new FileOutputStream(destination.getFileDescriptor());
VariationsUtils.writeSeed(out, seed);
}
// Use mSeedHandler to send tasks to mSeedThread.
private final HandlerThread mSeedThread;
private final Handler mSeedHandler;
// mSeed is the service's copy of the seed. It should only be used on mSeedThread.
private SeedInfo mSeed;
// Set true when we fail to load a seed, to prevent future loads until SeedUpdater runs.
private boolean mFailedReadingSeed;
// A Runnable which handles an individual request for the seed. Must run on mSeedThread.
private class SeedWriter implements Runnable {
private ParcelFileDescriptor mDestination;
// mDestinationDate is the date field of the requester's current seed, in milliseconds since
// epoch, or Long.MIN_VALUE if the requester has no seed. Only write our seed if our seed is
// newer than mDestinationDate.
private long mDestinationDate;
public SeedWriter(ParcelFileDescriptor destination, long date) {
mDestination = destination;
mDestinationDate = date;
}
@Override
public void run() {
assert Thread.currentThread() == mSeedThread;
try {
scheduleFetchIfNeeded();
// Load the seed if necessary.
if (VariationsSeedHolder.this.mSeed == null && !mFailedReadingSeed) {
VariationsSeedHolder.this.mSeed =
VariationsUtils.readSeedFile(VariationsUtils.getSeedFile());
mFailedReadingSeed = VariationsSeedHolder.this.mSeed == null;
}
// If there's no seed available, the app will have to request again.
if (VariationsSeedHolder.this.mSeed == null) return;
Date loadedSeedDate;
try {
loadedSeedDate = VariationsSeedHolder.this.mSeed.parseDate();
} catch (ParseException e) {
// Should never happen, as date was alread verified by readSeedFile.
assert false;
return;
}
if (mDestinationDate < loadedSeedDate.getTime()) {
writeSeedWithoutClosing(VariationsSeedHolder.this.mSeed, mDestination);
}
} finally {
VariationsUtils.closeSafely(mDestination);
onWriteFinished();
}
}
}
// A Runnable which updates both mSeed and the service's seed file. Must run on mSeedThread.
private class SeedUpdater implements Runnable {
private SeedInfo mNewSeed;
private Runnable mOnFinished;
public SeedUpdater(SeedInfo newSeed, Runnable onFinished) {
mNewSeed = newSeed;
mOnFinished = onFinished;
}
@Override
public void run() {
assert Thread.currentThread() == mSeedThread;
try {
VariationsSeedHolder.this.mSeed = mNewSeed;
// Update the seed file.
File newSeedFile = VariationsUtils.getNewSeedFile();
FileOutputStream out;
try {
out = new FileOutputStream(newSeedFile);
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to open seed file " + newSeedFile + " for update");
return;
}
if (!VariationsUtils.writeSeed(out, VariationsSeedHolder.this.mSeed)) {
Log.e(TAG, "Failed to write seed file " + newSeedFile + " for update");
return;
}
VariationsUtils.replaceOldWithNewSeed();
mFailedReadingSeed = false;
} finally {
mOnFinished.run();
}
}
}
@VisibleForTesting
protected VariationsSeedHolder() {
mSeedThread = new HandlerThread(/*name=*/"seed_holder");
mSeedThread.start();
mSeedHandler = new Handler(mSeedThread.getLooper());
}
/* package */ static VariationsSeedHolder getInstance() {
return sInstance;
}
@VisibleForTesting
public void writeSeedIfNewer(ParcelFileDescriptor destination, long date) {
mSeedHandler.post(new SeedWriter(destination, date));
}
@VisibleForTesting
public void updateSeed(SeedInfo newSeed, Runnable onFinished) {
mSeedHandler.post(new SeedUpdater(newSeed, onFinished));
}
@VisibleForTesting
public void scheduleFetchIfNeeded() {
AwVariationsSeedFetcher.scheduleIfNeeded();
}
// overridden by tests
public void onWriteFinished() {}
}