Commit 9138ce8a authored by Mason Tang's avatar Mason Tang
Browse files

Added basic support for searching events

 - Reusing agenda view for displaying search results
 - Currently not fragment-ized

Change-Id: I687b61ca86f92a54c1e402b881edd83111806161
parent ab29d9ea
......@@ -75,6 +75,10 @@
</intent-filter>
</activity>
<!-- Make all activities a searchable context -->
<meta-data android:name="android.app.default_searchable"
android:value=".SearchActivity"/>
<activity android:name="MonthActivity" android:label="@string/month_view"
android:theme="@style/CalendarTheme" />
<activity android:name="WeekActivity" android:label="@string/week_view"
......@@ -124,6 +128,15 @@
<activity android:name="SelectCalendarsActivity" android:label="@string/calendars_title" />
<activity android:name="CalendarPreferenceActivity" android:label="@string/preferences_title" />
<activity android:name="SearchActivity" android:label="@string/search_title"
android:launchMode="singleTop" android:theme="@android:style/Theme.Light">
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
</activity>
<activity android:name="AlertActivity" android:launchMode="singleInstance"
android:theme="@android:style/Theme.Light" android:excludeFromRecents="true" />
<receiver android:name="AlertReceiver">
......
......@@ -120,6 +120,9 @@
<!-- This is a label on a menu item. Pressing this menu item allows the
user to view and edit his Settings (or Preferences) -->
<string name="menu_preferences">"Settings"</string>
<!-- This is a label on a menu item. Pressing this menu item allows the
user to search their events. -->
<string name="search">"Search"</string>
<!-- Month view -->
<skip />
......@@ -247,6 +250,11 @@
<!-- This is shown at the bottom of the agenda view showing the range of events shown. -->
<string name="show_newer_events">Showing events until <xliff:g id="newest_search_range">%1$s</xliff:g>. Tap to look for more.</string>
<!-- Search activity strings -->
<skip />
<!-- Title of the search screen -->
<string name="search_title">Search my calendars</string>
<!-- ICS Import activity -->
<skip />
<!-- This is a abbreviation for 'Number of events' and is a label next to
......
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_label" >
</searchable>
\ No newline at end of file
......@@ -20,6 +20,7 @@ import com.android.calendar.AgendaAdapter.ViewHolder;
import com.android.calendar.AgendaWindowAdapter.EventInfo;
import com.android.calendar.CalendarController.EventType;
import android.content.Context;
import android.graphics.Rect;
import android.text.format.Time;
import android.util.Log;
......@@ -35,19 +36,18 @@ public class AgendaListView extends ListView implements OnItemClickListener {
private static final boolean DEBUG = false;
private AgendaWindowAdapter mWindowAdapter;
private DeleteEventHelper mDeleteEventHelper;
public AgendaListView(AgendaActivity agendaActivity) {
super(agendaActivity, null);
public AgendaListView(Context context) {
super(context, null);
setOnItemClickListener(this);
setChoiceMode(ListView.CHOICE_MODE_SINGLE);
setVerticalScrollBarEnabled(false);
mWindowAdapter = new AgendaWindowAdapter(agendaActivity, this);
mWindowAdapter = new AgendaWindowAdapter(context, this);
setAdapter(mWindowAdapter);
mDeleteEventHelper =
new DeleteEventHelper(agendaActivity, null, false /* don't exit when done */);
new DeleteEventHelper(context, null, false /* don't exit when done */);
}
@Override protected void onDetachedFromWindow() {
......@@ -71,6 +71,16 @@ public class AgendaListView extends ListView implements OnItemClickListener {
mWindowAdapter.refresh(time, forced);
}
public void search(String searchQuery, boolean forced) {
Time time = new Time();
long goToTime = getFirstVisibleTime();
if (goToTime <= 0) {
goToTime = System.currentTimeMillis();
}
time.set(goToTime);
mWindowAdapter.refresh(time, searchQuery, forced);
}
public void refresh(boolean forced) {
Time time = new Time();
long goToTime = getFirstVisibleTime();
......
......@@ -18,9 +18,11 @@ package com.android.calendar;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Calendar;
import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Instances;
......@@ -64,7 +66,11 @@ public class AgendaWindowAdapter extends BaseAdapter {
static final boolean DEBUGLOG = false;
private static String TAG = "AgendaWindowAdapter";
private static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
private static final String AGENDA_SORT_ORDER =
Calendar.Instances.START_DAY + " ASC, " +
Calendar.Instances.BEGIN + " ASC, " +
Calendar.Events.TITLE + " ASC";
public static final int INDEX_TITLE = 1;
public static final int INDEX_EVENT_LOCATION = 2;
public static final int INDEX_ALL_DAY = 3;
......@@ -108,8 +114,8 @@ public class AgendaWindowAdapter extends BaseAdapter {
private static final int PREFETCH_BOUNDARY = 1;
// Times to auto-expand/retry query after getting no data
private static final int RETRIES_ON_NO_DATA = 0;
/** Times to auto-expand/retry query after getting no data */
private static final int RETRIES_ON_NO_DATA = 1;
private Context mContext;
......@@ -117,11 +123,14 @@ public class AgendaWindowAdapter extends BaseAdapter {
private AgendaListView mAgendaListView;
private int mRowCount; // The sum of the rows in all the adapters
/** The sum of the rows in all the adapters */
private int mRowCount;
/** The number of times we have queried and gotten no results back */
private int mEmptyCursorCount;
private DayAdapterInfo mLastUsedInfo; // Cached value of the last used adapter.
/** Cached value of the last used adapter */
private DayAdapterInfo mLastUsedInfo;
private LinkedList<DayAdapterInfo> mAdapterInfos = new LinkedList<DayAdapterInfo>();
......@@ -133,24 +142,24 @@ public class AgendaWindowAdapter extends BaseAdapter {
private boolean mDoneSettingUpHeaderFooter = false;
/*
/**
* When the user scrolled to the top, a query will be made for older events
* and this will be incremented. Don't make more requests if
* mOlderRequests > mOlderRequestsProcessed.
*/
private int mOlderRequests;
// Number of "older" query that has been processed.
/** Number of "older" query that has been processed. */
private int mOlderRequestsProcessed;
/*
/**
* When the user scrolled to the bottom, a query will be made for newer
* events and this will be incremented. Don't make more requests if
* mNewerRequests > mNewerRequestsProcessed.
*/
private int mNewerRequests;
// Number of "newer" query that has been processed.
/** Number of "newer" query that has been processed. */
private int mNewerRequestsProcessed;
// Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
......@@ -160,6 +169,9 @@ public class AgendaWindowAdapter extends BaseAdapter {
private boolean mShuttingDown;
private boolean mHideDeclined;
/** The current search query, or null if none */
private String mSearchQuery;
// Types of Query
private static final int QUERY_TYPE_OLDER = 0; // Query for older events
private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
......@@ -174,6 +186,8 @@ public class AgendaWindowAdapter extends BaseAdapter {
int end;
String searchQuery;
int queryType;
public QuerySpec(int queryType) {
......@@ -188,6 +202,7 @@ public class AgendaWindowAdapter extends BaseAdapter {
result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
result = prime * result + queryType;
result = prime * result + start;
result = prime * result + searchQuery.hashCode();
if (goToTime != null) {
long goToTimeMillis = goToTime.toMillis(false);
result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
......@@ -202,9 +217,11 @@ public class AgendaWindowAdapter extends BaseAdapter {
if (getClass() != obj.getClass()) return false;
QuerySpec other = (QuerySpec) obj;
if (end != other.end || queryStartMillis != other.queryStartMillis
|| queryType != other.queryType || start != other.start) {
|| queryType != other.queryType || start != other.start
|| Utils.equals(searchQuery, other.searchQuery)) {
return false;
}
if (goToTime != null) {
if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
return false;
......@@ -259,16 +276,18 @@ public class AgendaWindowAdapter extends BaseAdapter {
}
}
public AgendaWindowAdapter(AgendaActivity agendaActivity,
public AgendaWindowAdapter(Context context,
AgendaListView agendaListView) {
mContext = agendaActivity;
mContext = context;
mAgendaListView = agendaListView;
mQueryHandler = new QueryHandler(agendaActivity.getContentResolver());
mQueryHandler = new QueryHandler(context.getContentResolver());
mStringBuilder = new StringBuilder(50);
mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
LayoutInflater inflater = (LayoutInflater) agendaActivity
mSearchQuery = null;
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
......@@ -469,6 +488,15 @@ public class AgendaWindowAdapter extends BaseAdapter {
}
public void refresh(Time goToTime, boolean forced) {
refresh(goToTime, mSearchQuery, forced);
}
public void refresh(Time goToTime, String searchQuery, boolean forced) {
if (!Utils.equals(searchQuery, mSearchQuery)) {
// When we change search terms, clean up any old state, start over
resetInstanceFields();
}
mSearchQuery = searchQuery;
if (DEBUGLOG) {
Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced"));
}
......@@ -484,7 +512,7 @@ public class AgendaWindowAdapter extends BaseAdapter {
// Query for a total of MIN_QUERY_DURATION days
int endDay = startDay + MIN_QUERY_DURATION;
queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN);
queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN);
}
public void close() {
......@@ -539,6 +567,20 @@ public class AgendaWindowAdapter extends BaseAdapter {
}
}
/**
* Resets any transient state in this instance and puts it back into a state
* where it can be treated as a newly instantiated adapter
*
* TODO are these all of the fields that need to be reset?
*/
private void resetInstanceFields() {
mEmptyCursorCount = 0;
mNewerRequests = 0;
mNewerRequestsProcessed = 0;
mOlderRequests = 0;
mOlderRequestsProcessed = 0;
}
private String buildQuerySelection() {
// Respect the preference to show/hide declined events
......@@ -551,13 +593,17 @@ public class AgendaWindowAdapter extends BaseAdapter {
}
}
private Uri buildQueryUri(int start, int end) {
StringBuilder path = new StringBuilder();
path.append(start);
path.append('/');
path.append(end);
Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString());
return uri;
private Uri buildQueryUri(int start, int end, String searchQuery) {
Uri rootUri = searchQuery == null ?
Instances.CONTENT_BY_DAY_URI :
Instances.CONTENT_SEARCH_BY_DAY_URI;
Uri.Builder builder = rootUri.buildUpon();
ContentUris.appendId(builder, start);
ContentUris.appendId(builder, end);
if (searchQuery != null) {
builder.appendPath(searchQuery);
}
return builder.build();
}
private boolean isInRange(int start, int end) {
......@@ -584,15 +630,18 @@ public class AgendaWindowAdapter extends BaseAdapter {
return queryDuration;
}
private boolean queueQuery(int start, int end, Time goToTime, int queryType) {
private boolean queueQuery(int start, int end, Time goToTime,
String searchQuery, int queryType) {
QuerySpec queryData = new QuerySpec(queryType);
queryData.goToTime = goToTime;
queryData.start = start;
queryData.end = end;
queryData.searchQuery = searchQuery;
return queueQuery(queryData);
}
private boolean queueQuery(QuerySpec queryData) {
queryData.searchQuery = mSearchQuery;
Boolean queuedQuery;
synchronized (mQueryQueue) {
queuedQuery = false;
......@@ -634,9 +683,12 @@ public class AgendaWindowAdapter extends BaseAdapter {
mQueryHandler.cancelOperation(0);
if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
mQueryHandler.startQuery(0, queryData, buildQueryUri(
queryData.start, queryData.end), PROJECTION,
buildQuerySelection(), null, AGENDA_SORT_ORDER);
Uri queryUri = buildQueryUri(
queryData.start, queryData.end, queryData.searchQuery);
mQueryHandler.startQuery(0, queryData, queryUri,
PROJECTION, buildQuerySelection(), null,
AGENDA_SORT_ORDER);
}
private String formatDateString(int julianDay) {
......
/*
* Copyright (C) 2010 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.calendar;
import static android.provider.Calendar.EVENT_BEGIN_TIME;
import dalvik.system.VMRuntime;
import android.app.Activity;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Calendar.Events;
import android.text.format.Time;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
/**
*/
public class SearchActivity extends Activity implements Navigator {
private static final String TAG = SearchActivity.class.getSimpleName();
private static boolean DEBUG = false;
private static final long INITIAL_HEAP_SIZE = 4*1024*1024;
protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time";
protected static final String BUNDLE_KEY_RESTORE_SEARCH_QUERY =
"key_restore_search_query";
private ContentResolver mContentResolver;
private AgendaListView mAgendaListView;
private Time mTime;
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_TIME_CHANGED)
|| action.equals(Intent.ACTION_DATE_CHANGED)
|| action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
mAgendaListView.refresh(true);
}
}
};
private ContentObserver mObserver = new ContentObserver(new Handler()) {
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
mAgendaListView.refresh(true);
}
};
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Eliminate extra GCs during startup by setting the initial heap size to 4MB.
// TODO: We should restore the old heap size once the activity reaches the idle state
VMRuntime.getRuntime().setMinimumHeapSize(INITIAL_HEAP_SIZE);
mAgendaListView = new AgendaListView(this);
setContentView(mAgendaListView);
mContentResolver = getContentResolver();
setTitle(R.string.search);
long millis = 0;
mTime = new Time();
if (icicle != null) {
// Returns 0 if key not found
millis = icicle.getLong(BUNDLE_KEY_RESTORE_TIME);
if (DEBUG) {
Log.v(TAG, "Restore value from icicle: " + millis);
}
}
if (millis == 0) {
// Didn't find a time in the bundle, look in intent or current time
millis = Utils.timeFromIntentInMillis(getIntent());
}
Intent intent = getIntent();
mTime.set(millis);
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
search(query);
}
}
@Override
protected void onNewIntent(Intent intent) {
// From the Android Dev Guide: "It's important to note that when
// onNewIntent(Intent) is called, the Activity has not been restarted,
// so the getIntent() method will still return the Intent that was first
// received with onCreate(). This is why setIntent(Intent) is called
// inside onNewIntent(Intent) (just in case you call getIntent() at a
// later time)."
setIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
search(query);
} else {
long time = Utils.timeFromIntentInMillis(intent);
if (time > 0) {
mTime.set(time);
goTo(mTime, false);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
long firstVisibleTime = mAgendaListView.getFirstVisibleTime();
if (firstVisibleTime > 0) {
mTime.set(firstVisibleTime);
outState.putLong(BUNDLE_KEY_RESTORE_TIME, firstVisibleTime);
if (DEBUG) {
Log.v(TAG, "onSaveInstanceState " + mTime.toString());
}
}
}
@Override
protected void onResume() {
super.onResume();
if (DEBUG) {
Log.v(TAG, "OnResume to " + mTime.toString());
}
SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(
getApplicationContext());
boolean hideDeclined = prefs.getBoolean(
CalendarPreferenceActivity.KEY_HIDE_DECLINED, false);
mAgendaListView.setHideDeclinedEvents(hideDeclined);
mAgendaListView.goTo(mTime, true);
mAgendaListView.onResume();
// Register for Intent broadcasts
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_DATE_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
registerReceiver(mIntentReceiver, filter);
mContentResolver.registerContentObserver(Events.CONTENT_URI, true, mObserver);
}
@Override
protected void onPause() {
super.onPause();
mAgendaListView.onPause();
mContentResolver.unregisterContentObserver(mObserver);
unregisterReceiver(mIntentReceiver);
// Record Agenda View as the (new) default detailed view.
Utils.setDefaultView(this, CalendarApplication.AGENDA_VIEW_ID);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuHelper.onPrepareOptionsMenu(this, menu);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuHelper.onCreateOptionsMenu(menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
MenuHelper.onOptionsItemSelected(this, item, this);
return super.onOptionsItemSelected(item);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
// Delete the currently selected event (if any)
mAgendaListView.deleteSelectedEvent();
break;
}
return super.onKeyDown(keyCode, event);
}
private void search(String searchQuery) {
mAgendaListView.search(searchQuery, true);
}
/* Navigator interface methods */
public void goToToday() {
Time now = new Time();
now.setToNow();
mAgendaListView.goTo(now, true); // Force refresh
}
public void goTo(Time time, boolean animate) {
mAgendaListView.goTo(time, false);
}
public long getSelectedTime() {
return mAgendaListView.getSelectedTime();
}
public boolean getAllDay() {
return false;
}
}
......@@ -307,4 +307,14 @@ public class Utils {
}
}
}
/**
* Null-safe object comparison
* @param s1
* @param s2
* @return
*/
public static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
}
......@@ -76,4 +76,17 @@ public class UtilsTests extends TestCase {
Utils.checkForDuplicateNames(mIsDuplicateName, mDuplicateNameCursor, NAME_COLUMN);
assertEquals(mIsDuplicateName, mIsDuplicateNameExpected);
}
@Smoke
@SmallTest
public void testEquals() {
assertTrue(Utils.equals(null, null));
assertFalse(Utils.equals("", null));
assertFalse(Utils.equals(null, ""));
assertTrue(Utils.equals("",""));
Integer int1 = new Integer(1);
Integer int2 = new Integer(1);
assertTrue(Utils.equals(int1, int2));
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment