From 5738fe642601237e16417105d6727f777e73aae3 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 31 Oct 2022 14:39:19 -0700 Subject: [PATCH] refactor(AsyncStorage): move android files from react-native-github Summary: ## Changelog: [Android] [Removed] - Removed AsyncStorage module Reviewed By: lunaleaps Differential Revision: D40175995 fbshipit-source-id: b583579b8c2fa6c502f265ffe464b81672bd7da5 --- .../storage/AsyncLocalStorageUtil.java | 125 ----- .../storage/AsyncStorageErrorUtil.java | 38 -- .../modules/storage/AsyncStorageModule.java | 433 ------------------ .../com/facebook/react/modules/storage/BUCK | 25 - .../storage/ReactDatabaseSupplier.java | 163 ------- .../main/java/com/facebook/react/shell/BUCK | 1 - .../react/shell/MainReactPackage.java | 5 - .../test/java/com/facebook/react/modules/BUCK | 1 - .../storage/AsyncStorageModuleTest.java | 345 -------------- 9 files changed, 1136 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java delete mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java deleted file mode 100644 index d3e8fb9b359134..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.storage; - -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.text.TextUtils; -import androidx.annotation.Nullable; -import com.facebook.react.bridge.ReadableArray; -import java.util.Arrays; -import java.util.Iterator; -import org.json.JSONException; -import org.json.JSONObject; - -/** Helper for database operations. */ -public class AsyncLocalStorageUtil { - - /** - * Build the String required for an SQL select statement: WHERE key IN (?, ?, ..., ?) without - * 'WHERE' and with selectionCount '?' - */ - /* package */ static String buildKeySelection(int selectionCount) { - String[] list = new String[selectionCount]; - Arrays.fill(list, "?"); - return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")"; - } - - /** - * Build the String[] arguments needed for an SQL selection, i.e.: {a, b, c} to be used in the SQL - * select statement: WHERE key in (?, ?, ?) - */ - /* package */ static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) { - String[] selectionArgs = new String[count]; - for (int keyIndex = 0; keyIndex < count; keyIndex++) { - selectionArgs[keyIndex] = keys.getString(start + keyIndex); - } - return selectionArgs; - } - - /** Returns the value of the given key, or null if not found. */ - public static @Nullable String getItemImpl(SQLiteDatabase db, String key) { - String[] columns = {VALUE_COLUMN}; - String[] selectionArgs = {key}; - - Cursor cursor = - db.query(TABLE_CATALYST, columns, KEY_COLUMN + "=?", selectionArgs, null, null, null); - - try { - if (!cursor.moveToFirst()) { - return null; - } else { - return cursor.getString(0); - } - } finally { - cursor.close(); - } - } - - /** Sets the value for the key given, returns true if successful, false otherwise. */ - /* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) { - ContentValues contentValues = new ContentValues(); - contentValues.put(KEY_COLUMN, key); - contentValues.put(VALUE_COLUMN, value); - - long inserted = - db.insertWithOnConflict( - TABLE_CATALYST, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); - - return (-1 != inserted); - } - - /** - * Does the actual merge of the (key, value) pair with the value stored in the database. NB: This - * assumes that a database lock is already in effect! - * - * @return the errorCode of the operation - */ - /* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value) - throws JSONException { - String oldValue = getItemImpl(db, key); - String newValue; - - if (oldValue == null) { - newValue = value; - } else { - JSONObject oldJSON = new JSONObject(oldValue); - JSONObject newJSON = new JSONObject(value); - deepMergeInto(oldJSON, newJSON); - newValue = oldJSON.toString(); - } - - return setItemImpl(db, key, newValue); - } - - /** - * Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by - * either overriding its values, or merging them (if the values of the same key in both objects - * are of type {@link JSONObject}). oldJSON will contain the result of this merge. - */ - private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON) throws JSONException { - Iterator keys = newJSON.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - - JSONObject newJSONObject = newJSON.optJSONObject(key); - JSONObject oldJSONObject = oldJSON.optJSONObject(key); - if (newJSONObject != null && oldJSONObject != null) { - deepMergeInto(oldJSONObject, newJSONObject); - oldJSON.put(key, oldJSONObject); - } else { - oldJSON.put(key, newJSON.get(key)); - } - } - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java deleted file mode 100644 index eaa59f4c2d05cb..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.storage; - -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; - -/** Helper class for database errors. */ -public class AsyncStorageErrorUtil { - - /** Create Error object to be passed back to the JS callback. */ - /* package */ static WritableMap getError(@Nullable String key, String errorMessage) { - WritableMap errorMap = Arguments.createMap(); - errorMap.putString("message", errorMessage); - if (key != null) { - errorMap.putString("key", key); - } - return errorMap; - } - - /* package */ static WritableMap getInvalidKeyError(@Nullable String key) { - return getError(key, "Invalid key"); - } - - /* package */ static WritableMap getInvalidValueError(@Nullable String key) { - return getError(key, "Invalid Value"); - } - - /* package */ static WritableMap getDBError(@Nullable String key) { - return getError(key, "Database Error"); - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java deleted file mode 100644 index 1b33b4b83d7ddd..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.storage; - -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; - -import android.database.Cursor; -import android.database.sqlite.SQLiteStatement; -import android.os.AsyncTask; -import com.facebook.common.logging.FLog; -import com.facebook.fbreact.specs.NativeAsyncSQLiteDBStorageSpec; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.GuardedAsyncTask; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.react.modules.common.ModuleDataCleaner; -import java.util.ArrayDeque; -import java.util.HashSet; -import java.util.concurrent.Executor; - -@ReactModule(name = AsyncStorageModule.NAME) -public final class AsyncStorageModule extends NativeAsyncSQLiteDBStorageSpec - implements ModuleDataCleaner.Cleanable { - - public static final String NAME = "AsyncSQLiteDBStorage"; - - // SQL variable number limit, defined by SQLITE_LIMIT_VARIABLE_NUMBER: - // https://raw.githubusercontent.com/android/platform_external_sqlite/master/dist/sqlite3.c - private static final int MAX_SQL_KEYS = 999; - - private ReactDatabaseSupplier mReactDatabaseSupplier; - private boolean mShuttingDown = false; - - // Adapted from - // https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237 - private class SerialExecutor implements Executor { - private final ArrayDeque mTasks = new ArrayDeque(); - private Runnable mActive; - private final Executor executor; - - SerialExecutor(Executor executor) { - this.executor = executor; - } - - public synchronized void execute(final Runnable r) { - mTasks.offer( - new Runnable() { - public void run() { - try { - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - - synchronized void scheduleNext() { - if ((mActive = mTasks.poll()) != null) { - executor.execute(mActive); - } - } - } - - private final SerialExecutor executor; - - public AsyncStorageModule(ReactApplicationContext reactContext) { - this(reactContext, AsyncTask.THREAD_POOL_EXECUTOR); - } - - @VisibleForTesting - AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) { - super(reactContext); - this.executor = new SerialExecutor(executor); - mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public void initialize() { - super.initialize(); - mShuttingDown = false; - } - - @Override - public void invalidate() { - mShuttingDown = true; - } - - @Override - public void clearSensitiveData() { - // Clear local storage. If fails, crash, since the app is potentially in a bad state and could - // cause a privacy violation. We're still not recovering from this well, but at least the error - // will be reported to the server. - mReactDatabaseSupplier.clearAndCloseDatabase(); - } - - /** - * Given an array of keys, this returns a map of (key, value) pairs for the keys found, and (key, - * null) for the keys that haven't been found. - */ - @Override - public void multiGet(final ReadableArray keys, final Callback callback) { - if (keys == null) { - callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null), null); - return; - } - - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); - return; - } - - String[] columns = {KEY_COLUMN, VALUE_COLUMN}; - HashSet keysRemaining = new HashSet<>(); - WritableArray data = Arguments.createArray(); - for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { - int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); - Cursor cursor = - mReactDatabaseSupplier - .get() - .query( - TABLE_CATALYST, - columns, - AsyncLocalStorageUtil.buildKeySelection(keyCount), - AsyncLocalStorageUtil.buildKeySelectionArgs(keys, keyStart, keyCount), - null, - null, - null); - keysRemaining.clear(); - try { - if (cursor.getCount() != keys.size()) { - // some keys have not been found - insert them with null into the final array - for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) { - keysRemaining.add(keys.getString(keyIndex)); - } - } - - if (cursor.moveToFirst()) { - do { - WritableArray row = Arguments.createArray(); - row.pushString(cursor.getString(0)); - row.pushString(cursor.getString(1)); - data.pushArray(row); - keysRemaining.remove(cursor.getString(0)); - } while (cursor.moveToNext()); - } - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); - return; - } finally { - cursor.close(); - } - - for (String key : keysRemaining) { - WritableArray row = Arguments.createArray(); - row.pushString(key); - row.pushNull(); - data.pushArray(row); - } - keysRemaining.clear(); - } - - callback.invoke(null, data); - } - }.executeOnExecutor(executor); - } - - /** - * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will - * return AsyncLocalStorageFailure, but all other pairs will have been inserted. The insertion - * will replace conflicting (key, value) pairs. - */ - @Override - public void multiSet(final ReadableArray keyValueArray, final Callback callback) { - if (keyValueArray.size() == 0) { - callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); - return; - } - - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null)); - return; - } - - String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; - SQLiteStatement statement = mReactDatabaseSupplier.get().compileStatement(sql); - WritableMap error = null; - try { - mReactDatabaseSupplier.get().beginTransaction(); - for (int idx = 0; idx < keyValueArray.size(); idx++) { - if (keyValueArray.getArray(idx).size() != 2) { - error = AsyncStorageErrorUtil.getInvalidValueError(null); - return; - } - if (keyValueArray.getArray(idx).getString(0) == null) { - error = AsyncStorageErrorUtil.getInvalidKeyError(null); - return; - } - if (keyValueArray.getArray(idx).getString(1) == null) { - error = AsyncStorageErrorUtil.getInvalidValueError(null); - return; - } - - statement.clearBindings(); - statement.bindString(1, keyValueArray.getArray(idx).getString(0)); - statement.bindString(2, keyValueArray.getArray(idx).getString(1)); - statement.execute(); - } - mReactDatabaseSupplier.get().setTransactionSuccessful(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } finally { - try { - mReactDatabaseSupplier.get().endTransaction(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - if (error == null) { - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } - } - } - if (error != null) { - callback.invoke(error); - } else { - callback.invoke(); - } - } - }.executeOnExecutor(executor); - } - - /** Removes all rows of the keys given. */ - @Override - public void multiRemove(final ReadableArray keys, final Callback callback) { - if (keys.size() == 0) { - callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); - return; - } - - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null)); - return; - } - - WritableMap error = null; - try { - mReactDatabaseSupplier.get().beginTransaction(); - for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { - int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); - mReactDatabaseSupplier - .get() - .delete( - TABLE_CATALYST, - AsyncLocalStorageUtil.buildKeySelection(keyCount), - AsyncLocalStorageUtil.buildKeySelectionArgs(keys, keyStart, keyCount)); - } - mReactDatabaseSupplier.get().setTransactionSuccessful(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } finally { - try { - mReactDatabaseSupplier.get().endTransaction(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - if (error == null) { - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } - } - } - if (error != null) { - callback.invoke(error); - } else { - callback.invoke(); - } - } - }.executeOnExecutor(executor); - } - - /** - * Given an array of (key, value) pairs, this will merge the given values with the stored values - * of the given keys, if they exist. - */ - @Override - public void multiMerge(final ReadableArray keyValueArray, final Callback callback) { - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null)); - return; - } - WritableMap error = null; - try { - mReactDatabaseSupplier.get().beginTransaction(); - for (int idx = 0; idx < keyValueArray.size(); idx++) { - if (keyValueArray.getArray(idx).size() != 2) { - error = AsyncStorageErrorUtil.getInvalidValueError(null); - return; - } - - if (keyValueArray.getArray(idx).getString(0) == null) { - error = AsyncStorageErrorUtil.getInvalidKeyError(null); - return; - } - - if (keyValueArray.getArray(idx).getString(1) == null) { - error = AsyncStorageErrorUtil.getInvalidValueError(null); - return; - } - - if (!AsyncLocalStorageUtil.mergeImpl( - mReactDatabaseSupplier.get(), - keyValueArray.getArray(idx).getString(0), - keyValueArray.getArray(idx).getString(1))) { - error = AsyncStorageErrorUtil.getDBError(null); - return; - } - } - mReactDatabaseSupplier.get().setTransactionSuccessful(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } finally { - try { - mReactDatabaseSupplier.get().endTransaction(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - if (error == null) { - error = AsyncStorageErrorUtil.getError(null, e.getMessage()); - } - } - } - if (error != null) { - callback.invoke(error); - } else { - callback.invoke(); - } - } - }.executeOnExecutor(executor); - } - - /** Clears the database. */ - @Override - public void clear(final Callback callback) { - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!mReactDatabaseSupplier.ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null)); - return; - } - try { - mReactDatabaseSupplier.clear(); - callback.invoke(); - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); - } - } - }.executeOnExecutor(executor); - } - - /** Returns an array with all keys from the database. */ - @Override - public void getAllKeys(final Callback callback) { - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); - return; - } - WritableArray data = Arguments.createArray(); - String[] columns = {KEY_COLUMN}; - Cursor cursor = - mReactDatabaseSupplier - .get() - .query(TABLE_CATALYST, columns, null, null, null, null, null); - try { - if (cursor.moveToFirst()) { - do { - data.pushString(cursor.getString(0)); - } while (cursor.moveToNext()); - } - } catch (Exception e) { - FLog.w(ReactConstants.TAG, e.getMessage(), e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); - return; - } finally { - cursor.close(); - } - callback.invoke(null, data); - } - }.executeOnExecutor(executor); - } - - /** Verify the database is open for reads and writes. */ - private boolean ensureDatabase() { - return !mShuttingDown && mReactDatabaseSupplier.ensureDatabase(); - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK deleted file mode 100644 index 7df1b3640bf9ce..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK +++ /dev/null @@ -1,25 +0,0 @@ -load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_root_target", "react_native_target", "rn_android_library") - -rn_android_library( - name = "storage", - srcs = glob(["**/*.java"]), - autoglob = False, - labels = [ - "pfh:ReactNative_CommonInfrastructurePlaceholder", - ], - language = "JAVA", - visibility = [ - "PUBLIC", - ], - deps = [ - react_native_dep("third-party/android/androidx:annotation"), - react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"), - react_native_dep("third-party/java/infer-annotations:infer-annotations"), - react_native_dep("third-party/java/jsr-305:jsr-305"), - react_native_target("java/com/facebook/react/bridge:bridge"), - react_native_target("java/com/facebook/react/common:common"), - react_native_target("java/com/facebook/react/module/annotations:annotations"), - react_native_target("java/com/facebook/react/modules/common:common"), - ], - exported_deps = [react_native_root_target(":FBReactNativeSpec")], -) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java deleted file mode 100644 index ccf9fb9fe2aa1f..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.storage; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.react.common.ReactConstants; - -/** - * Database supplier of the database used by react native. This creates, opens and deletes the - * database as necessary. - */ -public class ReactDatabaseSupplier extends SQLiteOpenHelper { - - // VisibleForTesting - public static final String DATABASE_NAME = "RKStorage"; - - private static final int DATABASE_VERSION = 1; - private static final int SLEEP_TIME_MS = 30; - - static final String TABLE_CATALYST = "catalystLocalStorage"; - static final String KEY_COLUMN = "key"; - static final String VALUE_COLUMN = "value"; - - static final String VERSION_TABLE_CREATE = - "CREATE TABLE " - + TABLE_CATALYST - + " (" - + KEY_COLUMN - + " TEXT PRIMARY KEY, " - + VALUE_COLUMN - + " TEXT NOT NULL" - + ")"; - - private static @Nullable ReactDatabaseSupplier sReactDatabaseSupplierInstance; - - private Context mContext; - private @Nullable SQLiteDatabase mDb; - private long mMaximumDatabaseSize = 6L * 1024L * 1024L; // 6 MB in bytes - - private ReactDatabaseSupplier(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mContext = context; - } - - public static ReactDatabaseSupplier getInstance(Context context) { - if (sReactDatabaseSupplierInstance == null) { - sReactDatabaseSupplierInstance = new ReactDatabaseSupplier(context.getApplicationContext()); - } - return sReactDatabaseSupplierInstance; - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(VERSION_TABLE_CREATE); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion != newVersion) { - deleteDatabase(); - onCreate(db); - } - } - - /** Verify the database exists and is open. */ - /* package */ synchronized boolean ensureDatabase() { - if (mDb != null && mDb.isOpen()) { - return true; - } - // Sometimes retrieving the database fails. We do 2 retries: first without database deletion - // and then with deletion. - SQLiteException lastSQLiteException = null; - for (int tries = 0; tries < 2; tries++) { - try { - if (tries > 0) { - deleteDatabase(); - } - mDb = getWritableDatabase(); - break; - } catch (SQLiteException e) { - lastSQLiteException = e; - } - // Wait before retrying. - try { - Thread.sleep(SLEEP_TIME_MS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - if (mDb == null) { - throw lastSQLiteException; - } - // This is a sane limit to protect the user from the app storing too much data in the database. - // This also protects the database from filling up the disk cache and becoming malformed - // (endTransaction() calls will throw an exception, not rollback, and leave the db malformed). - mDb.setMaximumSize(mMaximumDatabaseSize); - return true; - } - - /** Create and/or open the database. */ - public synchronized SQLiteDatabase get() { - ensureDatabase(); - return mDb; - } - - public synchronized void clearAndCloseDatabase() throws RuntimeException { - try { - clear(); - closeDatabase(); - FLog.d(ReactConstants.TAG, "Cleaned " + DATABASE_NAME); - } catch (Exception e) { - // Clearing the database has failed, delete it instead. - if (deleteDatabase()) { - FLog.d(ReactConstants.TAG, "Deleted Local Database " + DATABASE_NAME); - return; - } - // Everything failed, throw - throw new RuntimeException("Clearing and deleting database " + DATABASE_NAME + " failed"); - } - } - - /* package */ synchronized void clear() { - get().delete(TABLE_CATALYST, null, null); - } - - /** - * Sets the maximum size the database will grow to. The maximum size cannot be set below the - * current size. - */ - public synchronized void setMaximumSize(long size) { - mMaximumDatabaseSize = size; - if (mDb != null) { - mDb.setMaximumSize(mMaximumDatabaseSize); - } - } - - private synchronized boolean deleteDatabase() { - closeDatabase(); - return mContext.deleteDatabase(DATABASE_NAME); - } - - private synchronized void closeDatabase() { - if (mDb != null && mDb.isOpen()) { - mDb.close(); - mDb = null; - } - } - - // For testing purposes only! - public static void deleteInstance() { - sReactDatabaseSupplierInstance = null; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index c79b69687e0654..f6d575f9aff635 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -46,7 +46,6 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/permissions:permissions"), react_native_target("java/com/facebook/react/modules/share:share"), react_native_target("java/com/facebook/react/modules/statusbar:statusbar"), - react_native_target("java/com/facebook/react/modules/storage:storage"), react_native_target("java/com/facebook/react/modules/sound:sound"), react_native_target("java/com/facebook/react/modules/toast:toast"), react_native_target("java/com/facebook/react/modules/vibration:vibration"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 6fc4a536e2b1f7..f845a46118cfd8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -33,7 +33,6 @@ import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.sound.SoundManagerModule; import com.facebook.react.modules.statusbar.StatusBarModule; -import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; import com.facebook.react.modules.vibration.VibrationModule; import com.facebook.react.modules.websocket.WebSocketModule; @@ -69,7 +68,6 @@ AppStateModule.class, BlobModule.class, FileReaderModule.class, - AsyncStorageModule.class, ClipboardModule.class, DialogModule.class, FrescoModule.class, @@ -111,8 +109,6 @@ public MainReactPackage(MainPackageConfig config) { return new BlobModule(context); case FileReaderModule.NAME: return new FileReaderModule(context); - case AsyncStorageModule.NAME: - return new AsyncStorageModule(context); case ClipboardModule.NAME: return new ClipboardModule(context); case DialogModule.NAME: @@ -193,7 +189,6 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { AppStateModule.class, BlobModule.class, FileReaderModule.class, - AsyncStorageModule.class, ClipboardModule.class, DialogModule.class, FrescoModule.class, diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 7ab27c360a9714..235079472f328d 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -37,7 +37,6 @@ rn_robolectric_test( react_native_target("java/com/facebook/react/modules/dialog:dialog"), react_native_target("java/com/facebook/react/modules/network:network"), react_native_target("java/com/facebook/react/modules/share:share"), - react_native_target("java/com/facebook/react/modules/storage:storage"), react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"), react_native_target("java/com/facebook/react/touch:touch"), react_native_target("java/com/facebook/react/uimanager:uimanager"), diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java deleted file mode 100644 index d4b2ee2c4c4563..00000000000000 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.storage; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.JavaOnlyArray; -import com.facebook.react.bridge.JavaOnlyMap; -import com.facebook.react.bridge.ReactTestHelper; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.json.JSONArray; -import org.json.JSONObject; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.mockito.verification.VerificationMode; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.rule.PowerMockRule; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.android.util.concurrent.RoboExecutorService; - -/** Tests for {@link com.facebook.react.modules.storage.AsyncStorageModule}. */ -@PrepareForTest({Arguments.class}) -@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "android.*", "org.json.*"}) -@RunWith(RobolectricTestRunner.class) -public class AsyncStorageModuleTest { - - private AsyncStorageModule mStorage; - private JavaOnlyArray mEmptyArray; - - @Rule public PowerMockRule rule = new PowerMockRule(); - - @Before - public void prepareModules() { - PowerMockito.mockStatic(Arguments.class); - Mockito.when(Arguments.createArray()) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return new JavaOnlyArray(); - } - }); - - Mockito.when(Arguments.createMap()) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return new JavaOnlyMap(); - } - }); - - // don't use Robolectric before initializing mocks - mStorage = - new AsyncStorageModule( - ReactTestHelper.createCatalystContextForTest(), new RoboExecutorService()); - mEmptyArray = new JavaOnlyArray(); - } - - @After - public void cleanUp() { - RuntimeEnvironment.application.deleteDatabase(ReactDatabaseSupplier.DATABASE_NAME); - ReactDatabaseSupplier.deleteInstance(); - } - - @Test - public void testMultiSetMultiGet() { - final String key1 = "foo1"; - final String key2 = "foo2"; - final String fakeKey = "fakeKey"; - final String value1 = "bar1"; - final String value2 = "bar2"; - JavaOnlyArray keyValues = new JavaOnlyArray(); - keyValues.pushArray(getArray(key1, value1)); - keyValues.pushArray(getArray(key2, value2)); - - Callback setCallback = mock(Callback.class); - mStorage.multiSet(keyValues, setCallback); - verify(setCallback, Mockito.times(1)).invoke(); - - JavaOnlyArray keys = new JavaOnlyArray(); - keys.pushString(key1); - keys.pushString(key2); - - Callback getCallback = mock(Callback.class); - mStorage.multiGet(keys, getCallback); - verify(getCallback, Mockito.times(1)).invoke(null, keyValues); - - keys.pushString(fakeKey); - JavaOnlyArray row3 = new JavaOnlyArray(); - row3.pushString(fakeKey); - row3.pushString(null); - keyValues.pushArray(row3); - - Callback getCallback2 = mock(Callback.class); - mStorage.multiGet(keys, getCallback2); - verify(getCallback2, Mockito.times(1)).invoke(null, keyValues); - } - - @Test - public void testMultiRemove() { - final String key1 = "foo1"; - final String key2 = "foo2"; - final String value1 = "bar1"; - final String value2 = "bar2"; - - JavaOnlyArray keyValues = new JavaOnlyArray(); - keyValues.pushArray(getArray(key1, value1)); - keyValues.pushArray(getArray(key2, value2)); - mStorage.multiSet(keyValues, mock(Callback.class)); - - JavaOnlyArray keys = new JavaOnlyArray(); - keys.pushString(key1); - keys.pushString(key2); - - Callback getCallback = mock(Callback.class); - mStorage.multiRemove(keys, getCallback); - verify(getCallback, Mockito.times(1)).invoke(); - - Callback getAllCallback = mock(Callback.class); - mStorage.getAllKeys(getAllCallback); - verify(getAllCallback, Mockito.times(1)).invoke(null, mEmptyArray); - - mStorage.multiSet(keyValues, mock(Callback.class)); - - keys.pushString("fakeKey"); - Callback getCallback2 = mock(Callback.class); - mStorage.multiRemove(keys, getCallback2); - verify(getCallback2, Mockito.times(1)).invoke(); - - Callback getAllCallback2 = mock(Callback.class); - mStorage.getAllKeys(getAllCallback2); - verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray); - } - - @Test - public void testMultiMerge() throws Exception { - final String mergeKey = "mergeTest"; - - JSONObject value = new JSONObject(); - value.put("foo1", "bar1"); - value.put("foo2", createJSONArray("val1", "val2", 3)); - value.put("foo3", 1001); - value.put("foo4", createJSONObject("key1", "randomValueThatWillNeverBeUsed")); - - mStorage.multiSet(JavaOnlyArray.of(getArray(mergeKey, value.toString())), mock(Callback.class)); - { - Callback callback = mock(Callback.class); - mStorage.multiGet(getArray(mergeKey), callback); - verify(callback, Mockito.times(1)) - .invoke(null, JavaOnlyArray.of(getArray(mergeKey, value.toString()))); - } - - value.put("foo1", 1001); - value.put("foo2", createJSONObject("key1", "val1")); - value.put("foo3", "bar1"); - value.put("foo4", createJSONArray("val1", "val2", 3)); - - JSONObject newValue = new JSONObject(); - newValue.put("foo2", createJSONObject("key2", "val2")); - - JSONObject newValue2 = new JSONObject(); - newValue2.put("foo2", createJSONObject("key1", "val3")); - - mStorage.multiMerge( - JavaOnlyArray.of( - JavaOnlyArray.of(mergeKey, value.toString()), - JavaOnlyArray.of(mergeKey, newValue.toString()), - JavaOnlyArray.of(mergeKey, newValue2.toString())), - mock(Callback.class)); - - value.put("foo2", createJSONObject("key1", "val3", "key2", "val2")); - Callback callback = mock(Callback.class); - mStorage.multiGet(getArray(mergeKey), callback); - verify(callback, Mockito.times(1)) - .invoke(null, JavaOnlyArray.of(getArray(mergeKey, value.toString()))); - } - - @Test - public void testGetAllKeys() { - final String[] keys = {"foo", "foo2"}; - final String[] values = {"bar", "bar2"}; - JavaOnlyArray keyValues = new JavaOnlyArray(); - keyValues.pushArray(getArray(keys[0], values[0])); - keyValues.pushArray(getArray(keys[1], values[1])); - mStorage.multiSet(keyValues, mock(Callback.class)); - - JavaOnlyArray storedKeys = new JavaOnlyArray(); - storedKeys.pushString(keys[0]); - storedKeys.pushString(keys[1]); - - Callback getAllCallback = mock(Callback.class); - mStorage.getAllKeys(getAllCallback); - verify(getAllCallback, Mockito.times(1)).invoke(null, storedKeys); - - Callback getAllCallback2 = mock(Callback.class); - mStorage.multiRemove(getArray(keys[0]), mock(Callback.class)); - - mStorage.getAllKeys(getAllCallback2); - verify(getAllCallback2, Mockito.times(1)).invoke(null, getArray(keys[1])); - - mStorage.multiRemove(getArray(keys[1]), mock(Callback.class)); - Callback getAllCallback3 = mock(Callback.class); - mStorage.getAllKeys(getAllCallback3); - verify(getAllCallback3, Mockito.times(1)).invoke(null, mEmptyArray); - } - - @Test - public void testClear() { - JavaOnlyArray keyValues = new JavaOnlyArray(); - keyValues.pushArray(getArray("foo", "foo2")); - keyValues.pushArray(getArray("bar", "bar2")); - mStorage.multiSet(keyValues, mock(Callback.class)); - - Callback clearCallback2 = mock(Callback.class); - mStorage.clear(clearCallback2); - verify(clearCallback2, Mockito.times(1)).invoke(); - - Callback getAllCallback2 = mock(Callback.class); - mStorage.getAllKeys(getAllCallback2); - verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray); - } - - @Test - public void testHugeMultiGetMultiGet() { - // Test with many keys, so that it's above the 999 limit per batch imposed by SQLite. - final int keyCount = 1001; - // don't set keys that divide by this magical number, so that we can check that multiGet works, - // and returns null for missing keys - final int magicalNumber = 343; - - JavaOnlyArray keyValues = new JavaOnlyArray(); - for (int i = 0; i < keyCount; i++) { - if (i % magicalNumber > 0) { - keyValues.pushArray(getArray("key" + i, "value" + i)); - } - } - mStorage.multiSet(keyValues, mock(Callback.class)); - JavaOnlyArray keys = new JavaOnlyArray(); - for (int i = 0; i < keyCount; i++) { - keys.pushString("key" + i); - } - mStorage.multiGet( - keys, - new Callback() { - @Override - public void invoke(Object... args) { - assertThat(args.length).isEqualTo(2); - JavaOnlyArray resultArray = (JavaOnlyArray) args[1]; - - assertThat(resultArray.size()).isEqualTo(keyCount); - boolean keyReceived[] = new boolean[keyCount]; - for (int i = 0; i < keyCount; i++) { - String key = resultArray.getArray(i).getString(0).substring(3); - int idx = Integer.parseInt(key); - assertThat(keyReceived[idx]).isFalse(); - keyReceived[idx] = true; - - if (idx % magicalNumber > 0) { - String value = resultArray.getArray(i).getString(1).substring(5); - assertThat(key).isEqualTo(value); - } else { - assertThat(resultArray.getArray(i).isNull(1)); - } - } - } - }); - - // Test removal in same test, since it's costly to set up the test again. - // Remove only odd keys - JavaOnlyArray keyRemoves = new JavaOnlyArray(); - for (int i = 0; i < keyCount; i++) { - if (i % 2 > 0) { - keyRemoves.pushString("key" + i); - } - } - mStorage.multiRemove(keyRemoves, mock(Callback.class)); - mStorage.getAllKeys( - new Callback() { - @Override - public void invoke(Object... args) { - JavaOnlyArray resultArray = (JavaOnlyArray) args[1]; - assertThat(resultArray.size()).isEqualTo(499); - for (int i = 0; i < resultArray.size(); i++) { - String key = resultArray.getString(i).substring(3); - int idx = Integer.parseInt(key); - assertThat(idx % 2).isEqualTo(0); - } - } - }); - } - - private static JSONArray createJSONArray(Object... objects) { - return new JSONArray(Arrays.asList(objects)); - } - - private static JSONObject createJSONObject(Object... keysAndValues) { - if (keysAndValues.length % 2 != 0) { - throw new IllegalArgumentException("You must provide the same number of keys and values"); - } - Map map = new HashMap(); - for (int i = 0; i < keysAndValues.length; i += 2) { - map.put(keysAndValues[i], keysAndValues[i + 1]); - } - return new JSONObject(map); - } - - private JavaOnlyArray getArray(String... values) { - JavaOnlyArray array = new JavaOnlyArray(); - for (String value : values) { - array.pushString(value); - } - return array; - } - - private static void waitForAsync() { - Robolectric.flushForegroundThreadScheduler(); - Robolectric.flushBackgroundThreadScheduler(); - } - - private static T verify(T mock, VerificationMode mode) { - waitForAsync(); - return Mockito.verify(mock, mode); - } -}