diff options
10 files changed, 711 insertions, 2 deletions
diff --git a/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java b/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java new file mode 100644 index 0000000..a57a8b0 --- /dev/null +++ b/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java @@ -0,0 +1,103 @@ +// Copyright (c) 2013 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.base.test.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.SharedPreferences; +import android.test.mock.MockContentResolver; +import android.test.mock.MockContext; + +import java.util.HashMap; +import java.util.Map; + +/** + * ContextWrapper that adds functionality for SharedPreferences and a way to set and retrieve flags. + */ +public class AdvancedMockContext extends ContextWrapper { + + private final MockContentResolver mMockContentResolver = new MockContentResolver(); + + private final Map<String, SharedPreferences> mSharedPreferences = + new HashMap<String, SharedPreferences>(); + + private final Map<String, Boolean> flags = new HashMap<String, Boolean>(); + + public AdvancedMockContext(Context base) { + super(base); + } + + public AdvancedMockContext() { + super(new MockContext()); + } + + @Override + public String getPackageName() { + return getBaseContext().getPackageName(); + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public ContentResolver getContentResolver() { + return mMockContentResolver; + } + + public MockContentResolver getMockContentResolver() { + return mMockContentResolver; + } + + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + synchronized (mSharedPreferences) { + if (!mSharedPreferences.containsKey(name)) { + // Auto-create shared preferences to mimic Android Context behavior + mSharedPreferences.put(name, new InMemorySharedPreferences()); + } + return mSharedPreferences.get(name); + } + } + + public void addSharedPreferences(String name, Map<String, Object> data) { + synchronized (mSharedPreferences) { + mSharedPreferences.put(name, new InMemorySharedPreferences(data)); + } + } + + public void setFlag(String key) { + flags.put(key, true); + } + + public void clearFlag(String key) { + flags.remove(key); + } + + public boolean isFlagSet(String key) { + return flags.containsKey(key) && flags.get(key); + } + + public static class MapBuilder { + + private final Map<String, Object> mData = new HashMap<String, Object>(); + + public static MapBuilder create() { + return new MapBuilder(); + } + + public MapBuilder add(String key, Object value) { + mData.put(key, value); + return this; + } + + public Map<String, Object> build() { + return mData; + } + + } +} diff --git a/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java b/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java new file mode 100644 index 0000000..e42562b --- /dev/null +++ b/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java @@ -0,0 +1,187 @@ +// Copyright (c) 2013 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.base.test.util; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of SharedPreferences that can be used in tests. + * <p/> + * It keeps all state in memory, and there is no difference between apply() and commit(). + */ +public class InMemorySharedPreferences implements SharedPreferences { + + private final Map<String, Object> mData; + + private final SharedPreferences.Editor mEditor = new InMemoryEditor(); + + public InMemorySharedPreferences() { + mData = new HashMap<String, Object>(); + } + + public InMemorySharedPreferences(Map<String, Object> data) { + mData = data; + } + + @Override + public Map<String, ?> getAll() { + return mData; + } + + @Override + public String getString(String key, String defValue) { + if (mData.containsKey(key)) { + return (String) mData.get(key); + } + return defValue; + } + + @SuppressWarnings("unchecked") + @Override + public Set<String> getStringSet(String key, Set<String> defValues) { + if (mData.containsKey(key)) { + return (Set<String>) mData.get(key); + } + return defValues; + } + + @Override + public int getInt(String key, int defValue) { + if (mData.containsKey(key)) { + return (Integer) mData.get(key); + } + return defValue; + } + + @Override + public long getLong(String key, long defValue) { + if (mData.containsKey(key)) { + return (Long) mData.get(key); + } + return defValue; + } + + @Override + public float getFloat(String key, float defValue) { + if (mData.containsKey(key)) { + return (Float) mData.get(key); + } + return defValue; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (mData.containsKey(key)) { + return (Boolean) mData.get(key); + } + return defValue; + } + + @Override + public boolean contains(String key) { + return mData.containsKey(key); + } + + @Override + public SharedPreferences.Editor edit() { + return mEditor; + } + + @Override + public void registerOnSharedPreferenceChangeListener( + SharedPreferences.OnSharedPreferenceChangeListener + listener) { + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + SharedPreferences.OnSharedPreferenceChangeListener listener) { + } + + private class InMemoryEditor implements SharedPreferences.Editor { + + private boolean mClearCalled; + + private final Map<String, Object> mChanges = new HashMap<String, Object>(); + + @Override + public SharedPreferences.Editor putString(String key, String value) { + mChanges.put(key, value); + return this; + } + + @Override + public SharedPreferences.Editor putStringSet(String key, Set<String> values) { + mChanges.put(key, values); + return this; + } + + @Override + public SharedPreferences.Editor putInt(String key, int value) { + mChanges.put(key, value); + return this; + } + + @Override + public SharedPreferences.Editor putLong(String key, long value) { + mChanges.put(key, value); + return this; + } + + @Override + public SharedPreferences.Editor putFloat(String key, float value) { + mChanges.put(key, value); + return this; + } + + @Override + public SharedPreferences.Editor putBoolean(String key, boolean value) { + mChanges.put(key, value); + return this; + } + + @Override + public SharedPreferences.Editor remove(String key) { + // Magic value for removes + mChanges.put(key, this); + return this; + } + + @Override + public SharedPreferences.Editor clear() { + mClearCalled = true; + return this; + } + + @Override + public boolean commit() { + apply(); + return true; + } + + @Override + public void apply() { + if (mClearCalled) { + mData.clear(); + } + for (Map.Entry<String, Object> entry : mChanges.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value == this) { + // Special value for removal + mData.remove(key); + } else { + mData.put(key, value); + } + } + } + + } + +} diff --git a/chrome/android/testshell/java/AndroidManifest.xml b/chrome/android/testshell/java/AndroidManifest.xml index 45f0e29..0da98141 100644 --- a/chrome/android/testshell/java/AndroidManifest.xml +++ b/chrome/android/testshell/java/AndroidManifest.xml @@ -60,6 +60,10 @@ android:permission="org.chromium.content_shell.permission.SANDBOX" android:isolatedProcess="true" android:exported="false" /> + + <!-- Name of the class implementing the invalidation client, for sync notifications. --> + <meta-data android:name="org.chromium.sync.notifier.IMPLEMENTING_CLASS_NAME" + android:value="org.chromium.sync.notifier.TEST_VALUE" /> </application> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" /> diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 6e0cb29..2579fe6 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -1072,6 +1072,7 @@ '../content/content.gyp:content_java', '../content/content.gyp:navigation_interception_java', '../content/content.gyp:web_contents_delegate_android_java', + '../sync/sync.gyp:sync_java', '../third_party/guava/guava.gyp:guava_javalib', '../ui/ui.gyp:ui_java', ], diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 3bca828..ee7dfe8 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -2810,11 +2810,12 @@ 'target_name': 'chromium_testshell_test_apk', 'type': 'none', 'dependencies': [ - '../base/base.gyp:base', - '../base/base.gyp:base_java_test_support', 'chrome_java', 'chromium_testshell_java', + '../base/base.gyp:base', + '../base/base.gyp:base_java_test_support', '../content/content.gyp:content_java_test_support', + '../sync/sync.gyp:sync_javatests', '../tools/android/forwarder/forwarder.gyp:forwarder', ], 'variables': { diff --git a/sync/android/OWNERS b/sync/android/OWNERS new file mode 100644 index 0000000..72643ec --- /dev/null +++ b/sync/android/OWNERS @@ -0,0 +1,3 @@ +nileshagrawal@chromium.org +nyquist@chromium.org +yfriedman@chromium.org diff --git a/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java b/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java new file mode 100644 index 0000000..4dbdab0 --- /dev/null +++ b/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java @@ -0,0 +1,46 @@ +// Copyright (c) 2013 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.sync.internal_api.pub.base; + +import com.google.ipc.invalidation.external.client.types.ObjectId; +import com.google.protos.ipc.invalidation.Types; + +/** + * The model types that are synced in Chrome for Android. + */ +public enum ModelType { + + /** + * A bookmark folder or a bookmark URL object. + */ + BOOKMARK("BOOKMARK"), + /** + * A typed_url folder or a typed_url object. + */ + TYPED_URL("TYPED_URL"), + /** + * An object representing a browser session or tab. + */ + SESSION("SESSION"); + + private final String mModelType; + + ModelType(String modelType) { + mModelType = modelType; + } + + public ObjectId toObjectId() { + return ObjectId.newInstance(Types.ObjectSource.Type.CHROME_SYNC.getNumber(), + mModelType.getBytes()); + } + + public static ModelType fromObjectId(ObjectId objectId) { + try { + return valueOf(new String(objectId.getName())); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java new file mode 100644 index 0000000..71ac7b5 --- /dev/null +++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java @@ -0,0 +1,179 @@ +// Copyright (c) 2013 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.sync.notifier; + +import android.accounts.Account; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; + +import org.chromium.sync.internal_api.pub.base.ModelType; + +import java.util.Set; + +/** + * Controller used to send start, stop, and registration-change commands to the invalidation + * client library used by Sync. + */ +public class InvalidationController { + /** + * Constants and utility methods to create the intents used to communicate between the + * controller and the invalidation client library. + */ + public static class IntentProtocol { + /** + * Action set on register intents. + */ + public static final String ACTION_REGISTER = + "org.chromium.sync.notifier.ACTION_REGISTER_TYPES"; + + /** + * Special syncable type that lets us know to sync all types. + */ + public static final String ALL_TYPES_TYPE = "ALL_TYPES"; + + /** + * Parcelable-valued intent extra containing the account of the user. + */ + public static final String EXTRA_ACCOUNT = "account"; + + /** + * String-list-valued intent extra of the syncable types to sync. + */ + public static final String EXTRA_REGISTERED_TYPES = "registered_types"; + + /** + * Boolean-valued intent extra indicating that the service should be stopped. + */ + public static final String EXTRA_STOP = "stop"; + + /** + * Create an Intent that will start the invalidation listener service and + * register for the specified types. + */ + public static Intent createRegisterIntent(Account account, + boolean allTypes, Set<ModelType> types) { + Intent registerIntent = new Intent(ACTION_REGISTER); + String[] selectedTypesArray; + if (allTypes) { + selectedTypesArray = new String[]{ALL_TYPES_TYPE}; + } else { + selectedTypesArray = new String[types.size()]; + int pos = 0; + for (ModelType type : types) { + selectedTypesArray[pos++] = type.name(); + } + } + registerIntent.putStringArrayListExtra(EXTRA_REGISTERED_TYPES, + Lists.newArrayList(selectedTypesArray)); + registerIntent.putExtra(EXTRA_ACCOUNT, account); + return registerIntent; + } + + private IntentProtocol() { + // Disallow instantiation. + } + } + + /** + * Name of the manifest application metadata property specifying the name of the class + * implementing the invalidation client. + */ + private static final String IMPLEMENTING_CLASS_MANIFEST_PROPERTY = + "org.chromium.sync.notifier.IMPLEMENTING_CLASS_NAME"; + + /** + * Logging tag. + */ + private static final String TAG = InvalidationController.class.getSimpleName(); + + private final Context context; + + /** + * Sets the types for which the client should register for notifications. + * + * @param account Account of the user. + * @param allTypes If {@code true}, registers for all types, and {@code types} is ignored + * @param types Set of types for which to register. Ignored if {@code allTypes == true}. + */ + public void setRegisteredTypes(Account account, boolean allTypes, Set<ModelType> types) { + Intent registerIntent = IntentProtocol.createRegisterIntent(account, allTypes, types); + setDestinationClassName(registerIntent); + context.startService(registerIntent); + } + + /** + * Starts the invalidation client. + */ + public void start() { + Intent intent = setDestinationClassName(new Intent()); + context.startService(intent); + } + + /** + * Stops the invalidation client. + */ + public void stop() { + Intent intent = setDestinationClassName(new Intent()); + intent.putExtra(IntentProtocol.EXTRA_STOP, true); + context.startService(intent); + } + + /** + * Returns the contract authority to use when requesting sync. + */ + public String getContractAuthority() { + return context.getPackageName(); + } + + /** + * Returns a new instance that will use {@code context} to issue intents. + */ + public static InvalidationController newInstance(Context context) { + return new InvalidationController(context); + } + + /** + * Creates an instance using {@code context} to send intents. + */ + private InvalidationController(Context context) { + this.context = Preconditions.checkNotNull(context.getApplicationContext()); + } + + /** + * Sets the destination class name of {@code intent} to the value given by the manifest + * property named {@link #IMPLEMENTING_CLASS_MANIFEST_PROPERTY}. If no such property exists or + * its value is null, takes no action. + * + * @return {@code intent} + */ + private Intent setDestinationClassName(Intent intent) { + ApplicationInfo appInfo; + try { + // Fetch application info and read the appropriate metadata element. + appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), + PackageManager.GET_META_DATA); + String className = null; + if (appInfo.metaData != null) { + className = appInfo.metaData.getString(IMPLEMENTING_CLASS_MANIFEST_PROPERTY); + } + if (className == null) { + Log.wtf(TAG, "No value for " + IMPLEMENTING_CLASS_MANIFEST_PROPERTY + + " in manifest; sync notifications will not work"); + } else { + intent.setClassName(context, className); + } + } catch (NameNotFoundException exception) { + Log.wtf(TAG, "Cannot read own application info", exception); + } + return intent; + } +} diff --git a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java new file mode 100644 index 0000000..eee2bdf --- /dev/null +++ b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java @@ -0,0 +1,154 @@ +// Copyright (c) 2013 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.sync.notifier; + +import android.accounts.Account; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import org.chromium.base.test.util.AdvancedMockContext; +import org.chromium.base.test.util.Feature; +import org.chromium.sync.internal_api.pub.base.ModelType; +import org.chromium.sync.notifier.InvalidationController.IntentProtocol; + +import java.util.List; +import java.util.Set; + +/** + * Tests for the {@link InvalidationController}. + */ +public class InvalidationControllerTest extends InstrumentationTestCase { + private IntentSavingContext mContext; + private InvalidationController mController; + + @Override + protected void setUp() throws Exception { + mContext = new IntentSavingContext(getInstrumentation().getTargetContext()); + mController = InvalidationController.newInstance(mContext); + } + + @SmallTest + @Feature({"Sync"}) + public void testStart() throws Exception { + mController.start(); + assertEquals(1, mContext.getNumStartedIntents()); + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertNull(intent.getExtras()); + } + + @SmallTest + @Feature({"Sync"}) + public void testStop() throws Exception { + mController.stop(); + assertEquals(1, mContext.getNumStartedIntents()); + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertEquals(1, intent.getExtras().size()); + assertTrue(intent.hasExtra(IntentProtocol.EXTRA_STOP)); + assertTrue(intent.getBooleanExtra(IntentProtocol.EXTRA_STOP, false)); + } + + @SmallTest + @Feature({"Sync"}) + public void testRegisterForSpecificTypes() { + Account account = new Account("test@example.com", "bogus"); + mController.setRegisteredTypes(account, false, + Sets.newHashSet(ModelType.BOOKMARK, ModelType.SESSION)); + assertEquals(1, mContext.getNumStartedIntents()); + + // Validate destination. + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertEquals(IntentProtocol.ACTION_REGISTER, intent.getAction()); + + // Validate account. + Account intentAccount = intent.getParcelableExtra(IntentProtocol.EXTRA_ACCOUNT); + assertEquals(account, intentAccount); + + // Validate registered types. + Set<String> expectedTypes = + Sets.newHashSet(ModelType.BOOKMARK.name(), ModelType.SESSION.name()); + Set<String> actualTypes = Sets.newHashSet(); + actualTypes.addAll(intent.getStringArrayListExtra(IntentProtocol.EXTRA_REGISTERED_TYPES)); + assertEquals(expectedTypes, actualTypes); + } + + @SmallTest + @Feature({"Sync"}) + public void testRegisterForAllTypes() { + Account account = new Account("test@example.com", "bogus"); + mController.setRegisteredTypes(account, true, + Sets.newHashSet(ModelType.BOOKMARK, ModelType.SESSION)); + assertEquals(1, mContext.getNumStartedIntents()); + + // Validate destination. + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertEquals(IntentProtocol.ACTION_REGISTER, intent.getAction()); + + // Validate account. + Account intentAccount = intent.getParcelableExtra(IntentProtocol.EXTRA_ACCOUNT); + assertEquals(account, intentAccount); + + // Validate registered types. + Set<String> expectedTypes = Sets.newHashSet(IntentProtocol.ALL_TYPES_TYPE); + Set<String> actualTypes = Sets.newHashSet(); + actualTypes.addAll(intent.getStringArrayListExtra(IntentProtocol.EXTRA_REGISTERED_TYPES)); + assertEquals(expectedTypes, actualTypes); + } + + @SmallTest + @Feature({"Sync"}) + public void testGetContractAuthority() throws Exception { + assertEquals(mContext.getPackageName(), mController.getContractAuthority()); + } + + /** + * Asserts that {@code intent} is destined for the correct component. + */ + private static void validateIntentComponent(Intent intent) { + assertNotNull(intent.getComponent()); + assertEquals("org.chromium.sync.notifier.TEST_VALUE", + intent.getComponent().getClassName()); + } + + /** + * Mock context that saves all intents given to {@code startService}. + */ + private static class IntentSavingContext extends AdvancedMockContext { + private final List<Intent> startedIntents = Lists.newArrayList(); + + IntentSavingContext(Context targetContext) { + super(targetContext); + } + + @Override + public ComponentName startService(Intent intent) { + startedIntents.add(intent); + return new ComponentName(this, getClass()); + } + + int getNumStartedIntents() { + return startedIntents.size(); + } + + Intent getStartedIntent(int idx) { + return startedIntents.get(idx); + } + + @Override + public PackageManager getPackageManager() { + return getBaseContext().getPackageManager(); + } + } +} diff --git a/sync/sync.gyp b/sync/sync.gyp index c22fec1..7caf784 100644 --- a/sync/sync.gyp +++ b/sync/sync.gyp @@ -986,6 +986,37 @@ }, ], }], + ['OS == "android"', { + 'targets': [ + { + 'target_name': 'sync_java', + 'type': 'none', + 'variables': { + 'package_name': 'sync', + 'java_in_dir': '../sync/android/java', + }, + 'dependencies': [ + '../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_javalib', + '../third_party/guava/guava.gyp:guava_javalib', + '../third_party/jsr-305/jsr-305.gyp:jsr_305_javalib', + ], + 'includes': [ '../build/java.gypi' ], + }, + { + 'target_name': 'sync_javatests', + 'type': 'none', + 'variables': { + 'package_name': 'sync_javatests', + 'java_in_dir': '../sync/android/javatests', + }, + 'dependencies': [ + 'sync_java', + '../base/base.gyp:base_java_test_support', + ], + 'includes': [ '../build/java.gypi' ], + }, + ], + }], # Special target to wrap a gtest_target_type==shared_library # sync_unit_tests into an android apk for execution. |