blob: 44453590b9a442abc1dce341fe49621392fa051e [file] [log] [blame]
/*
* Copyright (C) 2009 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.globalsearch;
import android.app.SearchManager;
import android.app.SearchManager.DialogCursorProtocol;
import android.database.AbstractCursor;
import android.database.CursorIndexOutOfBoundsException;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* This is the Cursor that we return from SuggestionProvider. It relies on its
* {@link SuggestionBacker} for results and notifications of changes to the results.
*
* The backer is attached via {@link #attachBacker(SuggestionBacker)} once it is ready.
*
* Important: a local consistent copy of the suggestions is stored in the cursor. The only safe
* place to update this copy is in {@link #requery}.
*/
public class SuggestionCursor extends AbstractCursor implements SuggestionBacker.Listener {
// set to true to enable the more verbose debug logging for this file
private static final boolean DBG = false;
// set to true along with DBG to be even more verbose
private static final boolean SPEW = false;
// set to true to dump a full list of the suggestions each time the cursor is requeried
private static final boolean DUMP_SUGGESTIONS = false;
private static final String TAG = SuggestionCursor.class.getSimpleName();
// The extra used to tell a cursor to close itself. This is a hack to work around
// the fact that cross-process cursors currently don't get closed by Cursor.close(),
// http://b/issue?id=2015069
private static final String EXTRA_CURSOR_RESPOND_CLOSE_CURSOR = "cursor_respond_close_cursor";
// the same as the string in suggestActionMsgColumn in res/xml/searchable.xml
private static final String SUGGEST_COLUMN_ACTION_MSG_CALL = "suggest_action_msg_call";
private static final String[] COLUMNS = {
"_id",
SearchManager.SUGGEST_COLUMN_FORMAT,
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_ICON_1,
SearchManager.SUGGEST_COLUMN_ICON_2,
SearchManager.SUGGEST_COLUMN_QUERY,
SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_INTENT_DATA,
SUGGEST_COLUMN_ACTION_MSG_CALL,
SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
SearchManager.SUGGEST_COLUMN_BACKGROUND_COLOR
};
// Indices into COLUMNS
private static final int _ID = 0;
private static final int FORMAT = 1;
private static final int TEXT_1 = 2;
private static final int TEXT_2 = 3;
private static final int ICON_1 = 4;
private static final int ICON_2 = 5;
private static final int QUERY = 6;
private static final int INTENT_ACTION = 7;
private static final int INTENT_DATA = 8;
private static final int ACTION_MSG_CALL = 9;
private static final int INTENT_EXTRA_DATA = 10;
private static final int INTENT_COMPONENT_NAME = 11;
private static final int SHORTCUT_ID = 12;
private static final int BACKGROUND_COLOR = 13;
private boolean mOnMoreCalled = false;
private final String mQuery;
private final DelayedExecutor mDelayedExecutor;
private boolean mIncludeSources;
private CursorListener mListener;
private SuggestionBacker mBacker;
private long mNextNotify = 0;
// we keep a consistent snapshot locally
private ArrayList<SuggestionData> mData = new ArrayList<SuggestionData>(10);
/**
* We won't call {@link AbstractCursor#onChange} more than once per window.
*/
private static final int CURSOR_NOTIFY_WINDOW_MS = 100;
/**
* Interface for receiving notification from the cursor.
*/
public interface CursorListener {
/**
* Called when the cursor has been closed.
*/
void onClose();
/**
* Called when an item is clicked.
*
* @param click The suggestion that was clicked.
* @param displayedSuggestions The suggestions that have been displayed to the user.
*/
void onItemClicked(SuggestionData clicked, List<SuggestionData> displayedSuggestions);
/**
* Called the first time "more" becomes visible
*/
void onMoreVisible();
}
/**
* @param delayedExecutor used to post messages.
* @param query The query that was sent.
*/
public SuggestionCursor(DelayedExecutor delayedExecutor, String query) {
mQuery = query;
mDelayedExecutor = delayedExecutor;
mIncludeSources = false;
}
/**
* Set the suggestion backer, triggering the initial snapshot.
*
* @param backer The backer.
*/
public void attachBacker(SuggestionBacker backer) {
mBacker = backer;
mBacker.snapshotSuggestions(mData, mIncludeSources);
onNewResults();
}
/**
* Prefills the results from this cursor with the results from another. This is used when no
* other results are initially available to provide a smoother experience.
*
* @param other The other cursor to get the results from.
*/
public void prefill(SuggestionCursor other) {
if (!mData.isEmpty()) {
throw new IllegalStateException("prefilled when we aleady have results");
}
mData.clear();
mData.addAll(other.mData);
}
@Override
public String[] getColumnNames() {
return COLUMNS;
}
@Override
public int getCount() {
return mData.size();
}
/**
* Handles out-of-band messages from the search dialog.
*/
@Override
public Bundle respond(Bundle extras) {
if (DBG) Log.d(TAG, "respond(" + extras + ")");
// Hack to work around http://b/issue?id=2015069,
// "CursorToBulkCursorAdaptor.close() does not call mCursor.close()"
if (extras.getBoolean(EXTRA_CURSOR_RESPOND_CLOSE_CURSOR)) {
close();
return Bundle.EMPTY;
}
final int method = extras.getInt(SearchManager.DialogCursorProtocol.METHOD, -1);
if (method == -1) {
Log.w(TAG, "received unexpectd respond: no DialogCursorProtocol.METHOD specified.");
return Bundle.EMPTY;
}
switch (method) {
case DialogCursorProtocol.POST_REFRESH:
return respondPostRefresh(extras);
case DialogCursorProtocol.CLICK:
return respondClick(extras);
case DialogCursorProtocol.THRESH_HIT:
return respondThreshHit(extras);
default:
Log.e(TAG, "unexpected DialogCursorProtocol.METHOD " + method);
return Bundle.EMPTY;
}
}
/**
* Handle receiving and sending back information associated with
* {@link DialogCursorProtocol#POST_REFRESH}.
*
* @param request The bundle sent.
* @return The response bundle.
*/
private Bundle respondPostRefresh(Bundle request) {
Bundle response = new Bundle(2);
response.putBoolean(
DialogCursorProtocol.POST_REFRESH_RECEIVE_ISPENDING, isResultsPending());
if (isShowingMore() && !mOnMoreCalled) {
// tell the dialog we want to be notified when "more results" is first displayed
response.putInt(
DialogCursorProtocol.POST_REFRESH_RECEIVE_DISPLAY_NOTIFY,
getMoreResultsPosition());
}
return response;
}
private boolean isResultsPending() {
// asssume results are pending until we get the backer
return mBacker == null ? true : mBacker.isResultsPending();
}
private boolean isShowingMore() {
return mBacker != null && mBacker.isShowingMore();
}
private int getMoreResultsPosition() {
return mBacker == null ?
mData.size() :
mBacker.getMoreResultPosition();
}
/**
* Handle receiving and sending back information associated with
* {@link DialogCursorProtocol#CLICK}.
*
* @param request The bundle sent.
* @return The response bundle.
*/
private Bundle respondClick(Bundle request) {
final int pos = request.getInt(DialogCursorProtocol.CLICK_SEND_POSITION, -1);
int maxDisplayed = request.getInt(DialogCursorProtocol.CLICK_SEND_MAX_DISPLAY_POS, -1);
if (DBG) Log.d(TAG, "respondClick(), pos=" + pos + ", maxDisplayed=" + maxDisplayed);
if (pos == -1) {
Log.w(TAG, "DialogCursorProtocol.CLICK didn't come with extra CLICK_SEND_POSITION");
return Bundle.EMPTY;
}
// avoid exceptions from List.subList()
final int numSuggestions = mData.size();
if (maxDisplayed < -1) maxDisplayed = -1;
if (maxDisplayed >= numSuggestions) maxDisplayed = numSuggestions - 1;
List<SuggestionData> displayedSuggestions = mData.subList(0, maxDisplayed + 1);
if (mListener != null) mListener.onItemClicked(mData.get(pos), displayedSuggestions);
// if they click on the "more results item"
if (pos == getMoreResultsPosition()) {
// toggle the expansion of the additional sources
mIncludeSources = !mIncludeSources;
onNewResults();
if (mIncludeSources) {
// if we have switched to expanding,
// tell the search dialog to select the position of the "more" entry so that
// the additional corpus entries will become visible without having to
// manually scroll
final Bundle response = new Bundle();
response.putInt(DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, pos);
return response;
}
}
return Bundle.EMPTY;
}
/**
* Handle receiving and sending back information associated with
* {@link DialogCursorProtocol#THRESH_HIT}.
*
* We use this to get notified when "more" is first scrolled onto screen.
*
* @param request The bundle sent.
* @return The response bundle.
*/
private Bundle respondThreshHit(Bundle request) {
mOnMoreCalled = true;
if (mListener != null) mListener.onMoreVisible();
return Bundle.EMPTY;
}
@Override
public void close() {
if (DBG) Log.d(TAG, "close()");
super.close();
if (mListener != null) {
mListener.onClose();
}
}
@Override
protected void finalize() {
if (!mClosed) {
Log.w(TAG, "SuggestionCursor finalized without being closed. Someone is leaking.");
close();
}
super.finalize();
}
/**
* We don't copy over a fresh copy of the data, instead, we notify the cursor that the
* data has changed, and wait to for {@link #requery} to be called. This way, any
* adapter backed by this cursor will have a consistent view of the data, and {@link #requery}
* us when ready.
*
* Calls {@link AbstractCursor#onChange} only if there isn't already one planned to be called
* within {@link #CURSOR_NOTIFY_WINDOW_MS}.
*
* {@inheritDoc}
*/
public synchronized void onNewResults() {
if (DBG) Log.d(TAG, "onNewResults()");
if (!isClosed()) {
long now = SystemClock.uptimeMillis();
if (now < mNextNotify) {
if (DBG) Log.d(TAG, "-avoided a notify!");
return;
}
mNextNotify = now + CURSOR_NOTIFY_WINDOW_MS;
if (DBG) Log.d(TAG, "-posting onChange(false)");
mDelayedExecutor.postAtTime(mNotifier, mNextNotify);
}
}
private final Runnable mNotifier = new Runnable() {
public void run() {
SuggestionCursor.this.onChange(false);
}
};
/**
* Gets the current suggestion.
*/
private SuggestionData get() {
if (mPos < 0) {
throw new CursorIndexOutOfBoundsException("Before first row.");
}
if (mPos >= mData.size()) {
throw new CursorIndexOutOfBoundsException("After last row.");
}
SuggestionData suggestion = mData.get(mPos);
if (DBG && SPEW) Log.d(TAG, "get(" + mPos + ")");
if (DBG && SPEW) Log.d(TAG, suggestion.toString());
return suggestion;
}
@Override
public boolean requery() {
if (mBacker != null) {
mBacker.snapshotSuggestions(mData, mIncludeSources);
if (DBG) Log.d(TAG, "requery(), now " + mData.size() + " items");
}
if (DUMP_SUGGESTIONS) {
Log.d(TAG, "");
Log.d(TAG, "");
final int num = mData.size();
for (int i = 0; i < num; i++) {
final SuggestionData s = mData.get(i);
Log.d(TAG, "/" + i + "---------\\");
Log.d(TAG, "- " + s.getSource().getShortClassName());
Log.d(TAG, "- " + s.getTitle());
Log.d(TAG, "- " + s.getDescription());
Log.d(TAG, "- " + s.getIntentAction());
Log.d(TAG, "- " + s.getIntentData());
Log.d(TAG, "\\---------/");
}
}
return super.requery();
}
@Override
public double getDouble(int column) {
return Double.valueOf(getString(column));
}
@Override
public float getFloat(int column) {
return Float.valueOf(getString(column));
}
@Override
public int getInt(int column) {
return Integer.valueOf(getString(column));
}
@Override
public long getLong(int column) {
return Long.valueOf(getString(column));
}
@Override
public short getShort(int column) {
return Short.valueOf(getString(column));
}
@Override
public String getString(int columnIndex) {
if (DBG && SPEW) Log.d(TAG, "getString(columnIndex=" + columnIndex + ")");
return (String) getColumnValue(get(), columnIndex);
}
private Object getColumnValue(SuggestionData suggestion, int columnIndex) {
switch(columnIndex) {
case _ID: return Integer.toString(mPos);
case FORMAT: return suggestion.getFormat();
case TEXT_1: return suggestion.getTitle();
case TEXT_2: return suggestion.getDescription();
case ICON_1: return suggestion.getIcon1();
case ICON_2: return suggestion.getIcon2();
case QUERY: return suggestion.getIntentQuery();
case INTENT_ACTION: return suggestion.getIntentAction();
case INTENT_DATA: return suggestion.getIntentData();
case ACTION_MSG_CALL: return suggestion.getActionMsgCall();
case INTENT_EXTRA_DATA: return suggestion.getIntentExtraData();
case INTENT_COMPONENT_NAME: return suggestion.getIntentComponentName();
case SHORTCUT_ID: return suggestion.getShortcutId();
case BACKGROUND_COLOR: return Integer.toString(suggestion.getBackgroundColor());
default:
throw new RuntimeException("we musta forgot about one of the columns :-/");
}
}
@Override
public boolean isNull(int column) {
return getString(column) == null;
}
/**
* Sets the listener which will be notified if the "more results" entry is shown, and when
* the cursor has been closed.
*
* @param listener The listener. May be <code>null</code> to remove
* the current listener.
*/
public void setListener(CursorListener listener) {
mListener = listener;
}
}