| OLD | NEW |
| (Empty) |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.chrome.browser.bookmarks; | |
| 6 | |
| 7 import android.content.Context; | |
| 8 import android.os.Parcelable; | |
| 9 import android.support.v7.widget.Toolbar; | |
| 10 import android.text.Editable; | |
| 11 import android.text.TextUtils; | |
| 12 import android.text.TextWatcher; | |
| 13 import android.util.AttributeSet; | |
| 14 import android.util.SparseArray; | |
| 15 import android.view.KeyEvent; | |
| 16 import android.view.LayoutInflater; | |
| 17 import android.view.View; | |
| 18 import android.view.ViewGroup; | |
| 19 import android.view.inputmethod.EditorInfo; | |
| 20 import android.widget.AdapterView; | |
| 21 import android.widget.AdapterView.OnItemClickListener; | |
| 22 import android.widget.ArrayAdapter; | |
| 23 import android.widget.BaseAdapter; | |
| 24 import android.widget.EditText; | |
| 25 import android.widget.LinearLayout; | |
| 26 import android.widget.ListView; | |
| 27 import android.widget.TextView; | |
| 28 import android.widget.TextView.OnEditorActionListener; | |
| 29 import android.widget.ViewSwitcher; | |
| 30 | |
| 31 import org.json.JSONArray; | |
| 32 import org.json.JSONException; | |
| 33 | |
| 34 import org.chromium.base.ApiCompatibilityUtils; | |
| 35 import org.chromium.base.ContextUtils; | |
| 36 import org.chromium.chrome.R; | |
| 37 import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkItem; | |
| 38 import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkModelObserve
r; | |
| 39 import org.chromium.chrome.browser.bookmarks.BookmarkSearchRow.SearchHistoryDele
gate; | |
| 40 import org.chromium.components.bookmarks.BookmarkId; | |
| 41 import org.chromium.ui.UiUtils; | |
| 42 | |
| 43 import java.util.ArrayList; | |
| 44 import java.util.List; | |
| 45 | |
| 46 /** | |
| 47 * Activity for searching bookmarks. Search results will be updated when user is
typing. Before | |
| 48 * typing, a list of search history is shown. | |
| 49 */ | |
| 50 public class BookmarkSearchView extends LinearLayout implements OnItemClickListe
ner, | |
| 51 OnEditorActionListener, BookmarkUIObserver, SearchHistoryDelegate { | |
| 52 /** | |
| 53 * A custom {@link ViewSwitcher} that wraps another {@link ViewSwitcher} ins
ide. | |
| 54 */ | |
| 55 public static class HistoryResultSwitcher extends ViewSwitcher { | |
| 56 ViewSwitcher mResultEmptySwitcher; | |
| 57 | |
| 58 /** | |
| 59 * Constructor for xml inflation. | |
| 60 */ | |
| 61 public HistoryResultSwitcher(Context context, AttributeSet attrs) { | |
| 62 super(context, attrs); | |
| 63 } | |
| 64 | |
| 65 @Override | |
| 66 protected void onFinishInflate() { | |
| 67 super.onFinishInflate(); | |
| 68 mResultEmptySwitcher = (ViewSwitcher) findViewById(R.id.result_empty
_switcher); | |
| 69 } | |
| 70 | |
| 71 void showHistory() { | |
| 72 if (getCurrentView().getId() == R.id.bookmark_history_list) return; | |
| 73 showNext(); | |
| 74 } | |
| 75 | |
| 76 void showResult() { | |
| 77 if (getCurrentView().getId() == R.id.bookmark_history_list) showNext
(); | |
| 78 if (mResultEmptySwitcher.getCurrentView().getId() == R.id.bookmark_s
earch_empty_view) { | |
| 79 mResultEmptySwitcher.showNext(); | |
| 80 } | |
| 81 } | |
| 82 | |
| 83 void showEmpty() { | |
| 84 if (getCurrentView().getId() == R.id.bookmark_history_list) showNext
(); | |
| 85 if (mResultEmptySwitcher.getCurrentView().getId() == R.id.bookmark_r
esult_list) { | |
| 86 mResultEmptySwitcher.showNext(); | |
| 87 } | |
| 88 } | |
| 89 } | |
| 90 | |
| 91 private static enum UIState {HISTORY, RESULT, EMPTY} | |
| 92 | |
| 93 private static final String PREF_SEARCH_HISTORY = "bookmark_search_history"; | |
| 94 private static final int SEARCH_HISTORY_MAX_ENTRIES = 10; | |
| 95 private static final int HISTORY_ITEM_PADDING_START_DP = 72; | |
| 96 private static final int MAXIMUM_NUMBER_OF_RESULTS = 500; | |
| 97 | |
| 98 private BookmarkModel mBookmarkModel; | |
| 99 private BookmarkDelegate mDelegate; | |
| 100 private EditText mSearchText; | |
| 101 private ListView mResultList; | |
| 102 private ListView mHistoryList; | |
| 103 private HistoryResultSwitcher mHistoryResultSwitcher; | |
| 104 private UIState mCurrentUIState; | |
| 105 | |
| 106 private BookmarkModelObserver mModelObserver = new BookmarkModelObserver() { | |
| 107 @Override | |
| 108 public void bookmarkModelChanged() { | |
| 109 if (mCurrentUIState == UIState.RESULT || mCurrentUIState == UIState.
EMPTY) { | |
| 110 sendSearchQuery(); | |
| 111 } | |
| 112 } | |
| 113 | |
| 114 @Override | |
| 115 public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, Bookm
arkItem node, | |
| 116 boolean isDoingExtensiveChanges) { | |
| 117 // If isDoingExtensiveChanges is false, it will fall back to bookmar
kModelChange() | |
| 118 if (isDoingExtensiveChanges && mCurrentUIState == UIState.RESULT) { | |
| 119 sendSearchQuery(); | |
| 120 } | |
| 121 } | |
| 122 }; | |
| 123 | |
| 124 /** | |
| 125 * Constructor for inflating from XML. | |
| 126 */ | |
| 127 public BookmarkSearchView(Context context, AttributeSet attrs) { | |
| 128 super(context, attrs); | |
| 129 } | |
| 130 | |
| 131 @Override | |
| 132 protected void onFinishInflate() { | |
| 133 super.onFinishInflate(); | |
| 134 mSearchText = (EditText) findViewById(R.id.bookmark_search_text); | |
| 135 mResultList = (ListView) findViewById(R.id.bookmark_result_list); | |
| 136 mHistoryList = (ListView) findViewById(R.id.bookmark_history_list); | |
| 137 mHistoryResultSwitcher = (HistoryResultSwitcher) findViewById(R.id.histo
ry_result_switcher); | |
| 138 | |
| 139 Toolbar searchBar = (Toolbar) findViewById(R.id.search_bar); | |
| 140 searchBar.setNavigationIcon(R.drawable.back_normal); | |
| 141 searchBar.setNavigationContentDescription(R.string.accessibility_toolbar
_btn_back); | |
| 142 searchBar.setNavigationOnClickListener(new OnClickListener() { | |
| 143 @Override | |
| 144 public void onClick(View v) { | |
| 145 onBackPressed(); | |
| 146 } | |
| 147 }); | |
| 148 | |
| 149 mHistoryList.setOnItemClickListener(this); | |
| 150 mSearchText.setOnEditorActionListener(this); | |
| 151 mSearchText.addTextChangedListener(new TextWatcher() { | |
| 152 @Override | |
| 153 public void beforeTextChanged(CharSequence s, int start, int count,
int after) { | |
| 154 } | |
| 155 | |
| 156 @Override | |
| 157 public void onTextChanged(CharSequence s, int start, int before, int
count) { | |
| 158 } | |
| 159 | |
| 160 @Override | |
| 161 public void afterTextChanged(Editable s) { | |
| 162 if (TextUtils.isEmpty(s.toString().trim())) { | |
| 163 resetUI(); | |
| 164 } else { | |
| 165 sendSearchQuery(); | |
| 166 } | |
| 167 } | |
| 168 }); | |
| 169 mCurrentUIState = UIState.HISTORY; | |
| 170 } | |
| 171 | |
| 172 private void updateHistoryList() { | |
| 173 mHistoryList.setAdapter(new ArrayAdapter<String>(getContext(), | |
| 174 android.R.layout.simple_list_item_1, android.R.id.text1, | |
| 175 readHistoryList()) { | |
| 176 @Override | |
| 177 public View getView(int position, View convertView, ViewGroup parent
) { | |
| 178 View textView = super.getView(position, convertView, parent); | |
| 179 // Set padding start to specific size. | |
| 180 int paddingStart = (int) (HISTORY_ITEM_PADDING_START_DP | |
| 181 * getResources().getDisplayMetrics().density); | |
| 182 ApiCompatibilityUtils.setPaddingRelative(textView, paddingStart, | |
| 183 textView.getPaddingTop(), textView.getPaddingRight(), | |
| 184 textView.getPaddingBottom()); | |
| 185 return textView; | |
| 186 } | |
| 187 }); | |
| 188 } | |
| 189 | |
| 190 private void resetUI() { | |
| 191 setUIState(UIState.HISTORY); | |
| 192 mResultList.setAdapter(null); | |
| 193 if (!TextUtils.isEmpty(mSearchText.getText())) mSearchText.setText(""); | |
| 194 } | |
| 195 | |
| 196 private void sendSearchQuery() { | |
| 197 String currentText = mSearchText.getText().toString().trim(); | |
| 198 if (TextUtils.isEmpty(currentText)) return; | |
| 199 | |
| 200 List<BookmarkMatch> results = mBookmarkModel.searchBookmarks(currentText
, | |
| 201 MAXIMUM_NUMBER_OF_RESULTS); | |
| 202 populateResultListView(results); | |
| 203 } | |
| 204 | |
| 205 @Override | |
| 206 public boolean dispatchKeyEvent(KeyEvent event) { | |
| 207 // To intercept hardware key, a view must have focus. | |
| 208 if (mDelegate == null) return super.dispatchKeyEvent(event); | |
| 209 | |
| 210 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { | |
| 211 KeyEvent.DispatcherState state = getKeyDispatcherState(); | |
| 212 if (state != null) { | |
| 213 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeat
Count() == 0) { | |
| 214 state.startTracking(event, this); | |
| 215 return true; | |
| 216 } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isC
anceled() | |
| 217 && state.isTracking(event)) { | |
| 218 onBackPressed(); | |
| 219 return true; | |
| 220 } | |
| 221 } | |
| 222 } | |
| 223 | |
| 224 return super.dispatchKeyEvent(event); | |
| 225 } | |
| 226 | |
| 227 @Override | |
| 228 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
{ | |
| 229 // No-op because state saving/restoring is intentionally omitted in this
View. This is | |
| 230 // to fix a crash in Android M that TextView's old text is sometimes res
tored even if | |
| 231 // setText("") is called in onVisibilityChange(). crbug.com/596783 | |
| 232 } | |
| 233 | |
| 234 /** | |
| 235 * Make result list visible and popuplate the list with given list of bookma
rks. | |
| 236 */ | |
| 237 private void populateResultListView(List<BookmarkMatch> ids) { | |
| 238 if (ids.isEmpty()) { | |
| 239 setUIState(UIState.EMPTY); | |
| 240 } else { | |
| 241 setUIState(UIState.RESULT); | |
| 242 mResultList.setAdapter(new ResultListAdapter(ids, mDelegate)); | |
| 243 } | |
| 244 } | |
| 245 | |
| 246 private void setUIState(UIState state) { | |
| 247 if (mCurrentUIState == state) return; | |
| 248 mCurrentUIState = state; | |
| 249 if (state == UIState.HISTORY) { | |
| 250 mHistoryResultSwitcher.showHistory(); | |
| 251 updateHistoryList(); | |
| 252 } else if (state == UIState.RESULT) { | |
| 253 mHistoryResultSwitcher.showResult(); | |
| 254 } else if (state == UIState.EMPTY) { | |
| 255 mHistoryResultSwitcher.showEmpty(); | |
| 256 } | |
| 257 } | |
| 258 | |
| 259 @Override | |
| 260 protected void onVisibilityChanged(View changedView, int visibility) { | |
| 261 super.onVisibilityChanged(changedView, visibility); | |
| 262 // This method might be called very early. Null check on bookmark model
here. | |
| 263 if (mBookmarkModel == null) return; | |
| 264 | |
| 265 if (visibility == View.VISIBLE) { | |
| 266 mBookmarkModel.addObserver(mModelObserver); | |
| 267 updateHistoryList(); | |
| 268 mSearchText.requestFocus(); | |
| 269 UiUtils.showKeyboard(mSearchText); | |
| 270 } else { | |
| 271 UiUtils.hideKeyboard(mSearchText); | |
| 272 mBookmarkModel.removeObserver(mModelObserver); | |
| 273 resetUI(); | |
| 274 clearFocus(); | |
| 275 } | |
| 276 } | |
| 277 | |
| 278 private void onBackPressed() { | |
| 279 if (mCurrentUIState == UIState.HISTORY) { | |
| 280 mDelegate.closeSearchUI(); | |
| 281 } else { | |
| 282 resetUI(); | |
| 283 } | |
| 284 } | |
| 285 | |
| 286 @Override | |
| 287 public void onItemClick(AdapterView<?> parent, View view, int position, long
id) { | |
| 288 assert parent == mHistoryList : "Only history list should have onItemCli
ckListener."; | |
| 289 mSearchText.setText((String) parent.getAdapter().getItem(position)); | |
| 290 } | |
| 291 | |
| 292 @Override | |
| 293 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { | |
| 294 if (actionId == EditorInfo.IME_ACTION_SEARCH) { | |
| 295 UiUtils.hideKeyboard(v); | |
| 296 | |
| 297 // History is saved either when the user clicks search button or a s
earch result is | |
| 298 // clicked. | |
| 299 saveSearchHistory(); | |
| 300 } | |
| 301 return false; | |
| 302 } | |
| 303 | |
| 304 private void saveHistoryList(List<String> history) { | |
| 305 JSONArray jsonArray = new JSONArray(history); | |
| 306 ContextUtils.getAppSharedPreferences().edit() | |
| 307 .putString(PREF_SEARCH_HISTORY, jsonArray.toString()).apply(); | |
| 308 } | |
| 309 | |
| 310 private List<String> readHistoryList() { | |
| 311 try { | |
| 312 String unformatted = ContextUtils.getAppSharedPreferences() | |
| 313 .getString(PREF_SEARCH_HISTORY, "[]"); | |
| 314 JSONArray jsonArray = new JSONArray(unformatted); | |
| 315 ArrayList<String> result = new ArrayList<String>(); | |
| 316 for (int i = 0; i < jsonArray.length(); i++) { | |
| 317 result.add(jsonArray.getString(i)); | |
| 318 } | |
| 319 return result; | |
| 320 } catch (JSONException e) { | |
| 321 return new ArrayList<String>(); | |
| 322 } | |
| 323 } | |
| 324 | |
| 325 /** | |
| 326 * Adds the current search text as top entry of the list. | |
| 327 */ | |
| 328 private List<String> addCurrentTextToHistoryList(List<String> history) { | |
| 329 String text = mSearchText.getText().toString().trim(); | |
| 330 if (TextUtils.isEmpty(text)) return history; | |
| 331 | |
| 332 history.remove(text); | |
| 333 history.add(0, text); | |
| 334 if (history.size() > SEARCH_HISTORY_MAX_ENTRIES) { | |
| 335 history.remove(history.size() - 1); | |
| 336 } | |
| 337 return history; | |
| 338 } | |
| 339 | |
| 340 // SearchHistoryDelegate implementation | |
| 341 | |
| 342 @Override | |
| 343 public void saveSearchHistory() { | |
| 344 saveHistoryList((addCurrentTextToHistoryList(readHistoryList()))); | |
| 345 } | |
| 346 | |
| 347 // BookmarkUIObserver implementation | |
| 348 | |
| 349 @Override | |
| 350 public void onBookmarkDelegateInitialized(BookmarkDelegate delegate) { | |
| 351 mDelegate = delegate; | |
| 352 mDelegate.addUIObserver(this); | |
| 353 mBookmarkModel = mDelegate.getModel(); | |
| 354 } | |
| 355 | |
| 356 @Override | |
| 357 public void onDestroy() { | |
| 358 mBookmarkModel.removeObserver(mModelObserver); | |
| 359 mDelegate.removeUIObserver(this); | |
| 360 } | |
| 361 | |
| 362 @Override | |
| 363 public void onFolderStateSet(BookmarkId folder) { | |
| 364 } | |
| 365 | |
| 366 @Override | |
| 367 public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) { | |
| 368 } | |
| 369 | |
| 370 private class ResultListAdapter extends BaseAdapter { | |
| 371 private BookmarkDelegate mDelegate; | |
| 372 private List<BookmarkMatch> mBookmarktList; | |
| 373 | |
| 374 public ResultListAdapter(List<BookmarkMatch> bookmarkMatches, | |
| 375 BookmarkDelegate delegate) { | |
| 376 mDelegate = delegate; | |
| 377 mBookmarktList = bookmarkMatches; | |
| 378 } | |
| 379 | |
| 380 @Override | |
| 381 public int getCount() { | |
| 382 return mBookmarktList.size(); | |
| 383 } | |
| 384 | |
| 385 @Override | |
| 386 public BookmarkMatch getItem(int position) { | |
| 387 return mBookmarktList.get(position); | |
| 388 } | |
| 389 | |
| 390 @Override | |
| 391 public long getItemId(int position) { | |
| 392 return position; | |
| 393 } | |
| 394 | |
| 395 @Override | |
| 396 public View getView(int position, View convertView, ViewGroup parent) { | |
| 397 final BookmarkMatch bookmarkMatch = getItem(position); | |
| 398 if (convertView == null) { | |
| 399 convertView = LayoutInflater.from(parent.getContext()).inflate( | |
| 400 R.layout.bookmark_search_row, parent, false); | |
| 401 } | |
| 402 final BookmarkSearchRow row = (BookmarkSearchRow) convertView; | |
| 403 row.onBookmarkDelegateInitialized(mDelegate); | |
| 404 row.setBookmarkId(bookmarkMatch.getBookmarkId()); | |
| 405 row.setSearchHistoryDelegate(BookmarkSearchView.this); | |
| 406 return convertView; | |
| 407 } | |
| 408 } | |
| 409 } | |
| OLD | NEW |