Commit 5204eb18 authored by Paul Miller's avatar Paul Miller Committed by Commit Bot

WebView: Read and write variations seeds

Chrome stores the seed in preferences, but WebView doesn't persist preferences.
WebView must save save seeds separately. Serialize with protos, and add
aw_variations_seed.proto to mirror the fields in VariationsSeedFetcher.SeedInfo.
Since writing might not complete successfully, the file format must make
truncation unambiguous. The proto wire format is such that truncation in the
middle of a field will be detected, but truncation between fields is permitted.
But by requiring in code that all SeedInfo fields are present, any truncated
seed file will fail to load.

BUG=733857

Change-Id: I7f8d787afed83019c2d891aef419cdf7d593b71c
Reviewed-on: https://chromium-review.googlesource.com/1033823
Commit-Queue: Paul Miller <paulmiller@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#555281}
parent 108b8b22
......@@ -932,6 +932,7 @@ android_library("android_webview_java") {
android_library("android_webview_variations_utils_java") {
java_files = [ "java/src/org/chromium/android_webview/VariationsUtils.java" ]
deps = [
"//android_webview/proto:aw_variations_seed_proto_java",
"//components/variations/android:variations_java",
]
}
......
......@@ -6,6 +6,10 @@ package org.chromium.android_webview;
import android.support.annotation.Nullable;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.chromium.android_webview.proto.AwVariationsSeedOuterClass.AwVariationsSeed;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo;
......@@ -82,21 +86,34 @@ public class VariationsUtils {
}
}
// Returns null in case of incomplete/corrupt/missing seed.
// Silently returns null in case of incomplete/corrupt/missing seed, which is expected in case
// of an incomplete downoad or copy. Other IO problems are actual errors, and are logged.
@Nullable
public static SeedInfo readSeedFile(File inFile) {
// Read and discard the seed file, then return a mock seed. TODO(paulmiller): Return the
// actual seed, once seed downloading and serialization are implemented.
FileInputStream in = null;
try {
in = new FileInputStream(inFile);
byte[] data = new byte[1024];
while (in.read(data) > 0) {}
SeedInfo seed = new SeedInfo();
// Fill in a mock date so that seed.parseDate() doesn't crash.
seed.date = "Thu, 01 Jan 1970 12:34:56 GMT";
return seed;
AwVariationsSeed proto = null;
try {
proto = AwVariationsSeed.parseFrom(in);
} catch (InvalidProtocolBufferException e) {
return null;
}
if (!proto.hasSignature() ||
!proto.hasCountry() ||
!proto.hasDate() ||
!proto.hasIsGzipCompressed() ||
!proto.hasSeedData()) return null;
SeedInfo info = new SeedInfo();
info.signature = proto.getSignature();
info.country = proto.getCountry();
info.date = proto.getDate();
info.isGzipCompressed = proto.getIsGzipCompressed();
info.seedData = proto.getSeedData().toByteArray();
return info;
} catch (IOException e) {
Log.e(TAG, "Failed reading seed file \"" + inFile + "\": " + e.getMessage());
return null;
......@@ -107,11 +124,15 @@ public class VariationsUtils {
// Returns true on success. "out" will always be closed, regardless of success.
public static boolean writeSeed(FileOutputStream out, SeedInfo info) {
// Write 3 KB of zeros (the current size of an actual WebView seed). TODO(paulmiller): Write
// the actual seed, once seed downloading and serialization are implemented.
byte[] zeros = new byte[3 * 1024];
try {
out.write(zeros);
AwVariationsSeed proto = AwVariationsSeed.newBuilder()
.setSignature(info.signature)
.setCountry(info.country)
.setDate(info.date)
.setIsGzipCompressed(info.isGzipCompressed)
.setSeedData(ByteString.copyFrom(info.seedData))
.build();
proto.writeTo(out);
return true;
} catch (IOException e) {
Log.e(TAG, "Failed writing seed file: " + e.getMessage());
......
......@@ -11,6 +11,7 @@ import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import org.chromium.android_webview.VariationsUtils;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo;
/**
* VariationsSeedServer is a bound service that shares the Variations seed with all the WebViews
......@@ -21,7 +22,14 @@ public class VariationsSeedServer extends Service {
private final IVariationsSeedServer.Stub mBinder = new IVariationsSeedServer.Stub() {
@Override
public void getSeed(ParcelFileDescriptor newSeedFile, long oldSeedDate) {
VariationsUtils.writeSeed(new AutoCloseOutputStream(newSeedFile), null);
// Write a minimally correct seed so that readSeedFile() can read it without error.
// TODO(paulmiller): Write the actual seed, once seed downloading is implemented.
SeedInfo mock = new SeedInfo();
mock.signature = "";
mock.country = "";
mock.date = "Sun, 23 Jun 1912 00:00:00 GMT";
mock.seedData = new byte[0];
VariationsUtils.writeSeed(new AutoCloseOutputStream(newSeedFile), mock);
}
};
......
......@@ -20,6 +20,7 @@ import org.chromium.android_webview.VariationsSeedLoader;
import org.chromium.android_webview.VariationsUtils;
import org.chromium.android_webview.services.ServiceInit;
import org.chromium.android_webview.test.services.MockVariationsSeedServer;
import org.chromium.android_webview.test.util.VariationsTestUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseJUnit4ClassRunner;
......@@ -85,20 +86,6 @@ public class VariationsSeedLoaderTest {
}
}
private static void deleteIfExists(File file) throws IOException {
if (file.exists()) {
if (!file.delete()) {
throw new IOException("Failed to delete " + file);
}
}
}
private static void deleteSeeds() throws IOException {
deleteIfExists(VariationsUtils.getSeedFile());
deleteIfExists(VariationsUtils.getNewSeedFile());
deleteIfExists(VariationsUtils.getStampFile());
}
private Handler mMainHandler;
// Create a TestLoader, run it on the UI thread, and block until it's finished. The return value
......@@ -131,13 +118,13 @@ public class VariationsSeedLoaderTest {
.getTargetContext().getApplicationContext());
ServiceInit.setPrivateDataDirectorySuffix();
RecordHistogram.setDisabledForTests(true);
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
@After
public void tearDown() throws IOException {
RecordHistogram.setDisabledForTests(false);
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
// Test the case that:
......@@ -152,7 +139,7 @@ public class VariationsSeedLoaderTest {
// Since there was no seed, another seed should be requested.
Assert.assertTrue("No seed requested", seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -163,8 +150,9 @@ public class VariationsSeedLoaderTest {
@MediumTest
public void testHaveFreshSeed() throws Exception {
try {
Assert.assertTrue("Seed file already exists",
VariationsUtils.getSeedFile().createNewFile());
File oldFile = VariationsUtils.getSeedFile();
Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
VariationsTestUtils.writeMockSeed(oldFile);
boolean seedRequested = runTestLoaderBlocking();
......@@ -172,7 +160,7 @@ public class VariationsSeedLoaderTest {
Assert.assertFalse("New seed was requested when it should not have been",
seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -185,6 +173,7 @@ public class VariationsSeedLoaderTest {
try {
File oldFile = VariationsUtils.getSeedFile();
Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
VariationsTestUtils.writeMockSeed(oldFile);
oldFile.setLastModified(0);
boolean seedRequested = runTestLoaderBlocking();
......@@ -192,7 +181,7 @@ public class VariationsSeedLoaderTest {
// Since the seed was expired, another seed should be requested.
Assert.assertTrue("No seed requested", seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -206,6 +195,7 @@ public class VariationsSeedLoaderTest {
File oldFile = VariationsUtils.getSeedFile();
File newFile = VariationsUtils.getNewSeedFile();
Assert.assertTrue("New seed file already exists", newFile.createNewFile());
VariationsTestUtils.writeMockSeed(newFile);
boolean seedRequested = runTestLoaderBlocking();
......@@ -217,7 +207,7 @@ public class VariationsSeedLoaderTest {
Assert.assertFalse("New seed was requested when it should not have been",
seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -231,6 +221,7 @@ public class VariationsSeedLoaderTest {
File oldFile = VariationsUtils.getSeedFile();
File newFile = VariationsUtils.getNewSeedFile();
Assert.assertTrue("Seed file already exists", newFile.createNewFile());
VariationsTestUtils.writeMockSeed(newFile);
newFile.setLastModified(0);
boolean seedRequested = runTestLoaderBlocking();
......@@ -244,7 +235,7 @@ public class VariationsSeedLoaderTest {
// Since the "new" seed was expired, another seed should be requested.
Assert.assertTrue("No seed requested", seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -257,10 +248,12 @@ public class VariationsSeedLoaderTest {
try {
File oldFile = VariationsUtils.getSeedFile();
Assert.assertTrue("Old seed file already exists", oldFile.createNewFile());
VariationsTestUtils.writeMockSeed(oldFile);
oldFile.setLastModified(0);
File newFile = VariationsUtils.getNewSeedFile();
Assert.assertTrue("New seed file already exists", newFile.createNewFile());
VariationsTestUtils.writeMockSeed(newFile);
newFile.setLastModified(TimeUnit.DAYS.toMillis(1));
boolean seedRequested = runTestLoaderBlocking();
......@@ -274,7 +267,7 @@ public class VariationsSeedLoaderTest {
// Since the "new" seed was expired, another seed should be requested.
Assert.assertTrue("No seed requested", seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
......@@ -293,7 +286,7 @@ public class VariationsSeedLoaderTest {
Assert.assertFalse("New seed was requested when it should not have been",
seedRequested);
} finally {
deleteSeeds();
VariationsTestUtils.deleteSeeds();
}
}
}
// 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.test;
import android.support.test.filters.MediumTest;
import com.google.protobuf.ByteString;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.VariationsUtils;
import org.chromium.android_webview.proto.AwVariationsSeedOuterClass.AwVariationsSeed;
import org.chromium.android_webview.test.util.VariationsTestUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.parameter.SkipCommandLineParameterization;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Test reading and writing variations seeds.
*/
@RunWith(BaseJUnit4ClassRunner.class)
@SkipCommandLineParameterization
public class VariationsUtilsTest {
@Test
@MediumTest
public void testWriteAndReadSeed() throws IOException {
File file = null;
try {
file = File.createTempFile("seed", null, null);
VariationsTestUtils.writeMockSeed(file);
SeedInfo readSeed = VariationsUtils.readSeedFile(file);
VariationsTestUtils.assertSeedsEqual(VariationsTestUtils.createMockSeed(), readSeed);
} finally {
if (file != null) file.delete();
}
}
// Test reading a seed that has some but not all fields, which should fail.
@Test
@MediumTest
public void testReadSeedMissingFields() throws IOException {
File file = null;
try {
file = File.createTempFile("seed", null, null);
FileOutputStream stream = null;
try {
// Create a seed that's missing some fields.
stream = new FileOutputStream(file);
SeedInfo info = VariationsTestUtils.createMockSeed();
AwVariationsSeed proto = AwVariationsSeed.newBuilder()
.setSignature(info.signature)
.setCountry(info.country)
.setDate(info.date)
.build();
proto.writeTo(stream);
Assert.assertNull("Seed with missing fields should've failed to load.",
VariationsUtils.readSeedFile(file));
} finally {
if (stream != null) stream.close();
}
} finally {
if (file != null) file.delete();
}
}
// Test reading a seed that's been truncated at some arbitrary byte offsets, which should fail.
@Test
@MediumTest
public void testReadTruncatedSeed() throws IOException {
// Create a complete, serialized seed.
SeedInfo info = VariationsTestUtils.createMockSeed();
AwVariationsSeed proto = AwVariationsSeed.newBuilder()
.setSignature(info.signature)
.setCountry(info.country)
.setDate(info.date)
.setIsGzipCompressed(info.isGzipCompressed)
.setSeedData(ByteString.copyFrom(info.seedData))
.build();
byte[] protoBytes = proto.toByteArray();
// Sanity check: protoBytes is at least as long as the seedData field.
Assert.assertTrue(protoBytes.length >= info.seedData.length);
// Create slices of that seed in 10-byte increments.
for (int offset = 10; offset < protoBytes.length; offset += 10) {
byte[] slice = Arrays.copyOfRange(protoBytes, 0, offset);
File file = null;
try {
file = File.createTempFile("seed", null, null);
FileOutputStream stream = null;
try {
stream = new FileOutputStream(file);
stream.write(slice);
} finally {
if (stream != null) stream.close();
}
// Reading each truncated seed should fail.
Assert.assertNull("Seed truncated from " + protoBytes.length + " to " + offset +
" bytes should've failed to load.", VariationsUtils.readSeedFile(file));
} finally {
if (file != null) file.delete();
}
}
}
}
// 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.test.util;
import org.junit.Assert;
import org.chromium.android_webview.VariationsUtils;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Provide a canonical mock seed for tests.
*/
public class VariationsTestUtils {
public static void assertSeedsEqual(SeedInfo expected, SeedInfo actual) {
Assert.assertTrue("Expected " + expected + " but got " + actual,
seedsEqual(expected, actual));
}
public static boolean seedsEqual(SeedInfo a, SeedInfo b) {
return strEqual(a.signature, b.signature) &&
strEqual(a.country, b.country) &&
strEqual(a.date, b.date) &&
(a.isGzipCompressed == b.isGzipCompressed) &&
Arrays.equals(a.seedData, b.seedData);
}
private static boolean strEqual(String a, String b) {
return a == null ? b == null : a.equals(b);
}
public static SeedInfo createMockSeed() {
SeedInfo seed = new SeedInfo();
seed.seedData = "bogus seed data".getBytes();
seed.signature = "bogus seed signature";
seed.country = "GB";
seed.date = "Sun, 23 Jun 1912 00:00:00 GMT";
return seed;
}
public static void writeMockSeed(File dest) throws IOException {
FileOutputStream stream = null;
try {
stream = new FileOutputStream(dest);
VariationsUtils.writeSeed(stream, createMockSeed());
} finally {
if (stream != null) stream.close();
}
}
public static void deleteSeeds() throws IOException {
deleteIfExists(VariationsUtils.getSeedFile());
deleteIfExists(VariationsUtils.getNewSeedFile());
deleteIfExists(VariationsUtils.getStampFile());
}
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException("Failed to delete " + file);
}
}
}
# 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.
import("//build/config/android/rules.gni")
proto_java_library("aw_variations_seed_proto_java") {
proto_path = "."
sources = [ "aw_variations_seed.proto" ]
}
// 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.
syntax = "proto2";
package org.chromium.android_webview.proto;
option optimize_for = LITE_RUNTIME; // TODO(crbug/800281): Remove this after proto 4.0
option java_package = "org.chromium.android_webview.proto";
// WebView uses AwVariationsSeed to serialize a downloaded seed, along with the
// required HTTP header metadata, to a file. WebView must save seeds this way
// because it doesn't persist preferences.
//
// Next tag: 6
message AwVariationsSeed {
// Whether seed_data is compressed. Comes from HTTP header "X-Seed-Signature".
optional string signature = 1;
// 2-letter country code. Comes from HTTP header "X-Country".
optional string country = 2;
// Date the seed was downloaded. Comes from HTTP header "Date"; see RFC 2616,
// sections 3.3.1 and 14.18 for the format.
optional string date = 3;
// Whether seed_data is compressed. Comes from HTTP header "IM".
optional bool is_gzip_compressed = 4;
// The download body, itself a serialized VariationsSeed proto.
optional bytes seed_data = 5;
}
......@@ -228,6 +228,7 @@ instrumentation_test_apk("webview_instrumentation_test_apk") {
"../javatests/src/org/chromium/android_webview/test/TestAwServiceWorkerClient.java",
"../javatests/src/org/chromium/android_webview/test/UserAgentTest.java",
"../javatests/src/org/chromium/android_webview/test/VariationsSeedLoaderTest.java",
"../javatests/src/org/chromium/android_webview/test/VariationsUtilsTest.java",
"../javatests/src/org/chromium/android_webview/test/VisualStateTest.java",
"../javatests/src/org/chromium/android_webview/test/WebKitHitTestTest.java",
"../javatests/src/org/chromium/android_webview/test/WebViewAsynchronousFindApisTest.java",
......@@ -247,6 +248,7 @@ instrumentation_test_apk("webview_instrumentation_test_apk") {
"../javatests/src/org/chromium/android_webview/test/util/ImagePageGenerator.java",
"../javatests/src/org/chromium/android_webview/test/util/JSUtils.java",
"../javatests/src/org/chromium/android_webview/test/util/JavascriptEventObserver.java",
"../javatests/src/org/chromium/android_webview/test/util/VariationsTestUtils.java",
"../javatests/src/org/chromium/android_webview/test/util/VideoSurfaceViewUtils.java",
"../javatests/src/org/chromium/android_webview/test/util/VideoTestUtil.java",
"../javatests/src/org/chromium/android_webview/test/util/VideoTestWebServer.java",
......
......@@ -24,6 +24,7 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
......@@ -122,6 +123,7 @@ public class VariationsSeedFetcher {
* Object holding the seed data and related fields retrieved from HTTP headers.
*/
public static class SeedInfo {
// If you add fields, see VariationsTestUtils.
public String signature;
public String country;
public String date;
......@@ -134,8 +136,16 @@ public class VariationsSeedFetcher {
// instantiate a new one for each call.
return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US).parse(date);
}
@Override
public String toString() {
return "SeedInfo{signature=\"" + signature + "\" country=\"" + country
+ "\" date=\"" + date + " isGzipCompressed=" + isGzipCompressed
+ " seedData=" + Arrays.toString(seedData);
}
}
/**
* Fetch the first run variations seed.
* @param restrictMode The restrict mode parameter to pass to the server via a URL param.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment