/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.documentsui.queries; import static com.android.documentsui.base.SharedMinimal.DEBUG; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.documentsui.R; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A manager used to manage search history data. */ public class SearchHistoryManager { private static final String TAG = "SearchHistoryManager"; private static final String[] PROJECTION_HISTORY = new String[]{ DatabaseHelper.COLUMN_KEYWORD, DatabaseHelper.COLUMN_LAST_UPDATED_TIME }; private static SearchHistoryManager sManager; private final DatabaseHelper mHelper; private final int mLimitedHistoryCount; @GuardedBy("mLock") private final List mHistory = Collections.synchronizedList(new ArrayList<>()); private final Object mLock = new Object(); private DatabaseChangedListener mListener; private enum DATABASE_OPERATION { QUERY, ADD, DELETE, UPDATE } private SearchHistoryManager(Context context) { mHelper = new DatabaseHelper(context); mLimitedHistoryCount = context.getResources().getInteger( R.integer.config_maximum_search_history); } /** * Get the singleton instance of SearchHistoryManager. * * @return the singleton instance, guaranteed not null */ public static SearchHistoryManager getInstance(Context context) { synchronized (SearchHistoryManager.class) { if (sManager == null) { sManager = new SearchHistoryManager(context); sManager.new DatabaseTask(null, DATABASE_OPERATION.QUERY).executeOnExecutor( AsyncTask.SERIAL_EXECUTOR); } return sManager; } } private static class DatabaseHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 1; private static final String COLUMN_KEYWORD = "keyword"; private static final String COLUMN_LAST_UPDATED_TIME = "last_updated_time"; private static final String HISTORY_DATABASE = "search_history.db"; private static final String HISTORY_TABLE = "search_history"; private DatabaseHelper(Context context) { super(context, HISTORY_DATABASE, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + HISTORY_TABLE + " (" + COLUMN_KEYWORD + " TEXT NOT NULL, " + COLUMN_LAST_UPDATED_TIME + " INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //TODO: Doing database backup/restore data migration or upgrade with b/121987495 if (DEBUG) { Log.w(TAG, "Upgrading database..., Old version = " + oldVersion + ", New version = " + newVersion); } db.execSQL("DROP TABLE IF EXISTS " + HISTORY_TABLE); onCreate(db); } } /** * Get search history list with/without filter text. * @param filter the filter text * @return a list of search history */ public List getHistoryList(@Nullable String filter) { synchronized (mLock) { if (!TextUtils.isEmpty(filter)) { final List filterKeyword = Collections.synchronizedList(new ArrayList<>()); final String keyword = filter; for (String history : mHistory) { if (history.contains(keyword)) { filterKeyword.add(history); } } return filterKeyword; } else { return Collections.synchronizedList(new ArrayList<>(mHistory)); } } } /** * Add search keyword text to list. * @param keyword the text to be added */ public void addHistory(String keyword) { synchronized (mLock) { if (mHistory.remove(keyword)) { mHistory.add(0, keyword); new DatabaseTask(keyword, DATABASE_OPERATION.UPDATE).executeOnExecutor( AsyncTask.SERIAL_EXECUTOR); } else { if (mHistory.size() >= mLimitedHistoryCount) { new DatabaseTask(mHistory.remove(mHistory.size() - 1), DATABASE_OPERATION.DELETE).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, Boolean.FALSE); Log.w(TAG, "Over search history count !! keyword = " + keyword + "has been deleted"); } mHistory.add(0, keyword); new DatabaseTask(keyword, DATABASE_OPERATION.ADD).executeOnExecutor( AsyncTask.SERIAL_EXECUTOR); } } } /** * Delete search keyword text from list. * @param keyword the text to be deleted */ public void deleteHistory(String keyword) { synchronized (mLock) { if (mHistory.remove(keyword)) { new DatabaseTask(keyword, DATABASE_OPERATION.DELETE).executeOnExecutor( AsyncTask.SERIAL_EXECUTOR); } } } private class DatabaseTask extends AsyncTask { private final String mKeyword; private final DATABASE_OPERATION mOperation; public DatabaseTask(String keyword, DATABASE_OPERATION operation) { mKeyword = keyword; mOperation = operation; } private Cursor getSortedHistoryList() { final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(DatabaseHelper.HISTORY_TABLE); return queryBuilder.query(mHelper.getReadableDatabase(), PROJECTION_HISTORY, null, null, null, null, DatabaseHelper.COLUMN_LAST_UPDATED_TIME + " DESC"); } private void addDatabaseData() { final ContentValues values = new ContentValues(); values.put(DatabaseHelper.COLUMN_KEYWORD, mKeyword); values.put(DatabaseHelper.COLUMN_LAST_UPDATED_TIME, System.currentTimeMillis()); final long rowId = mHelper.getWritableDatabase().insert( DatabaseHelper.HISTORY_TABLE, null, values); if (rowId == -1) { Log.w(TAG, "Failed to add " + mKeyword + "to database!"); } if (mListener != null) { mListener.onAddChangedListener(rowId); } } private void deleteDatabaseData() { // We only care about the field of DatabaseHelper.COLUMN_KEYWORD for deleting StringBuilder selection = new StringBuilder(); selection.append(DatabaseHelper.COLUMN_KEYWORD).append("=?"); final int numberOfRows = mHelper.getWritableDatabase().delete( DatabaseHelper.HISTORY_TABLE, selection.toString(), new String[] { mKeyword }); if (numberOfRows == 0) { Log.w(TAG, "Failed to delete " + mKeyword + "from database!"); } if (mListener != null) { mListener.onDeleteChangedListener(numberOfRows); } } private void updateDatabaseData() { // We just need to update the field DatabaseHelper.COLUMN_LAST_UPDATED_TIME, // because we will sort by last modified when retrieving from database ContentValues values = new ContentValues(); values.put(DatabaseHelper.COLUMN_LAST_UPDATED_TIME, System.currentTimeMillis()); StringBuilder selection = new StringBuilder(); selection.append(DatabaseHelper.COLUMN_KEYWORD).append("=?"); final int numberOfRows = mHelper.getWritableDatabase().update( DatabaseHelper.HISTORY_TABLE, values, selection.toString(), new String[] { mKeyword }); if (numberOfRows == 0) { Log.w(TAG, "Failed to update " + mKeyword + "to database!"); } } private void parseHistoryFromCursor(Cursor cursor) { if (cursor == null) { if (DEBUG) { Log.e(TAG, "Null cursor happens when building local search history List!"); } return; } synchronized (mLock) { mHistory.clear(); try { while (cursor.moveToNext()) { mHistory.add(cursor.getString(cursor.getColumnIndex( DatabaseHelper.COLUMN_KEYWORD))); } } finally { cursor.close(); } } } @Override protected Void doInBackground(Object... params) { if (!TextUtils.isEmpty(mKeyword)) { switch (mOperation) { case ADD: addDatabaseData(); break; case DELETE: deleteDatabaseData(); break; case UPDATE: updateDatabaseData(); break; default: break; } } // params[0] is used to preventing reload twice when deleting over history count if (params.length <= 0 || (params.length > 0 && ((Boolean)params[0]).booleanValue())) { parseHistoryFromCursor(getSortedHistoryList()); } return null; } @Override protected void onPostExecute(Object result) { if (mListener != null) { mListener.onPostExecute(); } } } @VisibleForTesting public void setDatabaseListener(DatabaseChangedListener listener) { mListener = listener; } interface DatabaseChangedListener { void onAddChangedListener(long longResult); void onDeleteChangedListener(int intResult); void onPostExecute(); } }