// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.bookmarks;
import android.content.Context;
import android.os.Parcelable;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.ViewSwitcher;
import org.json.JSONArray;
import org.json.JSONException;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkItem;
import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkModelObserver;
import org.chromium.chrome.browser.bookmarks.BookmarkSearchRow.SearchHistoryDelegate;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.ui.UiUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Activity for searching bookmarks. Search results will be updated when user is typing. Before
* typing, a list of search history is shown.
*/
public class BookmarkSearchView extends LinearLayout implements OnItemClickListener,
OnEditorActionListener, BookmarkUIObserver, SearchHistoryDelegate {
/**
* A custom {@link ViewSwitcher} that wraps another {@link ViewSwitcher} inside.
*/
public static class HistoryResultSwitcher extends ViewSwitcher {
ViewSwitcher mResultEmptySwitcher;
/**
* Constructor for xml inflation.
*/
public HistoryResultSwitcher(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mResultEmptySwitcher = (ViewSwitcher) findViewById(R.id.result_empty_switcher);
}
void showHistory() {
if (getCurrentView().getId() == R.id.bookmark_history_list) return;
showNext();
}
void showResult() {
if (getCurrentView().getId() == R.id.bookmark_history_list) showNext();
if (mResultEmptySwitcher.getCurrentView().getId() == R.id.bookmark_search_empty_view) {
mResultEmptySwitcher.showNext();
}
}
void showEmpty() {
if (getCurrentView().getId() == R.id.bookmark_history_list) showNext();
if (mResultEmptySwitcher.getCurrentView().getId() == R.id.bookmark_result_list) {
mResultEmptySwitcher.showNext();
}
}
}
private static enum UIState {HISTORY, RESULT, EMPTY}
private static final String PREF_SEARCH_HISTORY = "bookmark_search_history";
private static final int SEARCH_HISTORY_MAX_ENTRIES = 10;
private static final int HISTORY_ITEM_PADDING_START_DP = 72;
private static final int MAXIMUM_NUMBER_OF_RESULTS = 500;
private BookmarkModel mBookmarkModel;
private BookmarkDelegate mDelegate;
private EditText mSearchText;
private ListView mResultList;
private ListView mHistoryList;
private HistoryResultSwitcher mHistoryResultSwitcher;
private UIState mCurrentUIState;
private BookmarkModelObserver mModelObserver = new BookmarkModelObserver() {
@Override
public void bookmarkModelChanged() {
if (mCurrentUIState == UIState.RESULT || mCurrentUIState == UIState.EMPTY) {
sendSearchQuery();
}
}
@Override
public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node,
boolean isDoingExtensiveChanges) {
// If isDoingExtensiveChanges is false, it will fall back to bookmarkModelChange()
if (isDoingExtensiveChanges && mCurrentUIState == UIState.RESULT) {
sendSearchQuery();
}
}
};
/**
* Constructor for inflating from XML.
*/
public BookmarkSearchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSearchText = (EditText) findViewById(R.id.bookmark_search_text);
mResultList = (ListView) findViewById(R.id.bookmark_result_list);
mHistoryList = (ListView) findViewById(R.id.bookmark_history_list);
mHistoryResultSwitcher = (HistoryResultSwitcher) findViewById(R.id.history_result_switcher);
Toolbar searchBar = (Toolbar) findViewById(R.id.search_bar);
searchBar.setNavigationIcon(R.drawable.back_normal);
searchBar.setNavigationContentDescription(R.string.accessibility_toolbar_btn_back);
searchBar.setNavigationOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
mHistoryList.setOnItemClickListener(this);
mSearchText.setOnEditorActionListener(this);
mSearchText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s.toString().trim())) {
resetUI();
} else {
sendSearchQuery();
}
}
});
mCurrentUIState = UIState.HISTORY;
}
private void updateHistoryList() {
mHistoryList.setAdapter(new ArrayAdapter<String>(getContext(),
android.R.layout.simple_list_item_1, android.R.id.text1,
readHistoryList()) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View textView = super.getView(position, convertView, parent);
// Set padding start to specific size.
int paddingStart = (int) (HISTORY_ITEM_PADDING_START_DP
* getResources().getDisplayMetrics().density);
ApiCompatibilityUtils.setPaddingRelative(textView, paddingStart,
textView.getPaddingTop(), textView.getPaddingRight(),
textView.getPaddingBottom());
return textView;
}
});
}
private void resetUI() {
setUIState(UIState.HISTORY);
mResultList.setAdapter(null);
if (!TextUtils.isEmpty(mSearchText.getText())) mSearchText.setText("");
}
private void sendSearchQuery() {
String currentText = mSearchText.getText().toString().trim();
if (TextUtils.isEmpty(currentText)) return;
List<BookmarkMatch> results = mBookmarkModel.searchBookmarks(currentText,
MAXIMUM_NUMBER_OF_RESULTS);
populateResultListView(results);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// To intercept hardware key, a view must have focus.
if (mDelegate == null) return super.dispatchKeyEvent(event);
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
state.startTracking(event, this);
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled()
&& state.isTracking(event)) {
onBackPressed();
return true;
}
}
}
return super.dispatchKeyEvent(event);
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// No-op because state saving/restoring is intentionally omitted in this View. This is
// to fix a crash in Android M that TextView's old text is sometimes restored even if
// setText("") is called in onVisibilityChange(). crbug.com/596783
}
/**
* Make result list visible and popuplate the list with given list of bookmarks.
*/
private void populateResultListView(List<BookmarkMatch> ids) {
if (ids.isEmpty()) {
setUIState(UIState.EMPTY);
} else {
setUIState(UIState.RESULT);
mResultList.setAdapter(new ResultListAdapter(ids, mDelegate));
}
}
private void setUIState(UIState state) {
if (mCurrentUIState == state) return;
mCurrentUIState = state;
if (state == UIState.HISTORY) {
mHistoryResultSwitcher.showHistory();
updateHistoryList();
} else if (state == UIState.RESULT) {
mHistoryResultSwitcher.showResult();
} else if (state == UIState.EMPTY) {
mHistoryResultSwitcher.showEmpty();
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
// This method might be called very early. Null check on bookmark model here.
if (mBookmarkModel == null) return;
if (visibility == View.VISIBLE) {
mBookmarkModel.addObserver(mModelObserver);
updateHistoryList();
mSearchText.requestFocus();
UiUtils.showKeyboard(mSearchText);
} else {
UiUtils.hideKeyboard(mSearchText);
mBookmarkModel.removeObserver(mModelObserver);
resetUI();
clearFocus();
}
}
private void onBackPressed() {
if (mCurrentUIState == UIState.HISTORY) {
mDelegate.closeSearchUI();
} else {
resetUI();
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
assert parent == mHistoryList : "Only history list should have onItemClickListener.";
mSearchText.setText((String) parent.getAdapter().getItem(position));
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
UiUtils.hideKeyboard(v);
// History is saved either when the user clicks search button or a search result is
// clicked.
saveSearchHistory();
}
return false;
}
private void saveHistoryList(List<String> history) {
JSONArray jsonArray = new JSONArray(history);
ContextUtils.getAppSharedPreferences().edit()
.putString(PREF_SEARCH_HISTORY, jsonArray.toString()).apply();
}
private List<String> readHistoryList() {
try {
String unformatted = ContextUtils.getAppSharedPreferences()
.getString(PREF_SEARCH_HISTORY, "[]");
JSONArray jsonArray = new JSONArray(unformatted);
ArrayList<String> result = new ArrayList<String>();
for (int i = 0; i < jsonArray.length(); i++) {
result.add(jsonArray.getString(i));
}
return result;
} catch (JSONException e) {
return new ArrayList<String>();
}
}
/**
* Adds the current search text as top entry of the list.
*/
private List<String> addCurrentTextToHistoryList(List<String> history) {
String text = mSearchText.getText().toString().trim();
if (TextUtils.isEmpty(text)) return history;
history.remove(text);
history.add(0, text);
if (history.size() > SEARCH_HISTORY_MAX_ENTRIES) {
history.remove(history.size() - 1);
}
return history;
}
// SearchHistoryDelegate implementation
@Override
public void saveSearchHistory() {
saveHistoryList((addCurrentTextToHistoryList(readHistoryList())));
}
// BookmarkUIObserver implementation
@Override
public void onBookmarkDelegateInitialized(BookmarkDelegate delegate) {
mDelegate = delegate;
mDelegate.addUIObserver(this);
mBookmarkModel = mDelegate.getModel();
}
@Override
public void onDestroy() {
mBookmarkModel.removeObserver(mModelObserver);
mDelegate.removeUIObserver(this);
}
@Override
public void onFolderStateSet(BookmarkId folder) {
}
@Override
public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) {
}
private class ResultListAdapter extends BaseAdapter {
private BookmarkDelegate mDelegate;
private List<BookmarkMatch> mBookmarktList;
public ResultListAdapter(List<BookmarkMatch> bookmarkMatches,
BookmarkDelegate delegate) {
mDelegate = delegate;
mBookmarktList = bookmarkMatches;
}
@Override
public int getCount() {
return mBookmarktList.size();
}
@Override
public BookmarkMatch getItem(int position) {
return mBookmarktList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final BookmarkMatch bookmarkMatch = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(
R.layout.bookmark_search_row, parent, false);
}
final BookmarkSearchRow row = (BookmarkSearchRow) convertView;
row.onBookmarkDelegateInitialized(mDelegate);
row.setBookmarkId(bookmarkMatch.getBookmarkId());
row.setSearchHistoryDelegate(BookmarkSearchView.this);
return convertView;
}
}
}