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 |