[Feed] Implement Content Storage API in Java

Implement Content Storage API in Java and JNI bridge.

Bug:828935

Change-Id: I6c70d18cfde0891f5c9533458bf453a891b4b2be
Reviewed-on: https://chromium-review.googlesource.com/1137739
Commit-Queue: Gang Wu <gangwu@chromium.org>
Reviewed-by: Sky Malice <skym@chromium.org>
Reviewed-by: Filip Gorski <fgorski@chromium.org>
Cr-Commit-Position: refs/heads/master@{#576950}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedContentStorage.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedContentStorage.java
new file mode 100644
index 0000000..9926d3a
--- /dev/null
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedContentStorage.java
@@ -0,0 +1,144 @@
+// 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.chrome.browser.feed;
+
+import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.functional.Consumer;
+import com.google.android.libraries.feed.host.storage.CommitResult;
+import com.google.android.libraries.feed.host.storage.ContentMutation;
+import com.google.android.libraries.feed.host.storage.ContentOperation;
+import com.google.android.libraries.feed.host.storage.ContentOperation.Delete;
+import com.google.android.libraries.feed.host.storage.ContentOperation.DeleteByPrefix;
+import com.google.android.libraries.feed.host.storage.ContentOperation.Type;
+import com.google.android.libraries.feed.host.storage.ContentOperation.Upsert;
+import com.google.android.libraries.feed.host.storage.ContentStorage;
+
+import org.chromium.base.Callback;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of {@link ContentStorage} that persisits data on native side.
+ */
+public class FeedContentStorage implements ContentStorage {
+    private FeedStorageBridge mFeedStorageBridge;
+
+    private static class StorageCallback<T> implements Callback<T> {
+        private final Consumer<Result<T>> mConsumer;
+
+        public StorageCallback(Consumer<Result<T>> consumer) {
+            mConsumer = consumer;
+        }
+
+        @Override
+        public void onResult(T data) {
+            // TODO(gangwu): Need to handle failure case.
+            mConsumer.accept(Result.success(data));
+        }
+    }
+
+    private static class CommitCallback implements Callback<Boolean> {
+        private final Consumer<CommitResult> mConsumer;
+        private int mOperationsLeft;
+        private boolean mSuccess;
+
+        CommitCallback(Consumer<CommitResult> consumer, int operationsCount) {
+            mConsumer = consumer;
+            mOperationsLeft = operationsCount;
+            mSuccess = true;
+        }
+
+        @Override
+        public void onResult(Boolean result) {
+            --mOperationsLeft;
+            assert mOperationsLeft >= 0;
+
+            mSuccess &= result.booleanValue();
+            // TODO(gangwu): if |result| is failure, all other operation should halt immediately.
+            if (mOperationsLeft == 0) {
+                if (mSuccess) {
+                    mConsumer.accept(CommitResult.SUCCESS);
+                } else {
+                    mConsumer.accept(CommitResult.FAILURE);
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates a {@link FeedContentStorage} for storing content for the current user.
+     *
+     * @param bridge {@link FeedStorageBridge} implementation can handle content storage request.
+     */
+    public FeedContentStorage(FeedStorageBridge bridge) {
+        mFeedStorageBridge = bridge;
+    }
+
+    /** Cleans up {@link FeedContentStorage}. */
+    public void destroy() {
+        assert mFeedStorageBridge != null;
+        mFeedStorageBridge.destroy();
+        mFeedStorageBridge = null;
+    }
+
+    @Override
+    public void get(List<String> keys, Consumer < Result < Map<String, byte[]>>> consumer) {
+        assert mFeedStorageBridge != null;
+        mFeedStorageBridge.loadContent(keys, new StorageCallback<Map<String, byte[]>>(consumer));
+    }
+
+    @Override
+    public void getAll(String prefix, Consumer < Result < Map<String, byte[]>>> consumer) {
+        assert mFeedStorageBridge != null;
+        mFeedStorageBridge.loadContentByPrefix(
+                prefix, new StorageCallback<Map<String, byte[]>>(consumer));
+    }
+
+    @Override
+    public void commit(ContentMutation mutation, Consumer<CommitResult> consumer) {
+        assert mFeedStorageBridge != null;
+
+        CommitCallback callback = new CommitCallback(consumer, mutation.getOperations().size());
+        for (ContentOperation operation : mutation.getOperations()) {
+            switch (operation.getType()) {
+                case Type.UPSERT:
+                    // TODO(gangwu): If upserts are continuous, we should conbine them into one
+                    // array, and then send to native side.
+                    Upsert upsert = (Upsert) operation;
+                    String[] upsertKeys = {upsert.getKey()};
+                    byte[][] upsertData = {upsert.getValue()};
+                    mFeedStorageBridge.saveContent(upsertKeys, upsertData, callback);
+                    break;
+                case Type.DELETE:
+                    // TODO(gangwu): If deletes are continuous, we should conbine them into one
+                    // array, and then send to native side.
+                    Delete delete = (Delete) operation;
+                    List<String> deleteKeys = Collections.singletonList(delete.getKey());
+                    mFeedStorageBridge.deleteContent(deleteKeys, callback);
+                    break;
+                case Type.DELETE_BY_PREFIX:
+                    DeleteByPrefix deleteByPrefix = (DeleteByPrefix) operation;
+                    String prefix = deleteByPrefix.getPrefix();
+                    mFeedStorageBridge.deleteContentByPrefix(prefix, callback);
+                    break;
+                case Type.DELETE_ALL:
+                    mFeedStorageBridge.deleteAllContent(callback);
+                    break;
+                default:
+                    // Unsupport type of operations, so cannot performance the operation.
+                    callback.onResult(false);
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void getAllKeys(Consumer < Result < List<String>>> consumer) {
+        assert mFeedStorageBridge != null;
+        mFeedStorageBridge.loadAllContentKeys(new StorageCallback<List<String>>(consumer));
+    }
+}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedStorageBridge.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedStorageBridge.java
new file mode 100644
index 0000000..b45d9719
--- /dev/null
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedStorageBridge.java
@@ -0,0 +1,116 @@
+// 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.chrome.browser.feed;
+
+import org.chromium.base.Callback;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.chrome.browser.profiles.Profile;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides access to native implementations of content storage and journal storage.
+ */
+@JNINamespace("feed")
+public class FeedStorageBridge {
+    private long mNativeFeedStorageBridge;
+
+    /**
+     * Creates a {@link FeedStorageBridge} for accessing native content and journal storage
+     * implementation for the current user, and initial native side bridge.
+     *
+     * @param profile {@link Profile} of the user we are rendering the Feed for.
+     */
+    public FeedStorageBridge() {}
+
+    /**
+     * Inits native side bridge.
+     *
+     * @param profile {@link Profile} of the user we are rendering the Feed for.
+     */
+    public void init(Profile profile) {
+        mNativeFeedStorageBridge = nativeInit(profile);
+    }
+
+    /** Cleans up native half of this bridge. */
+    public void destroy() {
+        assert mNativeFeedStorageBridge != 0;
+        nativeDestroy(mNativeFeedStorageBridge);
+        mNativeFeedStorageBridge = 0;
+    }
+
+    public void loadContent(List<String> keys, Callback<Map<String, byte[]>> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        String[] keysArray = keys.toArray(new String[keys.size()]);
+        nativeLoadContent(mNativeFeedStorageBridge, keysArray, callback);
+    }
+
+    public void loadContentByPrefix(String prefix, Callback<Map<String, byte[]>> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        nativeLoadContentByPrefix(mNativeFeedStorageBridge, prefix, callback);
+    }
+
+    public void loadAllContentKeys(Callback<List<String>> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        nativeLoadAllContentKeys(mNativeFeedStorageBridge, callback);
+    }
+
+    public void saveContent(String[] keys, byte[][] data, Callback<Boolean> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        nativeSaveContent(mNativeFeedStorageBridge, keys, data, callback);
+    }
+
+    public void deleteContent(List<String> keys, Callback<Boolean> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        String[] keysArray = keys.toArray(new String[keys.size()]);
+        nativeDeleteContent(mNativeFeedStorageBridge, keysArray, callback);
+    }
+
+    public void deleteContentByPrefix(String prefix, Callback<Boolean> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        nativeDeleteContentByPrefix(mNativeFeedStorageBridge, prefix, callback);
+    }
+
+    public void deleteAllContent(Callback<Boolean> callback) {
+        assert mNativeFeedStorageBridge != 0;
+        nativeDeleteAllContent(mNativeFeedStorageBridge, callback);
+    }
+
+    @CalledByNative
+    private static Object createKeyAndDataMap(String[] keys, byte[][] data) {
+        assert keys.length == data.length;
+        Map<String, byte[]> valueMap = new HashMap<>(keys.length);
+        for (int i = 0; i < keys.length && i < data.length; ++i) {
+            valueMap.put(keys[i], data[i]);
+        }
+        return valueMap;
+    }
+
+    @CalledByNative
+    private static List<String> createJavaList(String[] keys) {
+        return Arrays.asList(keys);
+    }
+
+    private native long nativeInit(Profile profile);
+    private native void nativeDestroy(long nativeFeedStorageBridge);
+    private native void nativeLoadContent(
+            long nativeFeedStorageBridge, String[] keys, Callback<Map<String, byte[]>> callback);
+    private native void nativeLoadContentByPrefix(
+            long nativeFeedStorageBridge, String prefix, Callback<Map<String, byte[]>> callback);
+    private native void nativeLoadAllContentKeys(
+            long nativeFeedStorageBridge, Callback<List<String>> callback);
+    private native void nativeSaveContent(
+            long nativeFeedStorageBridge, String[] keys, byte[][] data, Callback<Boolean> callback);
+    private native void nativeDeleteContent(
+            long nativeFeedStorageBridge, String[] keys, Callback<Boolean> callback);
+    private native void nativeDeleteContentByPrefix(
+            long nativeFeedStorageBridge, String prefix, Callback<Boolean> callback);
+    private native void nativeDeleteAllContent(
+            long nativeFeedStorageBridge, Callback<Boolean> callback);
+}
diff --git a/chrome/android/feed/feed_java_sources.gni b/chrome/android/feed/feed_java_sources.gni
index bb4ce6e..ebe41f4 100644
--- a/chrome/android/feed/feed_java_sources.gni
+++ b/chrome/android/feed/feed_java_sources.gni
@@ -9,6 +9,7 @@
   feed_deps = [ "//third_party/feed:feed_lib_java" ]
 
   feed_java_sources = [
+    "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedContentStorage.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedEventReporter.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedImageLoader.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedImageLoaderBridge.java",
@@ -18,6 +19,7 @@
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedProcessScopeFactory.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedRefreshTask.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSchedulerBridge.java",
+    "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedStorageBridge.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/StreamLifecycleManager.java",
     "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/action/FeedActionHandler.java",
   ]
@@ -25,6 +27,7 @@
   feed_srcjar_deps = [ "//components/feed/core:feed_core_java_enums_srcjar" ]
 
   feed_junit_test_java_sources = [
+    "junit/src/org/chromium/chrome/browser/feed/FeedContentStorageTest.java",
     "junit/src/org/chromium/chrome/browser/feed/FeedImageLoaderTest.java",
     "junit/src/org/chromium/chrome/browser/feed/StreamLifecycleManagerTest.java",
   ]
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedContentStorageTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedContentStorageTest.java
new file mode 100644
index 0000000..36ae6f4
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedContentStorageTest.java
@@ -0,0 +1,245 @@
+// 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.chrome.browser.feed;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+
+import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.functional.Consumer;
+import com.google.android.libraries.feed.host.storage.CommitResult;
+import com.google.android.libraries.feed.host.storage.ContentMutation;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.Callback;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.profiles.Profile;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link FeedContentStorage}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class FeedContentStorageTest {
+    public static final String CONTENT_KEY1 = "CONTENT_KEY_1";
+    public static final String CONTENT_KEY2 = "CONTENT_KEY_2";
+    public static final String CONTENT_KEY3 = "CONTENT_KEY_3";
+    public static final byte[] CONTENT_DATA1 = "CONTENT_DATA_1".getBytes(Charset.forName("UTF-8"));
+    public static final byte[] CONTENT_DATA2 = "CONTENT_DATA_2".getBytes(Charset.forName("UTF-8"));
+    public static final byte[] CONTENT_DATA3 = "CONTENT_DATA_3".getBytes(Charset.forName("UTF-8"));
+
+    @Mock
+    private FeedStorageBridge mBridge;
+    @Mock
+    private Consumer<CommitResult> mBooleanConsumer;
+    @Mock
+            private Consumer < Result < List<String>>> mListConsumer;
+    @Mock
+            private Consumer < Result < Map<String, byte[]>>> mMapConsumer;
+    @Mock
+    private Profile mProfile;
+    @Captor
+    private ArgumentCaptor<CommitResult> mCommitResultCaptor;
+    @Captor
+            private ArgumentCaptor < Result < List<String>>> mStringListCaptor;
+    @Captor
+            private ArgumentCaptor < Result < Map<String, byte[]>>> mMapCaptor;
+    @Captor
+    private ArgumentCaptor<String> mStringArgument;
+    @Captor
+    private ArgumentCaptor<List<String>> mStringListArgument;
+    @Captor
+    private ArgumentCaptor<String[]> mStringArrayArgument;
+    @Captor
+    private ArgumentCaptor<byte[][]> mByteArrayOfArrayArgument;
+    @Captor
+    private ArgumentCaptor<Callback<Boolean>> mBooleanCallbackArgument;
+    @Captor
+            private ArgumentCaptor < Callback < List<String>>> mStringListCallbackArgument;
+    @Captor
+            private ArgumentCaptor < Callback < Map<String, byte[]>>> mMapCallbackArgument;
+
+    private FeedContentStorage mContentStorage;
+
+    private Answer<Void> createMapAnswer(Map<String, byte[]> map) {
+        return new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                mMapCallbackArgument.getValue().onResult(map);
+                return null;
+            }
+        };
+    }
+
+    private Answer<Void> createStringListAnswer(List<String> stringList) {
+        return new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                mStringListCallbackArgument.getValue().onResult(stringList);
+                return null;
+            }
+        };
+    }
+
+    private Answer<Void> createBooleanAnswer(Boolean bool) {
+        return new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                mBooleanCallbackArgument.getValue().onResult(bool);
+                return null;
+            }
+        };
+    }
+
+    private void verifyMapResult(Map<String, byte[]> expectedMap, boolean expectedBoolean,
+            Result<Map<String, byte[]>> actualResult) {
+        assertEquals(expectedBoolean, actualResult.isSuccessful());
+        if (!expectedBoolean) return;
+
+        Map<String, byte[]> actualMap = actualResult.getValue();
+        assertEquals(expectedMap.size(), actualMap.size());
+        for (Map.Entry<String, byte[]> entry : expectedMap.entrySet()) {
+            assertTrue(actualMap.containsKey(entry.getKey()));
+            assertEquals(entry.getValue(), actualMap.get(entry.getKey()));
+        }
+    }
+
+    private void verifyStringListResult(
+            List<String> expectedList, boolean expectedBoolean, Result<List<String>> actualResult) {
+        assertEquals(expectedBoolean, actualResult.isSuccessful());
+        if (!expectedBoolean) return;
+
+        List<String> actualList = actualResult.getValue();
+        assertEquals(expectedList.size(), actualList.size());
+        for (String expectedString : expectedList) {
+            assertTrue(actualList.contains(expectedString));
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doNothing().when(mBridge).init(eq(mProfile));
+        mBridge.init(mProfile);
+        mContentStorage = new FeedContentStorage(mBridge);
+    }
+
+    @Test
+    @SmallTest
+    public void getTest() {
+        Map<String, byte[]> answerMap = new HashMap<>();
+        answerMap.put(CONTENT_KEY1, CONTENT_DATA1);
+        Answer<Void> answer = createMapAnswer(answerMap);
+        doAnswer(answer).when(mBridge).loadContent(
+                mStringListArgument.capture(), mMapCallbackArgument.capture());
+        List<String> keys = Arrays.asList(CONTENT_KEY1, CONTENT_KEY2);
+
+        mContentStorage.get(keys, mMapConsumer);
+        verify(mBridge, times(1)).loadContent(eq(keys), mMapCallbackArgument.capture());
+        verify(mMapConsumer, times(1)).accept(mMapCaptor.capture());
+        verifyMapResult(answerMap, true, mMapCaptor.getValue());
+    }
+
+    @Test
+    @SmallTest
+    public void getAllTest() {
+        Map<String, byte[]> answerMap = new HashMap<>();
+        answerMap.put(CONTENT_KEY1, CONTENT_DATA1);
+        answerMap.put(CONTENT_KEY2, CONTENT_DATA2);
+        answerMap.put(CONTENT_KEY3, CONTENT_DATA3);
+        Answer<Void> answer = createMapAnswer(answerMap);
+        doAnswer(answer).when(mBridge).loadContentByPrefix(
+                mStringArgument.capture(), mMapCallbackArgument.capture());
+
+        mContentStorage.getAll(CONTENT_KEY1, mMapConsumer);
+        verify(mBridge, times(1))
+                .loadContentByPrefix(eq(CONTENT_KEY1), mMapCallbackArgument.capture());
+        verify(mMapConsumer, times(1)).accept(mMapCaptor.capture());
+        verifyMapResult(answerMap, true, mMapCaptor.getValue());
+    }
+
+    @Test
+    @SmallTest
+    public void getAllKeysTest() {
+        List<String> answerStrings = Arrays.asList(CONTENT_KEY1, CONTENT_KEY2, CONTENT_KEY3);
+        Answer<Void> answer = createStringListAnswer(answerStrings);
+        doAnswer(answer).when(mBridge).loadAllContentKeys(mStringListCallbackArgument.capture());
+
+        mContentStorage.getAllKeys(mListConsumer);
+        verify(mBridge, times(1)).loadAllContentKeys(mStringListCallbackArgument.capture());
+        verify(mListConsumer, times(1)).accept(mStringListCaptor.capture());
+        verifyStringListResult(answerStrings, true, mStringListCaptor.getValue());
+    }
+
+    @Test
+    @SmallTest
+    public void commitTest() {
+        Answer<Void> answerSaveContent = createBooleanAnswer(true);
+        doAnswer(answerSaveContent)
+                .when(mBridge)
+                .saveContent(mStringArrayArgument.capture(), mByteArrayOfArrayArgument.capture(),
+                        mBooleanCallbackArgument.capture());
+
+        Answer<Void> answerDeleteContent = createBooleanAnswer(true);
+        doAnswer(answerDeleteContent)
+                .when(mBridge)
+                .deleteContent(mStringListArgument.capture(), mBooleanCallbackArgument.capture());
+
+        Answer<Void> answerDeleteContentByPrefix = createBooleanAnswer(true);
+        doAnswer(answerDeleteContentByPrefix)
+                .when(mBridge)
+                .deleteContentByPrefix(
+                        mStringArgument.capture(), mBooleanCallbackArgument.capture());
+
+        Answer<Void> answerDeleteAllContent = createBooleanAnswer(true);
+        doAnswer(answerDeleteAllContent)
+                .when(mBridge)
+                .deleteAllContent(mBooleanCallbackArgument.capture());
+
+        mContentStorage.commit(new ContentMutation.Builder()
+                                       .upsert(CONTENT_KEY1, CONTENT_DATA1)
+                                       .delete(CONTENT_KEY2)
+                                       .deleteByPrefix(CONTENT_KEY3)
+                                       .deleteAll()
+                                       .build(),
+                mBooleanConsumer);
+        verify(mBridge, times(1))
+                .saveContent(mStringArrayArgument.capture(), mByteArrayOfArrayArgument.capture(),
+                        mBooleanCallbackArgument.capture());
+        verify(mBridge, times(1))
+                .deleteContent(mStringListArgument.capture(), mBooleanCallbackArgument.capture());
+        verify(mBridge, times(1))
+                .deleteContentByPrefix(
+                        mStringArgument.capture(), mBooleanCallbackArgument.capture());
+        verify(mBridge, times(1)).deleteAllContent(mBooleanCallbackArgument.capture());
+
+        verify(mBooleanConsumer, times(1)).accept(mCommitResultCaptor.capture());
+        CommitResult commitResult = mCommitResultCaptor.getValue();
+        assertEquals(CommitResult.SUCCESS, commitResult);
+    }
+}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 3ed3fcd..d597743 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4334,6 +4334,8 @@
       "android/feed/feed_network_bridge.h",
       "android/feed/feed_scheduler_bridge.cc",
       "android/feed/feed_scheduler_bridge.h",
+      "android/feed/feed_storage_bridge.cc",
+      "android/feed/feed_storage_bridge.h",
     ]
     deps += [ "//components/feed/core:feed_core" ]
   }
@@ -4595,6 +4597,7 @@
         "../android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedImageLoaderBridge.java",
         "../android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedNetworkBridge.java",
         "../android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSchedulerBridge.java",
+        "../android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedStorageBridge.java",
       ]
     }
 
diff --git a/chrome/browser/android/feed/feed_storage_bridge.cc b/chrome/browser/android/feed/feed_storage_bridge.cc
new file mode 100644
index 0000000..3393ad2
--- /dev/null
+++ b/chrome/browser/android/feed/feed_storage_bridge.cc
@@ -0,0 +1,198 @@
+// 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.
+
+#include "chrome/browser/android/feed/feed_storage_bridge.h"
+
+#include <jni.h>
+
+#include <string>
+#include <vector>
+
+#include "base/android/callback_android.h"
+#include "base/android/jni_array.h"
+#include "base/android/jni_string.h"
+#include "base/bind.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/android/feed/feed_host_service_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_android.h"
+#include "components/feed/core/feed_host_service.h"
+#include "components/feed/core/feed_storage_database.h"
+#include "jni/FeedStorageBridge_jni.h"
+#include "ui/gfx/android/java_bitmap.h"
+#include "ui/gfx/image/image.h"
+
+namespace feed {
+
+using base::android::AppendJavaStringArrayToStringVector;
+using base::android::AttachCurrentThread;
+using base::android::ConvertJavaStringToUTF8;
+using base::android::JavaArrayOfByteArrayToStringVector;
+using base::android::JavaIntArrayToIntVector;
+using base::android::JavaRef;
+using base::android::JavaParamRef;
+using base::android::ScopedJavaGlobalRef;
+using base::android::ScopedJavaLocalRef;
+using base::android::ToJavaArrayOfByteArray;
+using base::android::ToJavaArrayOfStrings;
+
+static jlong JNI_FeedStorageBridge_Init(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_this,
+    const JavaParamRef<jobject>& j_profile) {
+  Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile);
+  FeedHostService* host_service =
+      FeedHostServiceFactory::GetForBrowserContext(profile);
+  DCHECK(host_service);
+  FeedStorageDatabase* feed_storage_database =
+      host_service->GetStorageDatabase();
+  DCHECK(feed_storage_database);
+  FeedStorageBridge* native_storage_bridge =
+      new FeedStorageBridge(feed_storage_database);
+  return reinterpret_cast<intptr_t>(native_storage_bridge);
+}
+
+FeedStorageBridge::FeedStorageBridge(FeedStorageDatabase* feed_storage_database)
+    : feed_storage_database_(feed_storage_database), weak_ptr_factory_(this) {}
+
+FeedStorageBridge::~FeedStorageBridge() = default;
+
+void FeedStorageBridge::Destroy(JNIEnv* env, const JavaRef<jobject>& j_this) {
+  delete this;
+}
+
+void FeedStorageBridge::LoadContent(JNIEnv* j_env,
+                                    const JavaRef<jobject>& j_this,
+                                    const JavaRef<jobjectArray>& j_keys,
+                                    const JavaRef<jobject>& j_callback) {
+  std::vector<std::string> keys;
+  AppendJavaStringArrayToStringVector(j_env, j_keys.obj(), &keys);
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->LoadContent(
+      keys, base::BindOnce(&FeedStorageBridge::OnLoadContentDone,
+                           weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::LoadContentByPrefix(
+    JNIEnv* j_env,
+    const JavaRef<jobject>& j_this,
+    const JavaRef<jstring>& j_prefix,
+    const JavaRef<jobject>& j_callback) {
+  std::string prefix = ConvertJavaStringToUTF8(j_env, j_prefix);
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->LoadContentByPrefix(
+      prefix, base::BindOnce(&FeedStorageBridge::OnLoadContentDone,
+                             weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::LoadAllContentKeys(JNIEnv* j_env,
+                                           const JavaRef<jobject>& j_this,
+                                           const JavaRef<jobject>& j_callback) {
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->LoadAllContentKeys(
+      base::BindOnce(&FeedStorageBridge::OnLoadAllContentKeysDone,
+                     weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::SaveContent(JNIEnv* j_env,
+                                    const JavaRef<jobject>& j_this,
+                                    const JavaRef<jobjectArray>& j_keys,
+                                    const JavaRef<jobjectArray>& j_data,
+                                    const JavaRef<jobject>& j_callback) {
+  std::vector<std::string> keys;
+  std::vector<std::string> data;
+  AppendJavaStringArrayToStringVector(j_env, j_keys.obj(), &keys);
+  JavaArrayOfByteArrayToStringVector(j_env, j_data.obj(), &data);
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  DCHECK_EQ(keys.size(), data.size());
+  std::vector<FeedStorageDatabase::KeyAndData> pairs;
+  for (size_t i = 0; i < keys.size() && i < data.size(); ++i) {
+    pairs.emplace_back(keys[i], data[i]);
+  }
+
+  feed_storage_database_->SaveContent(
+      std::move(pairs),
+      base::BindOnce(&FeedStorageBridge::OnStorageCommitDone,
+                     weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::DeleteContent(JNIEnv* j_env,
+                                      const JavaRef<jobject>& j_this,
+                                      const JavaRef<jobjectArray>& j_keys,
+                                      const JavaRef<jobject>& j_callback) {
+  std::vector<std::string> keys;
+  AppendJavaStringArrayToStringVector(j_env, j_keys.obj(), &keys);
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->DeleteContent(
+      keys, base::BindOnce(&FeedStorageBridge::OnStorageCommitDone,
+                           weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::DeleteContentByPrefix(
+    JNIEnv* j_env,
+    const JavaRef<jobject>& j_this,
+    const JavaRef<jstring>& j_prefix,
+    const JavaRef<jobject>& j_callback) {
+  std::string prefix = ConvertJavaStringToUTF8(j_env, j_prefix);
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->DeleteContentByPrefix(
+      prefix, base::BindOnce(&FeedStorageBridge::OnStorageCommitDone,
+                             weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::DeleteAllContent(JNIEnv* j_env,
+                                         const JavaRef<jobject>& j_this,
+                                         const JavaRef<jobject>& j_callback) {
+  ScopedJavaGlobalRef<jobject> callback(j_callback);
+
+  feed_storage_database_->DeleteAllContent(
+      base::BindOnce(&FeedStorageBridge::OnStorageCommitDone,
+                     weak_ptr_factory_.GetWeakPtr(), callback));
+}
+
+void FeedStorageBridge::OnLoadContentDone(
+    ScopedJavaGlobalRef<jobject> callback,
+    std::vector<FeedStorageDatabase::KeyAndData> pairs) {
+  std::vector<std::string> keys;
+  std::vector<std::string> data;
+  for (auto pair : pairs) {
+    keys.push_back(std::move(pair.first));
+    data.push_back(std::move(pair.second));
+  }
+
+  JNIEnv* env = AttachCurrentThread();
+  ScopedJavaLocalRef<jobjectArray> j_keys = ToJavaArrayOfStrings(env, keys);
+  ScopedJavaLocalRef<jobjectArray> j_data = ToJavaArrayOfByteArray(env, data);
+
+  // Ceate Java Map by JNI call.
+  ScopedJavaLocalRef<jobject> j_pairs =
+      Java_FeedStorageBridge_createKeyAndDataMap(env, j_keys, j_data);
+  RunObjectCallbackAndroid(callback, j_pairs);
+}
+
+void FeedStorageBridge::OnLoadAllContentKeysDone(
+    ScopedJavaGlobalRef<jobject> callback,
+    std::vector<std::string> keys) {
+  JNIEnv* env = AttachCurrentThread();
+  ScopedJavaLocalRef<jobjectArray> j_keys = ToJavaArrayOfStrings(env, keys);
+
+  // Ceate Java List by JNI call.
+  ScopedJavaLocalRef<jobject> j_keys_list =
+      Java_FeedStorageBridge_createJavaList(env, j_keys);
+  RunObjectCallbackAndroid(callback, j_keys_list);
+}
+
+void FeedStorageBridge::OnStorageCommitDone(
+    ScopedJavaGlobalRef<jobject> callback,
+    bool success) {
+  RunBooleanCallbackAndroid(callback, success);
+}
+
+}  // namespace feed
diff --git a/chrome/browser/android/feed/feed_storage_bridge.h b/chrome/browser/android/feed/feed_storage_bridge.h
new file mode 100644
index 0000000..19b7d8f
--- /dev/null
+++ b/chrome/browser/android/feed/feed_storage_bridge.h
@@ -0,0 +1,75 @@
+// 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.
+
+#ifndef CHROME_BROWSER_ANDROID_FEED_FEED_STORAGE_BRIDGE_H_
+#define CHROME_BROWSER_ANDROID_FEED_FEED_STORAGE_BRIDGE_H_
+
+#include "base/android/scoped_java_ref.h"
+#include "base/memory/weak_ptr.h"
+#include "components/feed/core/feed_storage_database.h"
+
+namespace feed {
+
+class FeedStorageDatabase;
+
+// Native counterpart of FeedStorageBridge.java. Holds non-owning pointers
+// to native implementation, to which operations are delegated. Results are
+// passed back by a single argument callback so
+// base::android::RunBooleanCallbackAndroid() and
+// base::android::RunObjectCallbackAndroid() can be used. This bridge is
+// instantiated, owned, and destroyed from Java.
+class FeedStorageBridge {
+ public:
+  explicit FeedStorageBridge(FeedStorageDatabase* feed_Storage_database);
+  ~FeedStorageBridge();
+
+  void Destroy(JNIEnv* j_env, const base::android::JavaRef<jobject>& j_this);
+
+  void LoadContent(JNIEnv* j_env,
+                   const base::android::JavaRef<jobject>& j_this,
+                   const base::android::JavaRef<jobjectArray>& j_keys,
+                   const base::android::JavaRef<jobject>& j_callback);
+  void LoadContentByPrefix(JNIEnv* j_env,
+                           const base::android::JavaRef<jobject>& j_this,
+                           const base::android::JavaRef<jstring>& j_prefix,
+                           const base::android::JavaRef<jobject>& j_callback);
+  void LoadAllContentKeys(JNIEnv* j_env,
+                          const base::android::JavaRef<jobject>& j_this,
+                          const base::android::JavaRef<jobject>& j_callback);
+  void SaveContent(JNIEnv* j_env,
+                   const base::android::JavaRef<jobject>& j_this,
+                   const base::android::JavaRef<jobjectArray>& j_keys,
+                   const base::android::JavaRef<jobjectArray>& j_data,
+                   const base::android::JavaRef<jobject>& j_callback);
+  void DeleteContent(JNIEnv* j_env,
+                     const base::android::JavaRef<jobject>& j_this,
+                     const base::android::JavaRef<jobjectArray>& j_keys,
+                     const base::android::JavaRef<jobject>& j_callback);
+  void DeleteContentByPrefix(JNIEnv* j_env,
+                             const base::android::JavaRef<jobject>& j_this,
+                             const base::android::JavaRef<jstring>& j_prefix,
+                             const base::android::JavaRef<jobject>& j_callback);
+  void DeleteAllContent(JNIEnv* j_env,
+                        const base::android::JavaRef<jobject>& j_this,
+                        const base::android::JavaRef<jobject>& j_callback);
+
+ private:
+  void OnLoadContentDone(base::android::ScopedJavaGlobalRef<jobject> callback,
+                         std::vector<FeedStorageDatabase::KeyAndData> pairs);
+  void OnLoadAllContentKeysDone(
+      base::android::ScopedJavaGlobalRef<jobject> callback,
+      std::vector<std::string> keys);
+  void OnStorageCommitDone(base::android::ScopedJavaGlobalRef<jobject> callback,
+                           bool success);
+
+  FeedStorageDatabase* feed_storage_database_;
+
+  base::WeakPtrFactory<FeedStorageBridge> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(FeedStorageBridge);
+};
+
+}  // namespace feed
+
+#endif  // CHROME_BROWSER_ANDROID_FEED_FEED_STORAGE_BRIDGE_H_