1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.documentsui.queries; 18 19 import static com.android.documentsui.base.SharedMinimal.DEBUG; 20 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.database.sqlite.SQLiteDatabase; 25 import android.database.sqlite.SQLiteOpenHelper; 26 import android.database.sqlite.SQLiteQueryBuilder; 27 import android.os.AsyncTask; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import androidx.annotation.GuardedBy; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.documentsui.R; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 41 /** 42 * A manager used to manage search history data. 43 */ 44 public class SearchHistoryManager { 45 46 private static final String TAG = "SearchHistoryManager"; 47 48 private static final String[] PROJECTION_HISTORY = new String[]{ 49 DatabaseHelper.COLUMN_KEYWORD, DatabaseHelper.COLUMN_LAST_UPDATED_TIME 50 }; 51 52 private static SearchHistoryManager sManager; 53 private final DatabaseHelper mHelper; 54 private final int mLimitedHistoryCount; 55 @GuardedBy("mLock") 56 private final List<String> mHistory = Collections.synchronizedList(new ArrayList<>()); 57 private final Object mLock = new Object(); 58 private DatabaseChangedListener mListener; 59 60 private enum DATABASE_OPERATION { 61 QUERY, ADD, DELETE, UPDATE 62 } 63 SearchHistoryManager(Context context)64 private SearchHistoryManager(Context context) { 65 mHelper = new DatabaseHelper(context); 66 mLimitedHistoryCount = context.getResources().getInteger( 67 R.integer.config_maximum_search_history); 68 } 69 70 /** 71 * Get the singleton instance of SearchHistoryManager. 72 * 73 * @return the singleton instance, guaranteed not null 74 */ getInstance(Context context)75 public static SearchHistoryManager getInstance(Context context) { 76 synchronized (SearchHistoryManager.class) { 77 if (sManager == null) { 78 sManager = new SearchHistoryManager(context); 79 sManager.new DatabaseTask(null, DATABASE_OPERATION.QUERY).executeOnExecutor( 80 AsyncTask.SERIAL_EXECUTOR); 81 } 82 return sManager; 83 } 84 } 85 86 private static class DatabaseHelper extends SQLiteOpenHelper { 87 88 private static final int DATABASE_VERSION = 1; 89 private static final String COLUMN_KEYWORD = "keyword"; 90 private static final String COLUMN_LAST_UPDATED_TIME = "last_updated_time"; 91 private static final String HISTORY_DATABASE = "search_history.db"; 92 private static final String HISTORY_TABLE = "search_history"; 93 DatabaseHelper(Context context)94 private DatabaseHelper(Context context) { 95 super(context, HISTORY_DATABASE, null, DATABASE_VERSION); 96 } 97 98 @Override onCreate(SQLiteDatabase db)99 public void onCreate(SQLiteDatabase db) { 100 db.execSQL("CREATE TABLE " + HISTORY_TABLE + " (" + COLUMN_KEYWORD + " TEXT NOT NULL, " 101 + COLUMN_LAST_UPDATED_TIME + " INTEGER)"); 102 } 103 104 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)105 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 106 //TODO: Doing database backup/restore data migration or upgrade with b/121987495 107 108 if (DEBUG) { 109 Log.w(TAG, "Upgrading database..., Old version = " + oldVersion 110 + ", New version = " + newVersion); 111 } 112 db.execSQL("DROP TABLE IF EXISTS " + HISTORY_TABLE); 113 onCreate(db); 114 } 115 } 116 117 /** 118 * Get search history list with/without filter text. 119 * @param filter the filter text 120 * @return a list of search history 121 */ getHistoryList(@ullable String filter)122 public List<String> getHistoryList(@Nullable String filter) { 123 synchronized (mLock) { 124 if (!TextUtils.isEmpty(filter)) { 125 final List<String> filterKeyword = Collections.synchronizedList(new ArrayList<>()); 126 final String keyword = filter; 127 for (String history : mHistory) { 128 if (history.contains(keyword)) { 129 filterKeyword.add(history); 130 } 131 } 132 return filterKeyword; 133 } else { 134 return Collections.synchronizedList(new ArrayList<>(mHistory)); 135 } 136 } 137 } 138 139 /** 140 * Add search keyword text to list. 141 * @param keyword the text to be added 142 */ addHistory(String keyword)143 public void addHistory(String keyword) { 144 synchronized (mLock) { 145 if (mHistory.remove(keyword)) { 146 mHistory.add(0, keyword); 147 new DatabaseTask(keyword, DATABASE_OPERATION.UPDATE).executeOnExecutor( 148 AsyncTask.SERIAL_EXECUTOR); 149 } else { 150 if (mHistory.size() >= mLimitedHistoryCount) { 151 new DatabaseTask(mHistory.remove(mHistory.size() - 1), 152 DATABASE_OPERATION.DELETE).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, 153 Boolean.FALSE); 154 155 Log.w(TAG, "Over search history count !! keyword = " + keyword 156 + "has been deleted"); 157 } 158 mHistory.add(0, keyword); 159 new DatabaseTask(keyword, DATABASE_OPERATION.ADD).executeOnExecutor( 160 AsyncTask.SERIAL_EXECUTOR); 161 } 162 } 163 } 164 165 /** 166 * Delete search keyword text from list. 167 * @param keyword the text to be deleted 168 */ deleteHistory(String keyword)169 public void deleteHistory(String keyword) { 170 synchronized (mLock) { 171 if (mHistory.remove(keyword)) { 172 new DatabaseTask(keyword, DATABASE_OPERATION.DELETE).executeOnExecutor( 173 AsyncTask.SERIAL_EXECUTOR); 174 } 175 } 176 } 177 178 private class DatabaseTask extends AsyncTask<Object, Void, Object> { 179 private final String mKeyword; 180 private final DATABASE_OPERATION mOperation; 181 DatabaseTask(String keyword, DATABASE_OPERATION operation)182 public DatabaseTask(String keyword, DATABASE_OPERATION operation) { 183 mKeyword = keyword; 184 mOperation = operation; 185 } 186 getSortedHistoryList()187 private Cursor getSortedHistoryList() { 188 final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 189 queryBuilder.setTables(DatabaseHelper.HISTORY_TABLE); 190 191 return queryBuilder.query(mHelper.getReadableDatabase(), PROJECTION_HISTORY, null, 192 null, null, null, DatabaseHelper.COLUMN_LAST_UPDATED_TIME + " DESC"); 193 } 194 addDatabaseData()195 private void addDatabaseData() { 196 final ContentValues values = new ContentValues(); 197 values.put(DatabaseHelper.COLUMN_KEYWORD, mKeyword); 198 values.put(DatabaseHelper.COLUMN_LAST_UPDATED_TIME, System.currentTimeMillis()); 199 200 final long rowId = mHelper.getWritableDatabase().insert( 201 DatabaseHelper.HISTORY_TABLE, null, values); 202 if (rowId == -1) { 203 Log.w(TAG, "Failed to add " + mKeyword + "to database!"); 204 } 205 206 if (mListener != null) { 207 mListener.onAddChangedListener(rowId); 208 } 209 } 210 deleteDatabaseData()211 private void deleteDatabaseData() { 212 // We only care about the field of DatabaseHelper.COLUMN_KEYWORD for deleting 213 StringBuilder selection = new StringBuilder(); 214 selection.append(DatabaseHelper.COLUMN_KEYWORD).append("=?"); 215 final int numberOfRows = mHelper.getWritableDatabase().delete( 216 DatabaseHelper.HISTORY_TABLE, selection.toString(), new String[] { 217 mKeyword }); 218 if (numberOfRows == 0) { 219 Log.w(TAG, "Failed to delete " + mKeyword + "from database!"); 220 } 221 222 if (mListener != null) { 223 mListener.onDeleteChangedListener(numberOfRows); 224 } 225 } 226 updateDatabaseData()227 private void updateDatabaseData() { 228 // We just need to update the field DatabaseHelper.COLUMN_LAST_UPDATED_TIME, 229 // because we will sort by last modified when retrieving from database 230 ContentValues values = new ContentValues(); 231 values.put(DatabaseHelper.COLUMN_LAST_UPDATED_TIME, System.currentTimeMillis()); 232 233 StringBuilder selection = new StringBuilder(); 234 selection.append(DatabaseHelper.COLUMN_KEYWORD).append("=?"); 235 final int numberOfRows = mHelper.getWritableDatabase().update( 236 DatabaseHelper.HISTORY_TABLE, values, selection.toString(), new String[] { 237 mKeyword }); 238 if (numberOfRows == 0) { 239 Log.w(TAG, "Failed to update " + mKeyword + "to database!"); 240 } 241 } 242 parseHistoryFromCursor(Cursor cursor)243 private void parseHistoryFromCursor(Cursor cursor) { 244 if (cursor == null) { 245 if (DEBUG) { 246 Log.e(TAG, "Null cursor happens when building local search history List!"); 247 } 248 return; 249 } 250 synchronized (mLock) { 251 mHistory.clear(); 252 try { 253 while (cursor.moveToNext()) { 254 mHistory.add(cursor.getString(cursor.getColumnIndex( 255 DatabaseHelper.COLUMN_KEYWORD))); 256 } 257 } finally { 258 cursor.close(); 259 } 260 } 261 } 262 263 @Override doInBackground(Object... params)264 protected Void doInBackground(Object... params) { 265 if (!TextUtils.isEmpty(mKeyword)) { 266 switch (mOperation) { 267 case ADD: 268 addDatabaseData(); 269 break; 270 case DELETE: 271 deleteDatabaseData(); 272 break; 273 case UPDATE: 274 updateDatabaseData(); 275 break; 276 default: 277 break; 278 } 279 } 280 281 // params[0] is used to preventing reload twice when deleting over history count 282 if (params.length <= 0 || (params.length > 0 && ((Boolean)params[0]).booleanValue())) { 283 parseHistoryFromCursor(getSortedHistoryList()); 284 } 285 return null; 286 } 287 288 @Override onPostExecute(Object result)289 protected void onPostExecute(Object result) { 290 if (mListener != null) { 291 mListener.onPostExecute(); 292 } 293 } 294 } 295 296 @VisibleForTesting setDatabaseListener(DatabaseChangedListener listener)297 public void setDatabaseListener(DatabaseChangedListener listener) { 298 mListener = listener; 299 } 300 301 interface DatabaseChangedListener { onAddChangedListener(long longResult)302 void onAddChangedListener(long longResult); onDeleteChangedListener(int intResult)303 void onDeleteChangedListener(int intResult); onPostExecute()304 void onPostExecute(); 305 } 306 }