blob: 26def876d2912de39ae2e8e28cc8c1bcdc7ef235 [file] [log] [blame]
/*
* 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<String> 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<String> getHistoryList(@Nullable String filter) {
synchronized (mLock) {
if (!TextUtils.isEmpty(filter)) {
final List<String> 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<Object, Void, Object> {
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();
}
}