/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.react.modules.storage; import javax.annotation.Nullable; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; 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; } }