diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch) | |
tree | 35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/provider | |
download | frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2 |
Initial Contribution
Diffstat (limited to 'core/java/android/provider')
19 files changed, 14905 insertions, 0 deletions
diff --git a/core/java/android/provider/BaseColumns.java b/core/java/android/provider/BaseColumns.java new file mode 100644 index 0000000..f594c19 --- /dev/null +++ b/core/java/android/provider/BaseColumns.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2006 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 android.provider; + +public interface BaseColumns +{ + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + */ + public static final String _ID = "_id"; + + /** + * The count of rows in a directory. + * <P>Type: INTEGER</P> + */ + public static final String _COUNT = "_count"; +} diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java new file mode 100644 index 0000000..7aaed49 --- /dev/null +++ b/core/java/android/provider/Browser.java @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.util.Log; +import android.webkit.WebIconDatabase; + +import java.util.Date; + +public class Browser { + private static final String LOGTAG = "browser"; + public static final Uri BOOKMARKS_URI = + Uri.parse("content://browser/bookmarks"); + + /** + * The name of extra data when starting Browser with ACTION_VIEW or + * ACTION_SEARCH intent. + * <p> + * The value should be an integer between 0 and 1000. If not set or set to + * 0, the Browser will use default. If set to 100, the Browser will start + * with 100%. + */ + public static final String INITIAL_ZOOM_LEVEL = "browser.initialZoomLevel"; + + /* if you change column order you must also change indices + below */ + public static final String[] HISTORY_PROJECTION = new String[] { + BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, + BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, + BookmarkColumns.FAVICON }; + + /* these indices dependent on HISTORY_PROJECTION */ + public static final int HISTORY_PROJECTION_ID_INDEX = 0; + public static final int HISTORY_PROJECTION_URL_INDEX = 1; + public static final int HISTORY_PROJECTION_VISITS_INDEX = 2; + public static final int HISTORY_PROJECTION_DATE_INDEX = 3; + public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4; + public static final int HISTORY_PROJECTION_TITLE_INDEX = 5; + public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6; + + /* columns needed to determine whether to truncate history */ + public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { + BookmarkColumns._ID, BookmarkColumns.DATE, }; + public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0; + + /* truncate this many history items at a time */ + public static final int TRUNCATE_N_OLDEST = 5; + + public static final Uri SEARCHES_URI = + Uri.parse("content://browser/searches"); + + /* if you change column order you must also change indices + below */ + public static final String[] SEARCHES_PROJECTION = new String[] { + SearchColumns._ID, SearchColumns.SEARCH, SearchColumns.DATE }; + + /* these indices dependent on SEARCHES_PROJECTION */ + public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1; + public static final int SEARCHES_PROJECTION_DATE_INDEX = 2; + + private static final String SEARCHES_WHERE_CLAUSE = "search = ?"; + + /* Set a cap on the count of history items in the history/bookmark + table, to prevent db and layout operations from dragging to a + crawl. Revisit this cap when/if db/layout performance + improvements are made. Note: this does not affect bookmark + entries -- if the user wants more bookmarks than the cap, they + get them. */ + private static final int MAX_HISTORY_COUNT = 250; + + /** + * Open the AddBookmark activity to save a bookmark. Launch with + * and/or url, which can be edited by the user before saving. + * @param c Context used to launch the AddBookmark activity. + * @param title Title for the bookmark. Can be null or empty string. + * @param url Url for the bookmark. Can be null or empty string. + */ + public static final void saveBookmark(Context c, + String title, + String url) { + Intent i = new Intent(Intent.ACTION_INSERT, Browser.BOOKMARKS_URI); + i.putExtra("title", title); + i.putExtra("url", url); + c.startActivity(i); + } + + public static final void sendString(Context c, String s) { + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + send.putExtra(Intent.EXTRA_TEXT, s); + + try { + c.startActivity(Intent.createChooser(send, + c.getText(com.android.internal.R.string.sendText))); + } catch(android.content.ActivityNotFoundException ex) { + // if no app handles it, do nothing + } + } + + /** + * Return a cursor pointing to a list of all the bookmarks. + * @param cr The ContentResolver used to access the database. + */ + public static final Cursor getAllBookmarks(ContentResolver cr) throws + IllegalStateException { + return cr.query(BOOKMARKS_URI, + new String[] { BookmarkColumns.URL }, + "bookmark = 1", null, null); + } + + /** + * Return a cursor pointing to a list of all visited site urls. + * @param cr The ContentResolver used to access the database. + */ + public static final Cursor getAllVisitedUrls(ContentResolver cr) throws + IllegalStateException { + return cr.query(BOOKMARKS_URI, + new String[] { BookmarkColumns.URL }, null, null, null); + } + + /** + * Update the visited history to acknowledge that a site has been + * visited. + * @param cr The ContentResolver used to access the database. + * @param url The site being visited. + * @param real Whether this is an actual visit, and should be added to the + * number of visits. + */ + public static final void updateVisitedHistory(ContentResolver cr, + String url, boolean real) { + long now = new Date().getTime(); + try { + StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = "); + DatabaseUtils.appendEscapedSQLString(sb, url); + Cursor c = cr.query( + BOOKMARKS_URI, + HISTORY_PROJECTION, + sb.toString(), + null, + null); + /* We should only get one answer that is exactly the same. */ + if (c.moveToFirst()) { + ContentValues map = new ContentValues(); + if (real) { + map.put(BookmarkColumns.VISITS, c + .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1); + } + map.put(BookmarkColumns.DATE, now); + cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null); + } else { + truncateHistory(cr); + ContentValues map = new ContentValues(); + map.put(BookmarkColumns.URL, url); + map.put(BookmarkColumns.VISITS, real ? 1 : 0); + map.put(BookmarkColumns.DATE, now); + map.put(BookmarkColumns.BOOKMARK, 0); + map.put(BookmarkColumns.TITLE, url); + map.put(BookmarkColumns.CREATED, 0); + cr.insert(BOOKMARKS_URI, map); + } + c.deactivate(); + } catch (IllegalStateException e) { + return; + } + } + + /** + * If there are more than MAX_HISTORY_COUNT non-bookmark history + * items in the bookmark/history table, delete TRUNCATE_N_OLDEST + * of them. This is used to keep our history table to a + * reasonable size. Note: it does not prune bookmarks. If the + * user wants 1000 bookmarks, the user gets 1000 bookmarks. + * + * @param cr The ContentResolver used to access the database. + */ + public static final void truncateHistory(ContentResolver cr) { + try { + // Select non-bookmark history, ordered by date + Cursor c = cr.query( + BOOKMARKS_URI, + TRUNCATE_HISTORY_PROJECTION, + "bookmark = 0", + null, + BookmarkColumns.DATE); + // Log.v(LOGTAG, "history count " + c.count()); + if (c.moveToFirst() && c.getCount() >= MAX_HISTORY_COUNT) { + /* eliminate oldest history items */ + for (int i = 0; i < TRUNCATE_N_OLDEST; i++) { + // Log.v(LOGTAG, "truncate history " + + // c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX)); + deleteHistoryWhere( + cr, "_id = " + + c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX)); + if (!c.moveToNext()) break; + } + } + c.deactivate(); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "truncateHistory", e); + return; + } + } + + /** + * Returns whether there is any history to clear. + * @param cr The ContentResolver used to access the database. + * @return boolean True if the history can be cleared. + */ + public static final boolean canClearHistory(ContentResolver cr) { + try { + Cursor c = cr.query( + BOOKMARKS_URI, + new String [] { BookmarkColumns._ID, + BookmarkColumns.BOOKMARK, + BookmarkColumns.VISITS }, + "bookmark = 0 OR visits > 0", + null, + null + ); + boolean ret = c.moveToFirst(); + c.deactivate(); + return ret; + } catch (IllegalStateException e) { + return false; + } + } + + /** + * Delete all entries from the bookmarks/history table which are + * not bookmarks. Also set all visited bookmarks to unvisited. + * @param cr The ContentResolver used to access the database. + */ + public static final void clearHistory(ContentResolver cr) { + deleteHistoryWhere(cr, null); + } + + /** + * Helper function to delete all history items and revert all + * bookmarks to zero visits which meet the criteria provided. + * @param cr The ContentResolver used to access the database. + * @param whereClause String to limit the items affected. + * null means all items. + */ + private static final void deleteHistoryWhere(ContentResolver cr, + String whereClause) { + try { + Cursor c = cr.query(BOOKMARKS_URI, + HISTORY_PROJECTION, + whereClause, + null, + null); + if (!c.moveToFirst()) { + c.deactivate(); + return; + } + + final WebIconDatabase iconDb = WebIconDatabase.getInstance(); + /* Delete favicons, and revert bookmarks which have been visited + * to simply bookmarks. + */ + StringBuffer sb = new StringBuffer(); + boolean firstTime = true; + do { + String url = c.getString(HISTORY_PROJECTION_URL_INDEX); + boolean isBookmark = + c.getInt(HISTORY_PROJECTION_BOOKMARK_INDEX) == 1; + if (isBookmark) { + if (firstTime) { + firstTime = false; + } else { + sb.append(" OR "); + } + sb.append("( _id = "); + sb.append(c.getInt(0)); + sb.append(" )"); + } else { + iconDb.releaseIconForPageUrl(url); + } + } while (c.moveToNext()); + c.deactivate(); + + if (!firstTime) { + ContentValues map = new ContentValues(); + map.put(BookmarkColumns.VISITS, 0); + map.put(BookmarkColumns.DATE, 0); + /* FIXME: Should I also remove the title? */ + cr.update(BOOKMARKS_URI, map, sb.toString(), null); + } + + String deleteWhereClause = BookmarkColumns.BOOKMARK + " = 0"; + if (whereClause != null) { + deleteWhereClause += " AND " + whereClause; + } + cr.delete(BOOKMARKS_URI, deleteWhereClause, null); + } catch (IllegalStateException e) { + return; + } + } + + /** + * Delete all history items from begin to end. + * @param cr The ContentResolver used to access the database. + * @param begin First date to remove. If -1, all dates before end. + * Inclusive. + * @param end Last date to remove. If -1, all dates after begin. + * Non-inclusive. + */ + public static final void deleteHistoryTimeFrame(ContentResolver cr, + long begin, long end) { + String whereClause; + String date = BookmarkColumns.DATE; + if (-1 == begin) { + if (-1 == end) { + clearHistory(cr); + return; + } + whereClause = date + " < " + Long.toString(end); + } else if (-1 == end) { + whereClause = date + " >= " + Long.toString(begin); + } else { + whereClause = date + " >= " + Long.toString(begin) + " AND " + date + + " < " + Long.toString(end); + } + deleteHistoryWhere(cr, whereClause); + } + + /** + * Remove a specific url from the history database. + * @param cr The ContentResolver used to access the database. + * @param url url to remove. + */ + public static final void deleteFromHistory(ContentResolver cr, + String url) { + StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = "); + DatabaseUtils.appendEscapedSQLString(sb, url); + String matchesUrl = sb.toString(); + deleteHistoryWhere(cr, matchesUrl); + } + + /** + * Add a search string to the searches database. + * @param cr The ContentResolver used to access the database. + * @param search The string to add to the searches database. + */ + public static final void addSearchUrl(ContentResolver cr, String search) { + long now = new Date().getTime(); + try { + Cursor c = cr.query( + SEARCHES_URI, + SEARCHES_PROJECTION, + SEARCHES_WHERE_CLAUSE, + new String [] { search }, + null); + /* We should only get one answer that is exactly the same. */ + if (c.moveToFirst()) { + ContentValues map = new ContentValues(); + map.put(BookmarkColumns.DATE, now); + cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null); + } else { + ContentValues map = new ContentValues(); + map.put(SearchColumns.SEARCH, search); + map.put(SearchColumns.DATE, now); + cr.insert(SEARCHES_URI, map); + } + c.deactivate(); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "addSearchUrl", e); + return; + } + } + /** + * Remove all searches from the search database. + * @param cr The ContentResolver used to access the database. + */ + public static final void clearSearches(ContentResolver cr) { + // FIXME: Should this clear the urls to which these searches lead? + // (i.e. remove google.com/query= blah blah blah) + try { + cr.delete(SEARCHES_URI, null, null); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "clearSearches", e); + } + } + + /** + * Request all icons from the database. + * @param cr The ContentResolver used to access the database. + * @param where Clause to be used to limit the query from the database. + * Must be an allowable string to be passed into a database query. + * @param listener IconListener that gets the icons once they are + * retrieved. + */ + public static final void requestAllIcons(ContentResolver cr, String where, + WebIconDatabase.IconListener listener) { + try { + final Cursor c = cr.query( + BOOKMARKS_URI, + HISTORY_PROJECTION, + where, null, null); + if (c.moveToFirst()) { + final WebIconDatabase db = WebIconDatabase.getInstance(); + do { + db.requestIconForPageUrl( + c.getString(HISTORY_PROJECTION_URL_INDEX), + listener); + } while (c.moveToNext()); + } + c.deactivate(); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "requestAllIcons", e); + } + } + + public static class BookmarkColumns implements BaseColumns { + public static final String URL = "url"; + public static final String VISITS = "visits"; + public static final String DATE = "date"; + public static final String BOOKMARK = "bookmark"; + public static final String TITLE = "title"; + public static final String CREATED = "created"; + public static final String FAVICON = "favicon"; + } + + public static class SearchColumns implements BaseColumns { + public static final String URL = "url"; + public static final String SEARCH = "search"; + public static final String DATE = "date"; + } +} diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java new file mode 100644 index 0000000..b07f1b8 --- /dev/null +++ b/core/java/android/provider/Calendar.java @@ -0,0 +1,1115 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import com.google.android.gdata.client.AndroidGDataClient; +import com.google.android.gdata.client.AndroidXmlParserFactory; +import com.google.wireless.gdata.calendar.client.CalendarClient; +import com.google.wireless.gdata.calendar.data.EventEntry; +import com.google.wireless.gdata.calendar.data.Who; +import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory; +import com.google.wireless.gdata.client.AuthenticationException; +import com.google.wireless.gdata.client.AllDeletedUnavailableException; +import com.google.wireless.gdata.data.Entry; +import com.google.wireless.gdata.data.StringUtils; +import com.google.wireless.gdata.parser.ParseException; +import com.android.internal.database.ArrayListCursor; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.pim.DateUtils; +import android.pim.ICalendar; +import android.pim.RecurrenceSet; +import android.pim.Time; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Vector; + +/** + * The Calendar provider contains all calendar events. + * + * @hide + */ +public final class Calendar { + + public static final String TAG = "Calendar"; + + /** + * Broadcast Action: An event reminder. + */ + public static final String + EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER"; + + /** + * These are the symbolic names for the keys used in the extra data + * passed in the intent for event reminders. + */ + public static final String EVENT_BEGIN_TIME = "beginTime"; + public static final String EVENT_END_TIME = "endTime"; + + public static final String AUTHORITY = "calendar"; + + /** + * The content:// style URL for the top-level calendar authority + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY); + + /** + * Columns from the Calendars table that other tables join into themselves. + */ + public interface CalendarsColumns + { + /** + * The color of the calendar + * <P>Type: INTEGER (color value)</P> + */ + public static final String COLOR = "color"; + + /** + * The level of access that the user has for the calendar + * <P>Type: INTEGER (one of the values below)</P> + */ + public static final String ACCESS_LEVEL = "access_level"; + + /** Cannot access the calendar */ + public static final int NO_ACCESS = 0; + /** Can only see free/busy information about the calendar */ + public static final int FREEBUSY_ACCESS = 100; + /** Can read all event details */ + public static final int READ_ACCESS = 200; + public static final int RESPOND_ACCESS = 300; + public static final int OVERRIDE_ACCESS = 400; + /** Full access to modify the calendar, but not the access control settings */ + public static final int CONTRIBUTOR_ACCESS = 500; + public static final int EDITOR_ACCESS = 600; + /** Full access to the calendar */ + public static final int OWNER_ACCESS = 700; + public static final int ROOT_ACCESS = 800; + + /** + * Is the calendar selected to be displayed? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String SELECTED = "selected"; + + /** + * The timezone the calendar's events occurs in + * <P>Type: TEXT</P> + */ + public static final String TIMEZONE = "timezone"; + + /** + * If this calendar is in the list of calendars that are selected for + * syncing then "sync_events" is 1, otherwise 0. + * <p>Type: INTEGER (boolean)</p> + */ + public static final String SYNC_EVENTS = "sync_events"; + } + + /** + * Contains a list of available calendars. + */ + public static class Calendars implements BaseColumns, SyncConstValue, CalendarsColumns + { + public static final Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) + { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * Convenience method perform a delete on the Calendar provider + * + * @param cr the ContentResolver + * @param selection the rows to delete + * @return the count of rows that were deleted + */ + public static int delete(ContentResolver cr, String selection, String[] selectionArgs) + { + return cr.delete(CONTENT_URI, selection, selectionArgs); + } + + /** + * Convenience method to delete all calendars that match the account. + * + * @param cr the ContentResolver + * @param account the account whose rows should be deleted + * @return the count of rows that were deleted + */ + public static int deleteCalendarsForAccount(ContentResolver cr, + String account) { + // delete all calendars that match this account + return Calendar.Calendars.delete(cr, Calendar.Calendars._SYNC_ACCOUNT + "=?", + new String[] {account}); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://calendar/calendars"); + + public static final Uri LIVE_CONTENT_URI = + Uri.parse("content://calendar/calendars?update=1"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "displayName"; + + /** + * The URL to the calendar + * <P>Type: TEXT (URL)</P> + */ + public static final String URL = "url"; + + /** + * The name of the calendar + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The display name of the calendar + * <P>Type: TEXT</P> + */ + public static final String DISPLAY_NAME = "displayName"; + + /** + * The location the of the events in the calendar + * <P>Type: TEXT</P> + */ + public static final String LOCATION = "location"; + + /** + * Should the calendar be hidden in the calendar selection panel? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String HIDDEN = "hidden"; + } + + public interface AttendeesColumns { + + /** + * The id of the event. + * <P>Type: INTEGER</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The name of the attendee. + * <P>Type: STRING</P> + */ + public static final String ATTENDEE_NAME = "attendeeName"; + + /** + * The email address of the attendee. + * <P>Type: STRING</P> + */ + public static final String ATTENDEE_EMAIL = "attendeeEmail"; + + /** + * The relationship of the attendee to the user. + * <P>Type: INTEGER (one of {@link #RELATIONSHIP_ATTENDEE}, ...}. + */ + public static final String ATTENDEE_RELATIONSHIP = "attendeeRelationship"; + + public static final int RELATIONSHIP_NONE = 0; + public static final int RELATIONSHIP_ATTENDEE = 1; + public static final int RELATIONSHIP_ORGANIZER = 2; + public static final int RELATIONSHIP_PERFORMER = 3; + public static final int RELATIONSHIP_SPEAKER = 4; + + /** + * The type of attendee. + * <P>Type: Integer (one of {@link #TYPE_REQUIRED}, {@link #TYPE_OPTIONAL}) + */ + public static final String ATTENDEE_TYPE = "attendeeType"; + + public static final int TYPE_NONE = 0; + public static final int TYPE_REQUIRED = 1; + public static final int TYPE_OPTIONAL = 2; + + /** + * The attendance status of the attendee. + * <P>Type: Integer (one of {@link #ATTENDEE_STATUS_ACCEPTED}, ...}. + */ + public static final String ATTENDEE_STATUS = "attendeeStatus"; + + public static final int ATTENDEE_STATUS_NONE = 0; + public static final int ATTENDEE_STATUS_ACCEPTED = 1; + public static final int ATTENDEE_STATUS_DECLINED = 2; + public static final int ATTENDEE_STATUS_INVITED = 3; + public static final int ATTENDEE_STATUS_TENTATIVE = 4; + } + + public static final class Attendees implements BaseColumns, + AttendeesColumns, EventsColumns { + public static final Uri CONTENT_URI = + Uri.parse("content://calendar/attendees"); + + // TODO: fill out this class when we actually start utilizing attendees + // in the calendar application. + } + + /** + * Columns from the Events table that other tables join into themselves. + */ + public interface EventsColumns + { + /** + * The calendar the event belongs to + * <P>Type: INTEGER (foreign key to the Calendars table)</P> + */ + public static final String CALENDAR_ID = "calendar_id"; + + /** + * The URI for an HTML version of this event. + * <P>Type: TEXT</P> + */ + public static final String HTML_URI = "htmlUri"; + + /** + * The title of the event + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * The description of the event + * <P>Type: TEXT</P> + */ + public static final String DESCRIPTION = "description"; + + /** + * Where the event takes place. + * <P>Type: TEXT</P> + */ + public static final String EVENT_LOCATION = "eventLocation"; + + /** + * The event status + * <P>Type: INTEGER (int)</P> + */ + public static final String STATUS = "eventStatus"; + + public static final int STATUS_TENTATIVE = 0; + public static final int STATUS_CONFIRMED = 1; + public static final int STATUS_CANCELED = 2; + + /** + * This is a copy of the attendee status for the owner of this event. + * This field is copied here so that we can efficiently filter out + * events that are declined without having to look in the Attendees + * table. + * + * <P>Type: INTEGER (int)</P> + */ + public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus"; + + /** + * The comments feed uri. + * <P>Type: TEXT</P> + */ + public static final String COMMENTS_URI = "commentsUri"; + + /** + * The time the event starts + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTSTART = "dtstart"; + + /** + * The time the event ends + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTEND = "dtend"; + + /** + * The duration of the event + * <P>Type: TEXT (duration in RFC2445 format)</P> + */ + public static final String DURATION = "duration"; + + /** + * The timezone for the event. + * <P>Type: TEXT + */ + public static final String EVENT_TIMEZONE = "eventTimezone"; + + /** + * Whether the event lasts all day or not + * <P>Type: INTEGER (boolean)</P> + */ + public static final String ALL_DAY = "allDay"; + + /** + * Visibility for the event. + * <P>Type: INTEGER</P> + */ + public static final String VISIBILITY = "visibility"; + + public static final int VISIBILITY_DEFAULT = 0; + public static final int VISIBILITY_CONFIDENTIAL = 1; + public static final int VISIBILITY_PRIVATE = 2; + public static final int VISIBILITY_PUBLIC = 3; + + /** + * Transparency for the event -- does the event consume time on the calendar? + * <P>Type: INTEGER</P> + */ + public static final String TRANSPARENCY = "transparency"; + + public static final int TRANSPARENCY_OPAQUE = 0; + + public static final int TRANSPARENCY_TRANSPARENT = 1; + + /** + * Whether the event has an alarm or not + * <P>Type: INTEGER (boolean)</P> + */ + public static final String HAS_ALARM = "hasAlarm"; + + /** + * Whether the event has extended properties or not + * <P>Type: INTEGER (boolean)</P> + */ + public static final String HAS_EXTENDED_PROPERTIES = "hasExtendedProperties"; + + /** + * The recurrence rule for the event. + * than one. + * <P>Type: TEXT</P> + */ + public static final String RRULE = "rrule"; + + /** + * The recurrence dates for the event. + * <P>Type: TEXT</P> + */ + public static final String RDATE = "rdate"; + + /** + * The recurrence exception rule for the event. + * <P>Type: TEXT</P> + */ + public static final String EXRULE = "exrule"; + + /** + * The recurrence exception dates for the event. + * <P>Type: TEXT</P> + */ + public static final String EXDATE = "exdate"; + + /** + * The original event this event is an exception for + * <P>Type: TEXT</P> + */ + public static final String ORIGINAL_EVENT = "originalEvent"; + + /** + * The time of the original instance time this event is an exception for + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String ORIGINAL_INSTANCE_TIME = "originalInstanceTime"; + + /** + * The last date this event repeats on, or NULL if it never ends + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String LAST_DATE = "lastDate"; + } + + /** + * Contains one entry per calendar event. Recurring events show up as a single entry. + */ + public static final class Events implements BaseColumns, SyncConstValue, + EventsColumns, CalendarsColumns { + + private static final String[] FETCH_ENTRY_COLUMNS = + new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; + + private static final String[] ATTENDEES_COLUMNS = + new String[] { AttendeesColumns.ATTENDEE_NAME, + AttendeesColumns.ATTENDEE_EMAIL, + AttendeesColumns.ATTENDEE_RELATIONSHIP, + AttendeesColumns.ATTENDEE_TYPE, + AttendeesColumns.ATTENDEE_STATUS }; + + private static CalendarClient sCalendarClient = null; + + public static final Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + private static String extractValue(ICalendar.Component component, + String propertyName) { + ICalendar.Property property = + component.getFirstProperty(propertyName); + if (property != null) { + return property.getValue(); + } + return null; + } + + public static final Uri insertVEvent(ContentResolver cr, + ICalendar.Component event, long calendarId, int status, + ContentValues values) { + + // TODO: define VEVENT component names as constants in some + // appropriate class (ICalendar.Component?). + + values.clear(); + + // title + String title = extractValue(event, "SUMMARY"); + if (TextUtils.isEmpty(title)) { + if (Config.LOGD) { + Log.d(TAG, "No SUMMARY provided for event. " + + "Cannot import."); + } + return null; + } + values.put(TITLE, title); + + // status + values.put(STATUS, status); + + // description + String description = extractValue(event, "DESCRIPTION"); + if (!TextUtils.isEmpty(description)) { + values.put(DESCRIPTION, description); + } + + // where + String where = extractValue(event, "LOCATION"); + if (!StringUtils.isEmpty(where)) { + values.put(EVENT_LOCATION, where); + } + + // Calendar ID + values.put(CALENDAR_ID, calendarId); + + boolean timesSet = false; + + // TODO: deal with VALARMs + + // dtstart & dtend + Time time = new Time(Time.TIMEZONE_UTC); + String dtstart = null; + String dtend = null; + String duration = null; + ICalendar.Property dtstartProp = event.getFirstProperty("DTSTART"); + // TODO: handle "floating" timezone (no timezone specified). + if (dtstartProp != null) { + dtstart = dtstartProp.getValue(); + if (!TextUtils.isEmpty(dtstart)) { + ICalendar.Parameter tzidParam = + dtstartProp.getFirstParameter("TZID"); + if (tzidParam != null && tzidParam.value != null) { + time.clear(tzidParam.value); + } + try { + time.parse2445(dtstart); + } catch (Exception e) { + if (Config.LOGD) { + Log.d(TAG, "Cannot parse dtstart " + dtstart, e); + } + return null; + } + if (time.allDay) { + values.put(ALL_DAY, 1); + } + values.put(DTSTART, time.toMillis(false /* use isDst */)); + values.put(EVENT_TIMEZONE, time.timezone); + } + + ICalendar.Property dtendProp = event.getFirstProperty("DTEND"); + if (dtendProp != null) { + dtend = dtendProp.getValue(); + if (!TextUtils.isEmpty(dtend)) { + // TODO: make sure the timezones are the same for + // start, end. + try { + time.parse2445(dtend); + } catch (Exception e) { + if (Config.LOGD) { + Log.d(TAG, "Cannot parse dtend " + dtend, e); + } + return null; + } + values.put(DTEND, time.toMillis(false /* use isDst */)); + } + } else { + // look for a duration + ICalendar.Property durationProp = + event.getFirstProperty("DURATION"); + if (durationProp != null) { + duration = durationProp.getValue(); + if (!TextUtils.isEmpty(duration)) { + // TODO: check that it is valid? + values.put(DURATION, duration); + } + } + } + } + if (TextUtils.isEmpty(dtstart) || + (TextUtils.isEmpty(dtend) && TextUtils.isEmpty(duration))) { + if (Config.LOGD) { + Log.d(TAG, "No DTSTART or DTEND/DURATION defined."); + } + return null; + } + + // rrule + if (!RecurrenceSet.populateContentValues(event, values)) { + return null; + } + + return cr.insert(CONTENT_URI, values); + } + + /** + * Returns a singleton instance of the CalendarClient used to fetch entries from the + * calendar server. + * @param cr The ContentResolver used to lookup the address of the calendar server in the + * settings database. + * @return The singleton instance of the CalendarClient used to fetch entries from the + * calendar server. + */ + private static synchronized CalendarClient getCalendarClient(ContentResolver cr) { + if (sCalendarClient == null) { + sCalendarClient = new CalendarClient( + new AndroidGDataClient(cr), + new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory())); + } + return sCalendarClient; + } + + /** + * Extracts the attendees information out of event and adds it to a new ArrayList of columns + * within the supplied ArrayList of rows. These rows are expected to be used within an + * {@link ArrayListCursor}. + */ + private static final void extractAttendeesIntoArrayList(EventEntry event, + ArrayList<ArrayList> rows) { + Log.d(TAG, "EVENT: " + event.toString()); + Vector<Who> attendees = (Vector<Who>) event.getAttendees(); + + int numAttendees = attendees == null ? 0 : attendees.size(); + + for (int i = 0; i < numAttendees; ++i) { + Who attendee = attendees.elementAt(i); + ArrayList row = new ArrayList(); + row.add(attendee.getValue()); + row.add(attendee.getEmail()); + row.add(attendee.getRelationship()); + row.add(attendee.getType()); + row.add(attendee.getStatus()); + rows.add(row); + } + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://calendar/events"); + + public static final Uri DELETED_CONTENT_URI = + Uri.parse("content://calendar/deleted_events"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = ""; + } + + /** + * Contains one entry per calendar event instance. Recurring events show up every time + * they occur. + */ + public static final class Instances implements BaseColumns, EventsColumns, CalendarsColumns { + + public static final Cursor query(ContentResolver cr, String[] projection, + long begin, long end) { + Uri.Builder builder = CONTENT_URI.buildUpon(); + ContentUris.appendId(builder, begin); + ContentUris.appendId(builder, end); + return cr.query(builder.build(), projection, Calendars.SELECTED + "=1", + null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query(ContentResolver cr, String[] projection, + long begin, long end, String where, String orderBy) { + Uri.Builder builder = CONTENT_URI.buildUpon(); + ContentUris.appendId(builder, begin); + ContentUris.appendId(builder, end); + if (TextUtils.isEmpty(where)) { + where = Calendars.SELECTED + "=1"; + } else { + where = "(" + where + ") AND " + Calendars.SELECTED + "=1"; + } + return cr.query(builder.build(), projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://calendar/instances/when"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "begin ASC"; + + /** + * The sort order is: events with an earlier start time occur + * first and if the start times are the same, then events with + * a later end time occur first. The later end time is ordered + * first so that long-running events in the calendar views appear + * first. If the start and end times of two events are + * the same then we sort alphabetically on the title. This isn't + * required for correctness, it just adds a nice touch. + */ + public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC"; + + /** + * The beginning time of the instance, in UTC milliseconds + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String BEGIN = "begin"; + + /** + * The ending time of the instance, in UTC milliseconds + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String END = "end"; + + /** + * The event for this instance + * <P>Type: INTEGER (long, foreign key to the Events table)</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The Julian start day of the instance, relative to the local timezone + * <P>Type: INTEGER (int)</P> + */ + public static final String START_DAY = "startDay"; + + /** + * The Julian end day of the instance, relative to the local timezone + * <P>Type: INTEGER (int)</P> + */ + public static final String END_DAY = "endDay"; + + /** + * The start minute of the instance measured from midnight in the + * local timezone. + * <P>Type: INTEGER (int)</P> + */ + public static final String START_MINUTE = "startMinute"; + + /** + * The end minute of the instance measured from midnight in the + * local timezone. + * <P>Type: INTEGER (int)</P> + */ + public static final String END_MINUTE = "endMinute"; + } + + /** + * A few Calendar globals are needed in the CalendarProvider for expanding + * the Instances table and these are all stored in the first (and only) + * row of the CalendarMetaData table. + */ + public interface CalendarMetaDataColumns { + /** + * The local timezone that was used for precomputing the fields + * in the Instances table. + */ + public static final String LOCAL_TIMEZONE = "localTimezone"; + + /** + * The minimum time used in expanding the Instances table, + * in UTC milliseconds. + * <P>Type: INTEGER</P> + */ + public static final String MIN_INSTANCE = "minInstance"; + + /** + * The maximum time used in expanding the Instances table, + * in UTC milliseconds. + * <P>Type: INTEGER</P> + */ + public static final String MAX_INSTANCE = "maxInstance"; + + /** + * The minimum Julian day in the BusyBits table. + * <P>Type: INTEGER</P> + */ + public static final String MIN_BUSYBITS = "minBusyBits"; + + /** + * The maximum Julian day in the BusyBits table. + * <P>Type: INTEGER</P> + */ + public static final String MAX_BUSYBITS = "maxBusyBits"; + } + + public static final class CalendarMetaData implements CalendarMetaDataColumns { + } + + public interface BusyBitsColumns { + /** + * The Julian day number. + * <P>Type: INTEGER (int)</P> + */ + public static final String DAY = "day"; + + /** + * The 24 bits representing the 24 1-hour time slots in a day. + * If an event in the Instances table overlaps part of a 1-hour + * time slot then the corresponding bit is set. The first time slot + * (12am to 1am) is bit 0. The last time slot (11pm to midnight) + * is bit 23. + * <P>Type: INTEGER (int)</P> + */ + public static final String BUSYBITS = "busyBits"; + + /** + * The number of all-day events that occur on this day. + * <P>Type: INTEGER (int)</P> + */ + public static final String ALL_DAY_COUNT = "allDayCount"; + } + + public static final class BusyBits implements BusyBitsColumns { + public static final Uri CONTENT_URI = Uri.parse("content://calendar/busybits/when"); + + public static final String[] PROJECTION = { DAY, BUSYBITS, ALL_DAY_COUNT }; + + // The number of minutes represented by one busy bit + public static final int MINUTES_PER_BUSY_INTERVAL = 60; + + // The number of intervals in a day + public static final int INTERVALS_PER_DAY = 24 * 60 / MINUTES_PER_BUSY_INTERVAL; + + /** + * Retrieves the busy bits for the Julian days starting at "startDay" + * for "numDays". + * + * @param cr the ContentResolver + * @param startDay the first Julian day in the range + * @param numDays the number of days to load (must be at least 1) + * @return a database cursor + */ + public static final Cursor query(ContentResolver cr, int startDay, int numDays) { + if (numDays < 1) { + return null; + } + int endDay = startDay + numDays - 1; + Uri.Builder builder = CONTENT_URI.buildUpon(); + ContentUris.appendId(builder, startDay); + ContentUris.appendId(builder, endDay); + return cr.query(builder.build(), PROJECTION, null /* selection */, + null /* selection args */, DAY); + } + } + + public interface RemindersColumns { + /** + * The event the reminder belongs to + * <P>Type: INTEGER (foreign key to the Events table)</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The minutes prior to the event that the alarm should ring. -1 + * specifies that we should use the default value for the system. + * <P>Type: INTEGER</P> + */ + public static final String MINUTES = "minutes"; + + public static final int MINUTES_DEFAULT = -1; + + /** + * The alarm method, as set on the server. DEFAULT, ALERT, EMAIL, and + * SMS are possible values; the device will only process DEFAULT and + * ALERT reminders (the other types are simply stored so we can send the + * same reminder info back to the server when we make changes). + */ + public static final String METHOD = "method"; + + public static final int METHOD_DEFAULT = 0; + public static final int METHOD_ALERT = 1; + public static final int METHOD_EMAIL = 2; + public static final int METHOD_SMS = 3; + } + + public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns { + public static final String TABLE_NAME = "Reminders"; + public static final Uri CONTENT_URI = Uri.parse("content://calendar/reminders"); + } + + public interface CalendarAlertsColumns { + /** + * The event that the alert belongs to + * <P>Type: INTEGER (foreign key to the Events table)</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The start time of the event, in UTC + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String BEGIN = "begin"; + + /** + * The end time of the event, in UTC + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String END = "end"; + + /** + * The alarm time of the event, in UTC + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String ALARM_TIME = "alarmTime"; + + /** + * The state of this alert. It starts out as SCHEDULED, then when + * the alarm goes off, it changes to FIRED, and then when the user + * sees and dismisses the alarm it changes to DISMISSED. + * <P>Type: INTEGER</P> + */ + public static final String STATE = "state"; + + public static final int SCHEDULED = 0; + public static final int FIRED = 1; + public static final int DISMISSED = 2; + + /** + * The number of minutes that this alarm precedes the start time + * <P>Type: INTEGER </P> + */ + public static final String MINUTES = "minutes"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "alarmTime ASC,begin ASC,title ASC"; + } + + public static final class CalendarAlerts implements BaseColumns, + CalendarAlertsColumns, EventsColumns, CalendarsColumns { + public static final String TABLE_NAME = "CalendarAlerts"; + public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendar_alerts"); + + /** + * This URI is for grouping the query results by event_id and begin + * time. This will return one result per instance of an event. So + * events with multiple alarms will appear just once, but multiple + * instances of a repeating event will show up multiple times. + */ + public static final Uri CONTENT_URI_BY_INSTANCE = + Uri.parse("content://calendar/calendar_alerts/by_instance"); + + public static final Uri insert(ContentResolver cr, long eventId, + long begin, long end, long alarmTime, int minutes) { + ContentValues values = new ContentValues(); + values.put(CalendarAlerts.EVENT_ID, eventId); + values.put(CalendarAlerts.BEGIN, begin); + values.put(CalendarAlerts.END, end); + values.put(CalendarAlerts.ALARM_TIME, alarmTime); + values.put(CalendarAlerts.STATE, SCHEDULED); + values.put(CalendarAlerts.MINUTES, minutes); + return cr.insert(CONTENT_URI, values); + } + + public static final Cursor query(ContentResolver cr, String[] projection, + String selection, String[] selectionArgs) { + return cr.query(CONTENT_URI, projection, selection, selectionArgs, + DEFAULT_SORT_ORDER); + } + + /** + * Finds the next alarm after (or equal to) the given time and returns + * the time of that alarm or -1 if no such alarm exists. + * + * @param cr the ContentResolver + * @param millis the time in UTC milliseconds + * @return the next alarm time greater than or equal to "millis", or -1 + * if no such alarm exists. + */ + public static final long findNextAlarmTime(ContentResolver cr, long millis) { + String selection = ALARM_TIME + ">=" + millis; + // TODO: construct an explicit SQL query so that we can add + // "LIMIT 1" to the end and get just one result. + String[] projection = new String[] { ALARM_TIME }; + Cursor cursor = query(cr, projection, selection, null); + long alarmTime = -1; + try { + if (cursor != null && cursor.moveToFirst()) { + alarmTime = cursor.getLong(0); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return alarmTime; + } + + /** + * Searches the CalendarAlerts table for alarms that should have fired + * but have not and then reschedules them. This method can be called + * at boot time to restore alarms that may have been lost due to a + * phone reboot. + * + * @param cr the ContentResolver + * @param context the Context + * @param manager the AlarmManager + */ + public static final void rescheduleMissedAlarms(ContentResolver cr, + Context context, AlarmManager manager) { + // Get all the alerts that have been scheduled but have not fired + // and should have fired by now and are not too old. + long now = System.currentTimeMillis(); + long ancient = now - 24 * DateUtils.HOUR_IN_MILLIS; + String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED + + " AND " + CalendarAlerts.ALARM_TIME + "<" + now + + " AND " + CalendarAlerts.ALARM_TIME + ">" + ancient + + " AND " + CalendarAlerts.END + ">=" + now; + String[] projection = new String[] { + _ID, + BEGIN, + END, + ALARM_TIME, + }; + Cursor cursor = CalendarAlerts.query(cr, projection, selection, null); + if (cursor == null) { + return; + } + + try { + while (cursor.moveToNext()) { + long id = cursor.getLong(0); + long begin = cursor.getLong(1); + long end = cursor.getLong(2); + long alarmTime = cursor.getLong(3); + Uri uri = ContentUris.withAppendedId(CONTENT_URI, id); + Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION); + intent.setData(uri); + intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, begin); + intent.putExtra(android.provider.Calendar.EVENT_END_TIME, end); + PendingIntent sender = PendingIntent.getBroadcast(context, + 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + manager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender); + } + } finally { + cursor.close(); + } + + } + + /** + * Searches for an entry in the CalendarAlerts table that matches + * the given event id, begin time and alarm time. If one is found + * then this alarm already exists and this method returns true. + * + * @param cr the ContentResolver + * @param eventId the event id to match + * @param begin the start time of the event in UTC millis + * @param alarmTime the alarm time of the event in UTC millis + * @return true if there is already an alarm for the given event + * with the same start time and alarm time. + */ + public static final boolean alarmExists(ContentResolver cr, long eventId, + long begin, long alarmTime) { + String selection = CalendarAlerts.EVENT_ID + "=" + eventId + + " AND " + CalendarAlerts.BEGIN + "=" + begin + + " AND " + CalendarAlerts.ALARM_TIME + "=" + alarmTime; + // TODO: construct an explicit SQL query so that we can add + // "LIMIT 1" to the end and get just one result. + String[] projection = new String[] { CalendarAlerts.ALARM_TIME }; + Cursor cursor = query(cr, projection, selection, null); + boolean found = false; + try { + if (cursor != null && cursor.getCount() > 0) { + found = true; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return found; + } + } + + public interface ExtendedPropertiesColumns { + /** + * The event the extended property belongs to + * <P>Type: INTEGER (foreign key to the Events table)</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The name of the extended property. This is a uri of the form + * {scheme}#{local-name} convention. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The value of the extended property. + * <P>Type: TEXT</P> + */ + public static final String VALUE = "value"; + } + + public static final class ExtendedProperties implements BaseColumns, + ExtendedPropertiesColumns, EventsColumns { + public static final Uri CONTENT_URI = + Uri.parse("content://calendar/extendedproperties"); + + // TODO: fill out this class when we actually start utilizing extendedproperties + // in the calendar application. + } +} diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java new file mode 100644 index 0000000..10fe3f5 --- /dev/null +++ b/core/java/android/provider/CallLog.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.provider.Contacts.People; +import com.android.internal.telephony.CallerInfo; +import android.text.TextUtils; +import android.util.Log; + +/** + * The CallLog provider contains information about placed and received calls. + */ +public class CallLog { + public static final String AUTHORITY = "call_log"; + + /** + * The content:// style URL for this provider + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY); + + /** + * Contains the recent calls. + */ + public static class Calls implements BaseColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://call_log/calls"); + + /** + * The content:// style URL for filtering this table on phone numbers + */ + public static final Uri CONTENT_FILTER_URI = + Uri.parse("content://call_log/calls/filter"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * The MIME type of {@link #CONTENT_URI} and {@link #CONTENT_FILTER_URI} + * providing a directory of calls. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/calls"; + + /** + * The MIME type of a {@link #CONTENT_URI} sub-directory of a single + * call. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/calls"; + + /** + * The type of the the phone number. + * <P>Type: INTEGER (int)</P> + */ + public static final String TYPE = "type"; + + public static final int INCOMING_TYPE = 1; + public static final int OUTGOING_TYPE = 2; + public static final int MISSED_TYPE = 3; + + /** + * The phone number as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String NUMBER = "number"; + + /** + * The date the call occured, in milliseconds since the epoch + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The duration of the call in seconds + * <P>Type: INTEGER (long)</P> + */ + public static final String DURATION = "duration"; + + /** + * Whether or not the call has been acknowledged + * <P>Type: INTEGER (boolean)</P> + */ + public static final String NEW = "new"; + + /** + * The cached name associated with the phone number, if it exists. + * This value is not guaranteed to be current, if the contact information + * associated with this number has changed. + * <P>Type: TEXT</P> + */ + public static final String CACHED_NAME = "name"; + + /** + * The cached number type (Home, Work, etc) associated with the + * phone number, if it exists. + * This value is not guaranteed to be current, if the contact information + * associated with this number has changed. + * <P>Type: INTEGER</P> + */ + public static final String CACHED_NUMBER_TYPE = "numbertype"; + + /** + * The cached number label, for a custom number type, associated with the + * phone number, if it exists. + * This value is not guaranteed to be current, if the contact information + * associated with this number has changed. + * <P>Type: TEXT</P> + */ + public static final String CACHED_NUMBER_LABEL = "numberlabel"; + + /** + * Adds a call to the call log. + * + * @param ci the CallerInfo object to get the target contact from. Can be null + * if the contact is unknown. + * @param context the context used to get the ContentResolver + * @param number the phone number to be added to the calls db + * @param isPrivateNumber <code>true</code> if the call was marked as private by the network + * @param callType enumerated values for "incoming", "outgoing", or "missed" + * @param start time stamp for the call in milliseconds + * @param duration call duration in seconds + * + * {@hide} + */ + public static Uri addCall(CallerInfo ci, Context context, String number, + boolean isPrivateNumber, int callType, long start, int duration) { + final ContentResolver resolver = context.getContentResolver(); + + if (TextUtils.isEmpty(number)) { + if (isPrivateNumber) { + number = CallerInfo.PRIVATE_NUMBER; + } else { + number = CallerInfo.UNKNOWN_NUMBER; + } + } + + ContentValues values = new ContentValues(5); + + values.put(NUMBER, number); + values.put(TYPE, Integer.valueOf(callType)); + values.put(DATE, Long.valueOf(start)); + values.put(DURATION, Long.valueOf(duration)); + values.put(NEW, Integer.valueOf(1)); + if (ci != null) { + values.put(CACHED_NAME, ci.name); + values.put(CACHED_NUMBER_TYPE, ci.numberType); + values.put(CACHED_NUMBER_LABEL, ci.numberLabel); + } + + if ((ci != null) && (ci.person_id > 0)) { + People.markAsContacted(resolver, ci.person_id); + } + + Uri result = resolver.insert(CONTENT_URI, values); + + removeExpiredEntries(context); + + return result; + } + + private static void removeExpiredEntries(Context context) { + final ContentResolver resolver = context.getContentResolver(); + resolver.delete(CONTENT_URI, "_id IN " + + "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER + + " LIMIT -1 OFFSET 500)", null); + } + } +} diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java new file mode 100644 index 0000000..5b79482 --- /dev/null +++ b/core/java/android/provider/Checkin.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import org.apache.commons.codec.binary.Base64; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.SQLException; +import android.net.Uri; +import android.os.SystemClock; +import android.server.data.CrashData; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; + +/** + * Contract class for {@link android.server.checkin.CheckinProvider}. + * Describes the exposed database schema, and offers methods to add + * events and statistics to be uploaded. + * + * @hide + */ +public final class Checkin { + public static final String AUTHORITY = "android.server.checkin"; + + /** + * The events table is a log of important timestamped occurrences. + * Each event has a type tag and an optional string value. + * If too many events are added before they can be reported, the + * content provider will erase older events to limit the table size. + */ + public interface Events extends BaseColumns { + public static final String TABLE_NAME = "events"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); + + public static final String TAG = "tag"; // TEXT + public static final String VALUE = "value"; // TEXT + public static final String DATE = "date"; // INTEGER + + /** Valid tag values. Extend as necessary for your needs. */ + public enum Tag { + BROWSER_BUG_REPORT, + CARRIER_BUG_REPORT, + CHECKIN_FAILURE, + CHECKIN_SUCCESS, + FOTA_BEGIN, + FOTA_FAILURE, + FOTA_INSTALL, + FOTA_PROMPT, + FOTA_PROMPT_ACCEPT, + FOTA_PROMPT_REJECT, + FOTA_PROMPT_SKIPPED, + GSERVICES_ERROR, + GSERVICES_UPDATE, + LOGIN_SERVICE_ACCOUNT_TRIED, + LOGIN_SERVICE_ACCOUNT_SAVED, + LOGIN_SERVICE_AUTHENTICATE, + LOGIN_SERVICE_CAPTCHA_ANSWERED, + LOGIN_SERVICE_CAPTCHA_SHOWN, + LOGIN_SERVICE_PASSWORD_ENTERED, + LOGIN_SERVICE_SWITCH_GOOGLE_MAIL, + NETWORK_DOWN, + NETWORK_UP, + PHONE_UI, + RADIO_BUG_REPORT, + SETUP_COMPLETED, + SETUP_INITIATED, + SETUP_IO_ERROR, + SETUP_NETWORK_ERROR, + SETUP_REQUIRED_CAPTCHA, + SETUP_RETRIES_EXHAUSTED, + SETUP_SERVER_ERROR, + SETUP_SERVER_TIMEOUT, + SYSTEM_APP_NOT_RESPONDING, + SYSTEM_BOOT, + SYSTEM_LAST_KMSG, + SYSTEM_RECOVERY_LOG, + SYSTEM_RESTART, + SYSTEM_SERVICE_LOOPING, + SYSTEM_TOMBSTONE, + TEST, + NETWORK_RX_MOBILE, + NETWORK_TX_MOBILE, + } + } + + /** + * The stats table is a list of counter values indexed by a tag name. + * Each statistic has a count and sum fields, so it can track averages. + * When multiple statistics are inserted with the same tag, the count + * and sum fields are added together into a single entry in the database. + */ + public interface Stats extends BaseColumns { + public static final String TABLE_NAME = "stats"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); + + public static final String TAG = "tag"; // TEXT UNIQUE + public static final String COUNT = "count"; // INTEGER + public static final String SUM = "sum"; // REAL + + /** Valid tag values. Extend as necessary for your needs. */ + public enum Tag { + CRASHES_REPORTED, + CRASHES_TRUNCATED, + ELAPSED_REALTIME_SEC, + ELAPSED_UPTIME_SEC, + HTTP_STATUS, + PHONE_GSM_REGISTERED, + PHONE_GPRS_ATTEMPTED, + PHONE_GPRS_CONNECTED, + PHONE_RADIO_RESETS, + TEST, + NETWORK_RX_MOBILE, + NETWORK_TX_MOBILE, + } + } + + /** + * The properties table is a set of tagged values sent with every checkin. + * Unlike statistics or events, they are not cleared after being uploaded. + * Multiple properties inserted with the same tag overwrite each other. + */ + public interface Properties extends BaseColumns { + public static final String TABLE_NAME = "properties"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); + + public static final String TAG = "tag"; // TEXT UNIQUE + public static final String VALUE = "value"; // TEXT + + /** Valid tag values, to be extended as necessary. */ + public enum Tag { + DESIRED_BUILD, + MARKET_CHECKIN, + } + } + + /** + * The crashes table is a log of crash reports, kept separate from the + * general event log because crashes are large, important, and bursty. + * Like the events table, the crashes table is pruned on insert. + */ + public interface Crashes extends BaseColumns { + public static final String TABLE_NAME = "crashes"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); + + // TODO: one or both of these should be a file attachment, not a column + public static final String DATA = "data"; // TEXT + public static final String LOGS = "logs"; // TEXT + } + + /** + * Intents with this action cause a checkin attempt. Normally triggered by + * a periodic alarm, these may be sent directly to force immediate checkin. + */ + public interface TriggerIntent { + public static final String ACTION = "android.server.checkin.CHECKIN"; + + // The category is used for GTalk service messages + public static final String CATEGORY = "android.server.checkin.CHECKIN"; + } + + private static final String TAG = "Checkin"; + + /** + * Helper function to log an event to the database. + * + * @param resolver from {@link android.content.Context#getContentResolver} + * @param tag identifying the type of event being recorded + * @param value associated with event, if any + * @return URI of the event that was added + */ + static public Uri logEvent(ContentResolver resolver, + Events.Tag tag, String value) { + try { + // Don't specify the date column; the content provider will add that. + ContentValues values = new ContentValues(); + values.put(Events.TAG, tag.toString()); + if (value != null) values.put(Events.VALUE, value); + return resolver.insert(Events.CONTENT_URI, values); + } catch (SQLException e) { + Log.e(TAG, "Can't log event: " + tag, e); // Database errors are not fatal. + return null; + } + } + + /** + * Helper function to update statistics in the database. + * Note that multiple updates to the same tag will be combined. + * + * @param tag identifying what is being observed + * @param count of occurrences + * @param sum of some value over these occurrences + * @return URI of the statistic that was returned + */ + static public Uri updateStats(ContentResolver resolver, + Stats.Tag tag, int count, double sum) { + try { + ContentValues values = new ContentValues(); + values.put(Stats.TAG, tag.toString()); + if (count != 0) values.put(Stats.COUNT, count); + if (sum != 0.0) values.put(Stats.SUM, sum); + return resolver.insert(Stats.CONTENT_URI, values); + } catch (SQLException e) { + Log.e(TAG, "Can't update stat: " + tag, e); // Database errors are not fatal. + return null; + } + } + + /** Minimum time to wait after a crash failure before trying again. */ + static private final long MIN_CRASH_FAILURE_RETRY = 10000; // 10 seconds + + /** {@link SystemClock#elapsedRealtime} of the last time a crash report failed. */ + static private volatile long sLastCrashFailureRealtime = -MIN_CRASH_FAILURE_RETRY; + + /** + * Helper function to report a crash. + * + * @param resolver from {@link android.content.Context#getContentResolver} + * @param crash data from {@link android.server.data.CrashData} + * @return URI of the crash report that was added + */ + static public Uri reportCrash(ContentResolver resolver, byte[] crash) { + try { + // If we are in a situation where crash reports fail (such as a full disk), + // it's important that we don't get into a loop trying to report failures. + // So discard all crash reports for a few seconds after reporting fails. + long realtime = SystemClock.elapsedRealtime(); + if (realtime - sLastCrashFailureRealtime < MIN_CRASH_FAILURE_RETRY) { + Log.e(TAG, "Crash logging skipped, too soon after logging failure"); + return null; + } + + // HACK: we don't support BLOB values, so base64 encode it. + byte[] encoded = Base64.encodeBase64(crash); + ContentValues values = new ContentValues(); + values.put(Crashes.DATA, new String(encoded)); + Uri uri = resolver.insert(Crashes.CONTENT_URI, values); + if (uri == null) { + Log.e(TAG, "Error reporting crash"); + sLastCrashFailureRealtime = SystemClock.elapsedRealtime(); + } + return uri; + } catch (Throwable t) { + // To avoid an infinite crash-reporting loop, swallow all errors and exceptions. + Log.e(TAG, "Error reporting crash: " + t); + sLastCrashFailureRealtime = SystemClock.elapsedRealtime(); + return null; + } + } + + /** + * Report a crash in CrashData format. + * + * @param resolver from {@link android.content.Context#getContentResolver} + * @param crash data to report + * @return URI of the crash report that was added + */ + static public Uri reportCrash(ContentResolver resolver, CrashData crash) { + try { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + crash.write(new DataOutputStream(data)); + return reportCrash(resolver, data.toByteArray()); + } catch (Throwable t) { + // Swallow all errors and exceptions when writing crash report + Log.e(TAG, "Error writing crash: " + t); + return null; + } + } +} + diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java new file mode 100644 index 0000000..91b1853 --- /dev/null +++ b/core/java/android/provider/Contacts.java @@ -0,0 +1,1606 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import android.widget.ImageView; + +import com.android.internal.R; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * The Contacts provider stores all information about contacts. + */ +public class Contacts { + private static final String TAG = "Contacts"; + + public static final String AUTHORITY = "contacts"; + + /** + * The content:// style URL for this provider + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY); + + /** Signifies an email address row that is stored in the ContactMethods table */ + public static final int KIND_EMAIL = 1; + /** Signifies a postal address row that is stored in the ContactMethods table */ + public static final int KIND_POSTAL = 2; + /** Signifies an IM address row that is stored in the ContactMethods table */ + public static final int KIND_IM = 3; + /** Signifies an Organization row that is stored in the Organizations table */ + public static final int KIND_ORGANIZATION = 4; + /** Signifies an Phone row that is stored in the Phones table */ + public static final int KIND_PHONE = 5; + + /** + * no public constructor since this is a utility class + */ + private Contacts() {} + + /** + * Columns from the Settings table that other columns join into themselves. + */ + public interface SettingsColumns { + /** + * The _SYNC_ACCOUNT to which this setting corresponds. This may be null. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT = "_sync_account"; + + /** + * The key of this setting. + * <P>Type: TEXT</P> + */ + public static final String KEY = "key"; + + /** + * The value of this setting. + * <P>Type: TEXT</P> + */ + public static final String VALUE = "value"; + } + + /** + * The settings over all of the people + */ + public static final class Settings implements BaseColumns, SettingsColumns { + /** + * no public constructor since this is a utility class + */ + private Settings() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/settings"); + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "settings"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "key ASC"; + + /** + * A setting that is used to indicate if we should sync down all groups for the + * specified account. For this setting the _SYNC_ACCOUNT column must be set. + * If this isn't set then we will only sync the groups whose SHOULD_SYNC column + * is set to true. + * <p> + * This is a boolean setting. It is true if it is set and it is anything other than the + * emptry string or "0". + */ + public static final String SYNC_EVERYTHING = "syncEverything"; + + public static String getSetting(ContentResolver cr, String account, String key) { + // For now we only support a single account and the UI doesn't know what + // the account name is, so we're using a global setting for SYNC_EVERYTHING. + // Some day when we add multiple accounts to the UI this should honor the account + // that was asked for. + String selectString; + String[] selectArgs; + if (false) { + selectString = (account == null) + ? "_sync_account is null AND key=?" + : "_sync_account=? AND key=?"; + selectArgs = (account == null) + ? new String[]{key} + : new String[]{account, key}; + } else { + selectString = "key=?"; + selectArgs = new String[] {key}; + } + Cursor cursor = cr.query(Settings.CONTENT_URI, new String[]{VALUE}, + selectString, selectArgs, null); + try { + if (!cursor.moveToNext()) return null; + return cursor.getString(0); + } finally { + cursor.close(); + } + } + + public static void setSetting(ContentResolver cr, String account, String key, + String value) { + ContentValues values = new ContentValues(); + // For now we only support a single account and the UI doesn't know what + // the account name is, so we're using a global setting for SYNC_EVERYTHING. + // Some day when we add multiple accounts to the UI this should honor the account + // that was asked for. + //values.put(_SYNC_ACCOUNT, account); + values.put(KEY, key); + values.put(VALUE, value); + cr.update(Settings.CONTENT_URI, values, null, null); + } + } + + /** + * Columns from the People table that other tables join into themselves. + */ + public interface PeopleColumns { + /** + * The persons name. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The display name. If name is not null name, else if number is not null number, + * else if email is not null email. + * <P>Type: TEXT</P> + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * Notes about the person. + * <P>Type: TEXT</P> + */ + public static final String NOTES = "notes"; + + /** + * The number of times a person has been contacted + * <P>Type: INTEGER</P> + */ + public static final String TIMES_CONTACTED = "times_contacted"; + + /** + * The last time a person was contacted. + * <P>Type: INTEGER</P> + */ + public static final String LAST_TIME_CONTACTED = "last_time_contacted"; + + /** + * A custom ringtone associated with a person. Not always present. + * <P>Type: TEXT (URI to the ringtone)</P> + */ + public static final String CUSTOM_RINGTONE = "custom_ringtone"; + + /** + * Whether the person should always be sent to voicemail. Not always + * present. + * <P>Type: INTEGER (0 for false, 1 for true)</P> + */ + public static final String SEND_TO_VOICEMAIL = "send_to_voicemail"; + + /** + * Is the contact starred? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String STARRED = "starred"; + + /** + * The server version of the photo + * <P>Type: TEXT (the version number portion of the photo URI)</P> + */ + public static final String PHOTO_VERSION = "photo_version"; + } + + /** + * This table contains people. + */ + public static final class People implements BaseColumns, SyncConstValue, PeopleColumns, + PhonesColumns, PresenceColumns { + /** + * no public constructor since this is a utility class + */ + private People() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/people"); + + /** + * The content:// style URL for filtering people by name. The filter + * argument should be passed as an additional path segment after this URI. + */ + public static final Uri CONTENT_FILTER_URI = + Uri.parse("content://contacts/people/filter"); + + /** + * The content:// style URL for the table that holds the deleted + * contacts. + */ + public static final Uri DELETED_CONTENT_URI = + Uri.parse("content://contacts/deleted_people"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = People.NAME + " ASC"; + + /** + * The ID of the persons preferred phone number. + * <P>Type: INTEGER (foreign key to phones table on the _ID field)</P> + */ + public static final String PRIMARY_PHONE_ID = "primary_phone"; + + /** + * The ID of the persons preferred email. + * <P>Type: INTEGER (foreign key to contact_methods table on the + * _ID field)</P> + */ + public static final String PRIMARY_EMAIL_ID = "primary_email"; + + /** + * The ID of the persons preferred organization. + * <P>Type: INTEGER (foreign key to organizations table on the + * _ID field)</P> + */ + public static final String PRIMARY_ORGANIZATION_ID = "primary_organization"; + + /** + * Mark a person as having been contacted. + * + * @param resolver the ContentResolver to use + * @param personId the person who was contacted + */ + public static void markAsContacted(ContentResolver resolver, long personId) { + Uri uri = ContentUris.withAppendedId(CONTENT_URI, personId); + uri = Uri.withAppendedPath(uri, "update_contact_time"); + ContentValues values = new ContentValues(); + // There is a trigger in place that will update TIMES_CONTACTED when + // LAST_TIME_CONTACTED is modified. + values.put(LAST_TIME_CONTACTED, System.currentTimeMillis()); + resolver.update(uri, values, null, null); + } + + /** + * Adds a person to the My Contacts group. + * + * @param resolver the resolver to use + * @param personId the person to add to the group + * @return the URI of the group membership row + * @throws IllegalStateException if the My Contacts group can't be found + */ + public static Uri addToMyContactsGroup(ContentResolver resolver, long personId) { + long groupId = 0; + Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION, + Groups.SYSTEM_ID + "='" + Groups.GROUP_MY_CONTACTS + "'", null, null); + if (groupsCursor != null) { + try { + if (groupsCursor.moveToFirst()) { + groupId = groupsCursor.getLong(0); + } + } finally { + groupsCursor.close(); + } + } + + if (groupId == 0) { + throw new IllegalStateException("Failed to find the My Contacts group"); + } + + return addToGroup(resolver, personId, groupId); + } + + /** + * Adds a person to a group referred to by name. + * + * @param resolver the resolver to use + * @param personId the person to add to the group + * @param groupName the name of the group to add the contact to + * @return the URI of the group membership row + * @throws IllegalStateException if the group can't be found + */ + public static Uri addToGroup(ContentResolver resolver, long personId, String groupName) { + long groupId = 0; + Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION, + Groups.NAME + "=?", new String[] { groupName }, null); + if (groupsCursor != null) { + try { + if (groupsCursor.moveToFirst()) { + groupId = groupsCursor.getLong(0); + } + } finally { + groupsCursor.close(); + } + } + + if (groupId == 0) { + throw new IllegalStateException("Failed to find the My Contacts group"); + } + + return addToGroup(resolver, personId, groupId); + } + + /** + * Adds a person to a group. + * + * @param resolver the resolver to use + * @param personId the person to add to the group + * @param groupId the group to add the person to + * @return the URI of the group membership row + */ + public static Uri addToGroup(ContentResolver resolver, long personId, long groupId) { + ContentValues values = new ContentValues(); + values.put(GroupMembership.PERSON_ID, personId); + values.put(GroupMembership.GROUP_ID, groupId); + return resolver.insert(GroupMembership.CONTENT_URI, values); + } + + private static final String[] GROUPS_PROJECTION = new String[] { + Groups._ID, + }; + + /** + * Creates a new contacts and adds it to the "My Contacts" group. + * + * @param resolver the ContentResolver to use + * @param values the values to use when creating the contact + * @return the URI of the contact, or null if the operation fails + */ + public static Uri createPersonInMyContactsGroup(ContentResolver resolver, + ContentValues values) { + + Uri contactUri = resolver.insert(People.CONTENT_URI, values); + if (contactUri == null) { + Log.e(TAG, "Failed to create the contact"); + return null; + } + + if (addToMyContactsGroup(resolver, ContentUris.parseId(contactUri)) == null) { + resolver.delete(contactUri, null, null); + return null; + } + return contactUri; + } + + public static Cursor queryGroups(ContentResolver resolver, long person) { + return resolver.query(GroupMembership.CONTENT_URI, null, "person=?", + new String[]{String.valueOf(person)}, Groups.DEFAULT_SORT_ORDER); + } + + /** + * Set the photo for this person. data may be null + * @param cr the ContentResolver to use + * @param person the Uri of the person whose photo is to be updated + * @param data the byte[] that represents the photo + */ + public static void setPhotoData(ContentResolver cr, Uri person, byte[] data) { + Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY); + ContentValues values = new ContentValues(); + values.put(Photos.DATA, data); + cr.update(photoUri, values, null, null); + } + + /** + * Opens an InputStream for the person's photo and returns the photo as a Bitmap. + * If the person's photo isn't present returns the placeholderImageResource instead. + * @param person the person whose photo should be used + */ + public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri person) { + Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY); + Cursor cursor = cr.query(photoUri, new String[]{Photos.DATA}, null, null, null); + try { + if (!cursor.moveToNext()) { + return null; + } + byte[] data = cursor.getBlob(0); + if (data == null) { + return null; + } + return new ByteArrayInputStream(data); + } finally { + cursor.close(); + } + } + + /** + * Opens an InputStream for the person's photo and returns the photo as a Bitmap. + * If the person's photo isn't present returns the placeholderImageResource instead. + * @param context the Context + * @param person the person whose photo should be used + * @param placeholderImageResource the image resource to use if the person doesn't + * have a photo + * @param options the decoding options, can be set to null + */ + public static Bitmap loadContactPhoto(Context context, Uri person, + int placeholderImageResource, BitmapFactory.Options options) { + if (person == null) { + return loadPlaceholderPhoto(placeholderImageResource, context, options); + } + + InputStream stream = openContactPhotoInputStream(context.getContentResolver(), person); + Bitmap bm = stream != null ? BitmapFactory.decodeStream(stream, null, options) : null; + if (bm == null) { + bm = loadPlaceholderPhoto(placeholderImageResource, context, options); + } + return bm; + } + + private static Bitmap loadPlaceholderPhoto(int placeholderImageResource, Context context, + BitmapFactory.Options options) { + if (placeholderImageResource == 0) { + return null; + } + return BitmapFactory.decodeResource(context.getResources(), + placeholderImageResource, options); + } + + /** + * A sub directory of a single person that contains all of their Phones. + */ + public static final class Phones implements BaseColumns, PhonesColumns, + PeopleColumns { + /** + * no public constructor since this is a utility class + */ + private Phones() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "phones"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "number ASC"; + } + + /** + * A subdirectory of a single person that contains all of their + * ContactMethods. + */ + public static final class ContactMethods + implements BaseColumns, ContactMethodsColumns, PeopleColumns { + /** + * no public constructor since this is a utility class + */ + private ContactMethods() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "contact_methods"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "data ASC"; + } + + /** + * The extensions for a person + */ + public static class Extensions implements BaseColumns, ExtensionsColumns { + /** + * no public constructor since this is a utility class + */ + private Extensions() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "extensions"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + /** + * The ID of the person this phone number is assigned to. + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + } + } + + /** + * Columns from the groups table. + */ + public interface GroupsColumns { + /** + * The group name. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * Notes about the group. + * <P>Type: TEXT</P> + */ + public static final String NOTES = "notes"; + + /** + * Whether this group should be synced if the SYNC_EVERYTHING settings is false + * for this group's account. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String SHOULD_SYNC = "should_sync"; + + /** + * The ID of this group if it is a System Group, null otherwise. + * <P>Type: TEXT</P> + */ + public static final String SYSTEM_ID = "system_id"; + } + + /** + * This table contains the groups for an account. + */ + public static final class Groups + implements BaseColumns, SyncConstValue, GroupsColumns { + /** + * no public constructor since this is a utility class + */ + private Groups() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/groups"); + + /** + * The content:// style URL for the table that holds the deleted + * groups. + */ + public static final Uri DELETED_CONTENT_URI = + Uri.parse("content://contacts/deleted_groups"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * groups. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroup"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * group. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contactsgroup"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = NAME + " ASC"; + + /** + * + */ + public static final String GROUP_ANDROID_STARRED = "Starred in Android"; + + /** + * The "My Contacts" system group. + */ + public static final String GROUP_MY_CONTACTS = "Contacts"; + } + + /** + * Columns from the Phones table that other columns join into themselves. + */ + public interface PhonesColumns { + /** + * The type of the the phone number. + * <P>Type: INTEGER (one of the constants below)</P> + */ + public static final String TYPE = "type"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_MOBILE = 2; + public static final int TYPE_WORK = 3; + public static final int TYPE_FAX_WORK = 4; + public static final int TYPE_FAX_HOME = 5; + public static final int TYPE_PAGER = 6; + public static final int TYPE_OTHER = 7; + + /** + * The user provided label for the phone number, only used if TYPE is TYPE_CUSTOM. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "label"; + + /** + * The phone number as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String NUMBER = "number"; + + /** + * The normalized phone number + * <P>Type: TEXT</P> + */ + public static final String NUMBER_KEY = "number_key"; + + /** + * Whether this is the primary phone number + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String ISPRIMARY = "isprimary"; + } + + /** + * This table stores phone numbers and a reference to the person that the + * contact method belongs to. Phone numbers are stored separately from + * other contact methods to make caller ID lookup more efficient. + */ + public static final class Phones + implements BaseColumns, PhonesColumns, PeopleColumns { + /** + * no public constructor since this is a utility class + */ + private Phones() {} + + public static final CharSequence getDisplayLabel(Context context, int type, + CharSequence label, CharSequence[] labelArray) { + CharSequence display = ""; + + if (type != People.Phones.TYPE_CUSTOM) { + CharSequence[] labels = labelArray != null? labelArray + : context.getResources().getTextArray( + com.android.internal.R.array.phoneTypes); + try { + display = labels[type - 1]; + } catch (ArrayIndexOutOfBoundsException e) { + display = labels[People.Phones.TYPE_HOME - 1]; + } + } else { + if (!TextUtils.isEmpty(label)) { + display = label; + } + } + return display; + } + + public static final CharSequence getDisplayLabel(Context context, int type, + CharSequence label) { + return getDisplayLabel(context, type, label, null); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/phones"); + + /** + * The content:// style URL for filtering phone numbers + */ + public static final Uri CONTENT_FILTER_URL = + Uri.parse("content://contacts/phones/filter"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * phones. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * phone. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + /** + * The ID of the person this phone number is assigned to. + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + } + + public static final class GroupMembership implements BaseColumns, GroupsColumns { + /** + * no public constructor since this is a utility class + */ + private GroupMembership() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/groupmembership"); + + /** + * The content:// style URL for this table + */ + public static final Uri RAW_CONTENT_URI = + Uri.parse("content://contacts/groupmembershipraw"); + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "groupmembership"; + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of all + * person groups. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroupmembership"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person group. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/contactsgroupmembership"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "group_id ASC"; + + /** + * The row id of the accounts group. + * <P>Type: TEXT</P> + */ + public static final String GROUP_ID = "group_id"; + + /** + * The sync id of the group. + * <P>Type: TEXT</P> + */ + public static final String GROUP_SYNC_ID = "group_sync_id"; + + /** + * The account of the group. + * <P>Type: TEXT</P> + */ + public static final String GROUP_SYNC_ACCOUNT = "group_sync_account"; + + /** + * The row id of the person. + * <P>Type: TEXT</P> + */ + public static final String PERSON_ID = "person"; + } + + /** + * Columns from the ContactMethods table that other tables join into + * themseleves. + */ + public interface ContactMethodsColumns { + /** + * The kind of the the contact method. For example, email address, + * postal address, etc. + * <P>Type: INTEGER (one of the values below)</P> + */ + public static final String KIND = "kind"; + + /** + * The type of the contact method, must be one of the types below. + * <P>Type: INTEGER (one of the values below)</P> + */ + public static final String TYPE = "type"; + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + + /** + * The user defined label for the the contact method. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "label"; + + /** + * The data for the contact method. + * <P>Type: TEXT</P> + */ + public static final String DATA = "data"; + + /** + * Auxiliary data for the contact method. + * <P>Type: TEXT</P> + */ + public static final String AUX_DATA = "aux_data"; + + /** + * Whether this is the primary organization + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String ISPRIMARY = "isprimary"; + } + + /** + * This table stores all non-phone contact methods and a reference to the + * person that the contact method belongs to. + */ + public static final class ContactMethods + implements BaseColumns, ContactMethodsColumns, PeopleColumns { + /** + * The column with latitude data for postal locations + * <P>Type: REAL</P> + */ + public static final String POSTAL_LOCATION_LATITUDE = DATA; + + /** + * The column with longitude data for postal locations + * <P>Type: REAL</P> + */ + public static final String POSTAL_LOCATION_LONGITUDE = AUX_DATA; + + /** + * The predefined IM protocol types. The protocol can either be non-present, one + * of these types, or a free-form string. These cases are encoded in the AUX_DATA + * column as: + * - null + * - pre:<an integer, one of the protocols below> + * - custom:<a string> + */ + public static final int PROTOCOL_AIM = 0; + public static final int PROTOCOL_MSN = 1; + public static final int PROTOCOL_YAHOO = 2; + public static final int PROTOCOL_SKYPE = 3; + public static final int PROTOCOL_QQ = 4; + public static final int PROTOCOL_GOOGLE_TALK = 5; + public static final int PROTOCOL_ICQ = 6; + public static final int PROTOCOL_JABBER = 7; + + public static String encodePredefinedImProtocol(int protocol) { + return "pre:" + protocol; + } + + public static String encodeCustomImProtocol(String protocolString) { + return "custom:" + protocolString; + } + + public static Object decodeImProtocol(String encodedString) { + if (encodedString == null) { + return null; + } + + if (encodedString.startsWith("pre:")) { + return Integer.parseInt(encodedString.substring(4)); + } + + if (encodedString.startsWith("custom:")) { + return encodedString.substring(7); + } + + throw new IllegalArgumentException( + "the value is not a valid encoded protocol, " + encodedString); + } + + /** + * This looks up the provider category defined in + * {@link android.provider.Im.ProviderCategories} from the predefined IM protocol id. + * This is used for interacting with the IM application. + * + * @param protocol the protocol ID + * @return the provider category the IM app uses for the given protocol, or null if no + * provider is defined for the given protocol + * @hide + */ + public static String lookupProviderCategoryFromId(int protocol) { + switch (protocol) { + case PROTOCOL_GOOGLE_TALK: + return Im.ProviderCategories.GTALK; + case PROTOCOL_AIM: + return Im.ProviderCategories.AIM; + case PROTOCOL_MSN: + return Im.ProviderCategories.MSN; + case PROTOCOL_YAHOO: + return Im.ProviderCategories.YAHOO; + case PROTOCOL_ICQ: + return Im.ProviderCategories.ICQ; + } + return null; + } + + /** + * no public constructor since this is a utility class + */ + private ContactMethods() {} + + public static final CharSequence getDisplayLabel(Context context, int kind, + int type, CharSequence label) { + CharSequence display = ""; + switch (kind) { + case KIND_EMAIL: { + if (type != People.ContactMethods.TYPE_CUSTOM) { + CharSequence[] labels = context.getResources().getTextArray( + com.android.internal.R.array.emailAddressTypes); + try { + display = labels[type - 1]; + } catch (ArrayIndexOutOfBoundsException e) { + display = labels[ContactMethods.TYPE_HOME - 1]; + } + } else { + if (!TextUtils.isEmpty(label)) { + display = label; + } + } + break; + } + + case KIND_POSTAL: { + if (type != People.ContactMethods.TYPE_CUSTOM) { + CharSequence[] labels = context.getResources().getTextArray( + com.android.internal.R.array.postalAddressTypes); + try { + display = labels[type - 1]; + } catch (ArrayIndexOutOfBoundsException e) { + display = labels[ContactMethods.TYPE_HOME - 1]; + } + } else { + if (!TextUtils.isEmpty(label)) { + display = label; + } + } + break; + } + + default: + display = context.getString(R.string.untitled); + } + return display; + } + + /** + * Add a longitude and latitude location to a postal address. + * + * @param context the context to use when updating the database + * @param postalId the address to update + * @param latitude the latitude for the address + * @param longitude the longitude for the address + */ + public void addPostalLocation(Context context, long postalId, + double latitude, double longitude) { + final ContentResolver resolver = context.getContentResolver(); + // Insert the location + ContentValues values = new ContentValues(2); + values.put(POSTAL_LOCATION_LATITUDE, latitude); + values.put(POSTAL_LOCATION_LONGITUDE, longitude); + Uri loc = resolver.insert(CONTENT_URI, values); + long locId = ContentUris.parseId(loc); + + // Update the postal address + values.clear(); + values.put(AUX_DATA, locId); + resolver.update(ContentUris.withAppendedId(CONTENT_URI, postalId), values, null, null); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/contact_methods"); + + /** + * The content:// style URL for sub-directory of e-mail addresses. + */ + public static final Uri CONTENT_EMAIL_URI = + Uri.parse("content://contacts/contact_methods/email"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * phones. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact-methods"; + + /** + * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\ + * multiple {@link Contacts#KIND_EMAIL} entries. + */ + public static final String CONTENT_EMAIL_TYPE = "vnd.android.cursor.dir/email"; + + /** + * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\ + * multiple {@link Contacts#KIND_POSTAL} entries. + */ + public static final String CONTENT_POSTAL_TYPE = "vnd.android.cursor.dir/postal-address"; + + /** + * The MIME type of a {@link #CONTENT_URI} sub-directory of a single + * {@link Contacts#KIND_EMAIL} entry. + */ + public static final String CONTENT_EMAIL_ITEM_TYPE = "vnd.android.cursor.item/email"; + + /** + * The MIME type of a {@link #CONTENT_URI} sub-directory of a single + * {@link Contacts#KIND_POSTAL} entry. + */ + public static final String CONTENT_POSTAL_ITEM_TYPE + = "vnd.android.cursor.item/postal-address"; + + /** + * The MIME type of a {@link #CONTENT_URI} sub-directory of a single + * {@link Contacts#KIND_IM} entry. + */ + public static final String CONTENT_IM_ITEM_TYPE = "vnd.android.cursor.item/jabber-im"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + /** + * The ID of the person this contact method is assigned to. + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + } + + /** + * The IM presence columns with some contacts specific columns mixed in. + */ + public interface PresenceColumns extends Im.CommonPresenceColumns { + /** + * The IM service the presence is coming from. Formatted using either + * {@link Contacts.ContactMethods#encodePredefinedImProtocol} or + * {@link Contacts.ContactMethods#encodeCustomImProtocol}. + * <P>Type: STRING</P> + */ + public static final String IM_PROTOCOL = "im_protocol"; + + /** + * The IM handle the presence item is for. The handle is scoped to + * the {@link #IM_PROTOCOL}. + * <P>Type: STRING</P> + */ + public static final String IM_HANDLE = "im_handle"; + + /** + * The IM account for the local user that the presence data came from. + * <P>Type: STRING</P> + */ + public static final String IM_ACCOUNT = "im_account"; + } + + /** + * Contains presence information about contacts. + * @hide + */ + public static final class Presence + implements BaseColumns, PresenceColumns, PeopleColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/presence"); + + /** + * The ID of the person this presence item is assigned to. + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + + /** + * Gets the resource ID for the proper presence icon. + * + * @param status the status to get the icon for + * @return the resource ID for the proper presence icon + */ + public static final int getPresenceIconResourceId(int status) { + switch (status) { + case Contacts.People.AVAILABLE: + return com.android.internal.R.drawable.presence_online; + + case Contacts.People.IDLE: + case Contacts.People.AWAY: + return com.android.internal.R.drawable.presence_away; + + case Contacts.People.DO_NOT_DISTURB: + return com.android.internal.R.drawable.presence_busy; + + case Contacts.People.INVISIBLE: + return com.android.internal.R.drawable.presence_invisible; + + case Contacts.People.OFFLINE: + default: + return com.android.internal.R.drawable.presence_offline; + } + } + + /** + * Sets a presence icon to the proper graphic + * + * @param icon the icon to to set + * @param serverStatus that status + */ + public static final void setPresenceIcon(ImageView icon, int serverStatus) { + icon.setImageResource(getPresenceIconResourceId(serverStatus)); + } + } + + /** + * Columns from the Organizations table that other columns join into themselves. + */ + public interface OrganizationColumns { + /** + * The type of the the phone number. + * <P>Type: INTEGER (one of the constants below)</P> + */ + public static final String TYPE = "type"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_WORK = 1; + public static final int TYPE_OTHER = 2; + + /** + * The user provided label, only used if TYPE is TYPE_CUSTOM. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "label"; + + /** + * The name of the company for this organization. + * <P>Type: TEXT</P> + */ + public static final String COMPANY = "company"; + + /** + * The title within this organization. + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * The person this organization is tied to. + * <P>Type: TEXT</P> + */ + public static final String PERSON_ID = "person"; + + /** + * Whether this is the primary organization + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String ISPRIMARY = "isprimary"; + } + + /** + * A sub directory of a single person that contains all of their Phones. + */ + public static final class Organizations implements BaseColumns, OrganizationColumns { + /** + * no public constructor since this is a utility class + */ + private Organizations() {} + + public static final CharSequence getDisplayLabel(Context context, int type, + CharSequence label) { + CharSequence display = ""; + + if (type != TYPE_CUSTOM) { + CharSequence[] labels = context.getResources().getTextArray( + com.android.internal.R.array.organizationTypes); + try { + display = labels[type - 1]; + } catch (ArrayIndexOutOfBoundsException e) { + display = labels[People.Phones.TYPE_HOME - 1]; + } + } else { + if (!TextUtils.isEmpty(label)) { + display = label; + } + } + return display; + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/organizations"); + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "organizations"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "company, title, isprimary ASC"; + } + + /** + * Columns from the Photos table that other columns join into themselves. + */ + public interface PhotosColumns { + /** + * The _SYNC_VERSION of the photo that was last downloaded + * <P>Type: TEXT</P> + */ + public static final String LOCAL_VERSION = "local_version"; + + /** + * The person this photo is associated with. + * <P>Type: TEXT</P> + */ + public static final String PERSON_ID = "person"; + + /** + * non-zero if a download is required and the photo isn't marked as a bad resource. + * You must specify this in the columns in order to use it in the where clause. + * <P>Type: INTEGER(boolean)</P> + */ + public static final String DOWNLOAD_REQUIRED = "download_required"; + + /** + * non-zero if this photo is known to exist on the server + * <P>Type: INTEGER(boolean)</P> + */ + public static final String EXISTS_ON_SERVER = "exists_on_server"; + + /** + * Contains the description of the upload or download error from + * the previous attempt. If null then the previous attempt succeeded. + * <P>Type: TEXT</P> + */ + public static final String SYNC_ERROR = "sync_error"; + + /** + * The image data, or null if there is no image. + * <P>Type: BLOB</P> + */ + public static final String DATA = "data"; + + } + + /** + * The photos over all of the people + */ + public static final class Photos implements BaseColumns, PhotosColumns, SyncConstValue { + /** + * no public constructor since this is a utility class + */ + private Photos() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/photos"); + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "photo"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "person ASC"; + } + + public interface ExtensionsColumns { + /** + * The name of this extension. May not be null. There may be at most one row for each name. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The value of this extension. May not be null. + * <P>Type: TEXT</P> + */ + public static final String VALUE = "value"; + } + + /** + * The extensions for a person + */ + public static final class Extensions implements BaseColumns, ExtensionsColumns { + /** + * no public constructor since this is a utility class + */ + private Extensions() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://contacts/extensions"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * phones. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_extensions"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * phone. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_extensions"; + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "person, name ASC"; + + /** + * The ID of the person this phone number is assigned to. + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + } + + /** + * Contains helper classes used to create or manage {@link android.content.Intent Intents} + * that involve contacts. + */ + public static final class Intents { + /** + * This is the intent that is fired when a search suggestion is clicked on. + */ + public static final String SEARCH_SUGGESTION_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_CLICKED"; + + /** + * This is the intent that is fired when a search suggestion for dialing a number + * is clicked on. + */ + public static final String SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED"; + + /** + * This is the intent that is fired when a search suggestion for creating a contact + * is clicked on. + */ + public static final String SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED"; + + /** + * Starts an Activity that lets the user pick a contact to attach an image to. + * After picking the contact it launches the image cropper in face detection mode. + */ + public static final String ATTACH_IMAGE = + "com.android.contacts.action.ATTACH_IMAGE"; + + /** + * Intents related to the Contacts app UI. + */ + public static final class UI { + /** + * The action for the default contacts list tab. + */ + public static final String LIST_DEFAULT = + "com.android.contacts.action.LIST_DEFAULT"; + + /** + * The action for the contacts list tab. + */ + public static final String LIST_GROUP_ACTION = + "com.android.contacts.action.LIST_GROUP"; + + /** + * When in LIST_GROUP_ACTION mode, this is the group to display. + */ + public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP"; + + /** + * The action for the all contacts list tab. + */ + public static final String LIST_ALL_CONTACTS_ACTION = + "com.android.contacts.action.LIST_ALL_CONTACTS"; + + /** + * The action for the contacts with phone numbers list tab. + */ + public static final String LIST_CONTACTS_WITH_PHONES_ACTION = + "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES"; + + /** + * The action for the starred contacts list tab. + */ + public static final String LIST_STARRED_ACTION = + "com.android.contacts.action.LIST_STARRED"; + + /** + * The action for the frequent contacts list tab. + */ + public static final String LIST_FREQUENT_ACTION = + "com.android.contacts.action.LIST_FREQUENT"; + + /** + * The action for the "strequent" contacts list tab. It first lists the starred + * contacts in alphabetical order and then the frequent contacts in descending + * order of the number of times they have been contacted. + */ + public static final String LIST_STREQUENT_ACTION = + "com.android.contacts.action.LIST_STREQUENT"; + + /** + * A key for to be used as an intent extra to set the activity + * title to a custom String value. + */ + public static final String TITLE_EXTRA_KEY = + "com.android.contacts.extra.TITLE_EXTRA"; + + /** + * Activity Action: Display a filtered list of contacts + * <p> + * Input: Extra field {@link #FILTER_TEXT_EXTRA_KEY} is the text to use for + * filtering + * <p> + * Output: Nothing. + */ + public static final String FILTER_CONTACTS_ACTION = + "com.android.contacts.action.FILTER_CONTACTS"; + + /** + * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION} + * intents to supply the text on which to filter. + */ + public static final String FILTER_TEXT_EXTRA_KEY = + "com.android.contacts.extra.FILTER_TEXT"; + } + + /** + * Convenience class that contains string constants used + * to create contact {@link android.content.Intent Intents}. + */ + public static final class Insert { + /** The action code to use when adding a contact */ + public static final String ACTION = Intent.ACTION_INSERT; + + /** + * If present, forces a bypass of quick insert mode. + */ + public static final String FULL_MODE = "full_mode"; + + /** + * The extra field for the contact name. + * <P>Type: String</P> + */ + public static final String NAME = "name"; + + /** + * The extra field for the contact company. + * <P>Type: String</P> + */ + public static final String COMPANY = "company"; + + /** + * The extra field for the contact job title. + * <P>Type: String</P> + */ + public static final String JOB_TITLE = "job_title"; + + /** + * The extra field for the contact notes. + * <P>Type: String</P> + */ + public static final String NOTES = "notes"; + + /** + * The extra field for the contact phone number. + * <P>Type: String</P> + */ + public static final String PHONE = "phone"; + + /** + * The extra field for the contact phone number type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a type and label.</P> + */ + public static final String PHONE_TYPE = "phone_type"; + + /** + * The extra field for the phone isprimary flag. + * <P>Type: boolean</P> + */ + public static final String PHONE_ISPRIMARY = "phone_isprimary"; + + /** + * The extra field for the contact email address. + * <P>Type: String</P> + */ + public static final String EMAIL = "email"; + + /** + * The extra field for the contact email type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a type and label.</P> + */ + public static final String EMAIL_TYPE = "email_type"; + + /** + * The extra field for the email isprimary flag. + * <P>Type: boolean</P> + */ + public static final String EMAIL_ISPRIMARY = "email_isprimary"; + + /** + * The extra field for the contact postal address. + * <P>Type: String</P> + */ + public static final String POSTAL = "postal"; + + /** + * The extra field for the contact postal address type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a type and label.</P> + */ + public static final String POSTAL_TYPE = "postal_type"; + + /** + * The extra field for the postal isprimary flag. + * <P>Type: boolean</P> + */ + public static final String POSTAL_ISPRIMARY = "postal_isprimary"; + + /** + * The extra field for an IM handle. + * <P>Type: String</P> + */ + public static final String IM_HANDLE = "im_handle"; + + /** + * The extra field for the IM protocol + * <P>Type: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} + * or {@link Contacts.ContactMethods#encodeCustomImProtocol}.</P> + */ + public static final String IM_PROTOCOL = "im_protocol"; + + /** + * The extra field for the IM isprimary flag. + * <P>Type: boolean</P> + */ + public static final String IM_ISPRIMARY = "im_isprimary"; + } + } +} diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java new file mode 100644 index 0000000..42e9d95 --- /dev/null +++ b/core/java/android/provider/Downloads.java @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2008 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 android.provider; + +import android.net.Uri; + +/** + * Exposes constants used to interact with the download manager's + * content provider. + * The constants URI ... STATUS are the names of columns in the downloads table. + * + * @hide + */ +// For 1.0 the download manager can't deal with abuse from untrusted apps, so +// this API is hidden. +public final class Downloads implements BaseColumns { + private Downloads() {} + /** + * The content:// URI for the data table in the provider + */ + public static final Uri CONTENT_URI = + Uri.parse("content://downloads/download"); + + /** + * Broadcast Action: this is sent by the download manager to the app + * that had initiated a download when that download completes. The + * download's content: uri is specified in the intent's data. + */ + public static final String DOWNLOAD_COMPLETED_ACTION = + "android.intent.action.DOWNLOAD_COMPLETED"; + + /** + * Broadcast Action: this is sent by the download manager to the app + * that had initiated a download when the user selects the notification + * associated with that download. The download's content: uri is specified + * in the intent's data if the click is associated with a single download, + * or Downloads.CONTENT_URI if the notification is associated with + * multiple downloads. + * Note: this is not currently sent for downloads that have completed + * successfully. + */ + public static final String NOTIFICATION_CLICKED_ACTION = + "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"; + + /** + * The name of the column containing the URI of the data being downloaded. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read</P> + */ + public static final String URI = "uri"; + + /** + * The name of the column containing the HTTP method to use for this + * download. See the METHOD_* constants for a list of legal values. + * <P>Type: INTEGER</P> + * <P>Owner can Init/Read</P> + */ + public static final String METHOD = "method"; + + /** + * The name of the column containing the entity to be sent with the + * request of this download. Only use for methods that support sending + * entities, i.e. POST. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> + */ + public static final String ENTITY = "entity"; + + /** + * The name of the column containing the flags that indicates whether + * the initiating application is capable of verifying the integrity of + * the downloaded file. When this flag is set, the download manager + * performs downloads and reports success even in some situations where + * it can't guarantee that the download has completed (e.g. when doing + * a byte-range request without an ETag, or when it can't determine + * whether a download fully completed). + * <P>Type: BOOLEAN</P> + * <P>Owner can Init/Read</P> + */ + public static final String NO_INTEGRITY = "no_integrity"; + + /** + * The name of the column containing the filename that the initiating + * application recommends. When possible, the download manager will attempt + * to use this filename, or a variation, as the actual name for the file. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read</P> + */ + public static final String FILENAME_HINT = "hint"; + + /** + * The name of the column containing the filename where the downloaded data + * was actually stored. + * <P>Type: TEXT</P> + * <P>Owner can Read</P> + * <P>UI can Read</P> + */ + public static final String FILENAME = "_data"; + + /** + * The name of the column containing the MIME type of the downloaded data. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read</P> + * <P>UI can Read</P> + */ + public static final String MIMETYPE = "mimetype"; + + /** + * The name of the column containing the flag that controls the destination + * of the download. See the DESTINATION_* constants for a list of legal values. + * <P>Type: INTEGER</P> + * <P>Owner can Init/Read</P> + * <P>UI can Read</P> + */ + public static final String DESTINATION = "destination"; + + /** + * The name of the column containing the flags that controls whether + * the download must be saved with the filename used for OTA updates. + * Must be used with INTERNAL, and the initiating application must hold the + * android.permission.DOWNLOAD_OTA_UPDATE permission. + * <P>Type: BOOLEAN</P> + * <P>Owner can Init/Read</P> + * <P>UI can Read</P> + */ + public static final String OTA_UPDATE = "otaupdate"; + + /** + * The name of the columns containing the flag that controls whether + * files with private/inernal/system MIME types can be downloaded. + * <P>Type: BOOLEAN</P> + * <P>Owner can Init/Read</P> + */ + public static final String NO_SYSTEM_FILES = "no_system"; + + /** + * The name of the column containing the flags that controls whether the + * download is displayed by the UI. See the VISIBILITY_* constants for + * a list of legal values. + * <P>Type: INTEGER</P> + * <P>Owner can Init/Read/Write</P> + * <P>UI can Read/Write (only for entries that are visible)</P> + */ + public static final String VISIBILITY = "visibility"; + + /** + * The name of the column containing the command associated with the + * download. After a download is initiated, this is the only column that + * applications can modify. See the CONTROL_* constants for a list of legal + * values. Note: doesn't do anything in 1.0. The API will be hooked up + * in a future version, and is provided here as an indication of things + * to come. + * <P>Type: INTEGER</P> + * <P>Owner can Init/Read/Write</P> + * <P>UI can Init/Read/Write</P> + * @hide + */ + public static final String CONTROL = "control"; + + /** + * The name of the column containing the current status of the download. + * Applications can read this to follow the progress of each download. See + * the STATUS_* constants for a list of legal values. + * <P>Type: INTEGER</P> + * <P>Owner can Read</P> + * <P>UI can Read</P> + */ + public static final String STATUS = "status"; + + /** + * The name of the column containing the date at which some interesting + * status changed in the download. Stored as a System.currentTimeMillis() + * value. + * <P>Type: BIGINT</P> + * <P>Owner can Read</P> + * <P>UI can Read</P> + */ + public static final String LAST_MODIFICATION = "lastmod"; + + /** + * The name of the column containing the number of consecutive connections + * that have failed. + * <P>Type: INTEGER</P> + */ + public static final String FAILED_CONNECTIONS = "numfailed"; + + /** + * The name of the column containing the package name of the application + * that initiating the download. The download manager will send + * notifications to a component in this package when the download completes. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read</P> + * <P>UI can Read</P> + */ + public static final String NOTIFICATION_PACKAGE = "notificationpackage"; + + /** + * The name of the column containing the component name of the class that + * will receive notifications associated with the download. The + * package/class combination is passed to + * Intent.setClassName(String,String). + * <P>Type: TEXT</P> + * <P>Owner can Init/Read</P> + * <P>UI can Read</P> + */ + public static final String NOTIFICATION_CLASS = "notificationclass"; + + /** + * If extras are specified when requesting a download they will be provided in the intent that + * is sent to the specified class and package when a download has finished. + */ + public static final String NOTIFICATION_EXTRAS = "notificationextras"; + + /** + * The name of the column contain the values of the cookie to be used for + * the download. This is used directly as the value for the Cookie: HTTP + * header that gets sent with the request. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> + */ + public static final String COOKIE_DATA = "cookiedata"; + + /** + * The name of the column containing the user agent that the initiating + * application wants the download manager to use for this download. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> + */ + public static final String USER_AGENT = "useragent"; + + /** + * The name of the column containing the referer (sic) that the initiating + * application wants the download manager to use for this download. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> + */ + public static final String REFERER = "referer"; + + /** + * The name of the column containing the total size of the file being + * downloaded. + * <P>Type: INTEGER</P> + * <P>Owner can Read</P> + * <P>UI can Read</P> + */ + public static final String TOTAL_BYTES = "total_bytes"; + + /** + * The name of the column containing the size of the part of the file that + * has been downloaded so far. + * <P>Type: INTEGER</P> + * <P>Owner can Read</P> + * <P>UI can Read</P> + */ + public static final String CURRENT_BYTES = "current_bytes"; + + /** + * The name of the column containing the entity tag for the response. + * <P>Type: TEXT</P> + * @hide + */ + public static final String ETAG = "etag"; + + /** + * The name of the column containing the UID of the application that + * initiated the download. + * <P>Type: INTEGER</P> + * @hide + */ + public static final String UID = "uid"; + + /** + * The name of the column where the initiating application can provide the + * UID of another application that is allowed to access this download. If + * multiple applications share the same UID, all those applications will be + * allowed to access this download. This column can be updated after the + * download is initiated. + * <P>Type: INTEGER</P> + * <P>Owner can Init/Read/Write</P> + */ + public static final String OTHER_UID = "otheruid"; + + /** + * The name of the column where the initiating application can provided the + * title of this download. The title will be displayed ito the user in the + * list of downloads. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read/Write</P> + * <P>UI can Read</P> + */ + public static final String TITLE = "title"; + + /** + * The name of the column where the initiating application can provide the + * description of this download. The description will be displayed to the + * user in the list of downloads. + * <P>Type: TEXT</P> + * <P>Owner can Init/Read/Write</P> + * <P>UI can Read</P> + */ + public static final String DESCRIPTION = "description"; + + /** + * The name of the column where the download manager indicates whether the + * media scanner was notified about this download. + * <P>Type: BOOLEAN</P> + * @hide + */ + public static final String MEDIA_SCANNED = "scanned"; + + /* + * Lists the destinations that an application can specify for a download. + */ + + /** + * This download will be saved to the external storage. This is the + * default behavior, and should be used for any file that the user + * can freely access, copy, delete. Even with that destination, + * unencrypted DRM files are saved in secure internal storage. + * Downloads to the external destination only write files for which + * there is a registered handler. The resulting files are accessible + * by filename to all applications. + */ + public static final int DESTINATION_EXTERNAL = 0; + + /** + * This download will be saved to the download manager's private + * partition. This is the behavior used by applications that want to + * download private files that are used and deleted soon after they + * get downloaded. All file types are allowed, and only the initiating + * application can access the file (indirectly through a content + * provider). + */ + public static final int DESTINATION_CACHE_PARTITION = 1; + + /** + * This download will be saved to the download manager's private + * partition and will be purged as necessary to make space. This is + * for private files (similar to CACHE_PARTITION) that aren't deleted + * immediately after they are used, and are kept around by the download + * manager as long as space is available. + */ + public static final int DESTINATION_CACHE_PARTITION_PURGEABLE = 2; + + /** + * This download will be saved to the download manager's cache + * on the shared data partition. Use CACHE_PARTITION_PURGEABLE instead. + */ + public static final int DESTINATION_DATA_CACHE = 3; + + /* (not javadoc) + * This download will be saved to a file specified by the initiating + * applications. + * @hide + */ + //public static final int DESTINATION_PROVIDER = 4; + + /* + * Lists the commands that an application can set to control an ongoing + * download. Note: those aren't working. + */ + + /** + * This download can run + * @hide + */ + public static final int CONTROL_RUN = 0; + + /** + * This download must pause (might be restarted) + * @hide + */ + public static final int CONTROL_PAUSE = 1; + + /** + * This download must abort (will never be restarted) + * @hide + */ + public static final int CONTROL_STOP = 2; + + /* + * Lists the states that the download manager can set on a download + * to notify applications of the download progress. + * The codes follow the HTTP families:<br> + * 1xx: informational<br> + * 2xx: success<br> + * 3xx: redirects (not used by the download manager)<br> + * 4xx: client errors<br> + * 5xx: server errors + */ + + /** + * Returns whether the status is informational (i.e. 1xx). + */ + public static boolean isStatusInformational(int status) { + return (status >= 100 && status < 200); + } + + /** + * Returns whether the download is suspended. (i.e. whether the download + * won't complete without some action from outside the download + * manager). + */ + public static boolean isStatusSuspended(int status) { + return (status == STATUS_PENDING_PAUSED || status == STATUS_RUNNING_PAUSED); + } + + /** + * Returns whether the status is a success (i.e. 2xx). + */ + public static boolean isStatusSuccess(int status) { + return (status >= 200 && status < 300); + } + + /** + * Returns whether the status is an error (i.e. 4xx or 5xx). + */ + public static boolean isStatusError(int status) { + return (status >= 400 && status < 600); + } + + /** + * Returns whether the status is a client error (i.e. 4xx). + */ + public static boolean isStatusClientError(int status) { + return (status >= 400 && status < 500); + } + + /** + * Returns whether the status is a server error (i.e. 5xx). + */ + public static boolean isStatusServerError(int status) { + return (status >= 500 && status < 600); + } + + /** + * Returns whether the download has completed (either with success or + * error). + */ + public static boolean isStatusCompleted(int status) { + return (status >= 200 && status < 300) || (status >= 400 && status < 600); + } + + /** + * This download hasn't stated yet + */ + public static final int STATUS_PENDING = 190; + + /** + * This download hasn't stated yet and is paused + */ + public static final int STATUS_PENDING_PAUSED = 191; + + /** + * This download has started + */ + public static final int STATUS_RUNNING = 192; + + /** + * This download has started and is paused + */ + public static final int STATUS_RUNNING_PAUSED = 193; + + /** + * This download has successfully completed. + * Warning: there might be other status values that indicate success + * in the future. + * Use isSucccess() to capture the entire category. + */ + public static final int STATUS_SUCCESS = 200; + + /** + * This request couldn't be parsed. This is also used when processing + * requests with unknown/unsupported URI schemes. + */ + public static final int STATUS_BAD_REQUEST = 400; + + /** + * The server returned an auth error. + */ + public static final int STATUS_NOT_AUTHORIZED = 401; + + /** + * This download can't be performed because the content type cannot be + * handled. + */ + public static final int STATUS_NOT_ACCEPTABLE = 406; + + /** + * This download cannot be performed because the length cannot be + * determined accurately. This is the code for the HTTP error "Length + * Required", which is typically used when making requests that require + * a content length but don't have one, and it is also used in the + * client when a response is received whose length cannot be determined + * accurately (therefore making it impossible to know when a download + * completes). + */ + public static final int STATUS_LENGTH_REQUIRED = 411; + + /** + * This download was interrupted and cannot be resumed. + * This is the code for the HTTP error "Precondition Failed", and it is + * also used in situations where the client doesn't have an ETag at all. + */ + public static final int STATUS_PRECONDITION_FAILED = 412; + + /** + * This download was canceled + */ + public static final int STATUS_CANCELED = 490; + /** + * @hide + * Alternate spelling + */ + public static final int STATUS_CANCELLED = 490; + + /** + * This download has completed with an error. + * Warning: there will be other status values that indicate errors in + * the future. Use isStatusError() to capture the entire category. + */ + public static final int STATUS_UNKNOWN_ERROR = 491; + /** + * @hide + * Legacy name - use STATUS_UNKNOWN_ERROR + */ + public static final int STATUS_ERROR = 491; + + /** + * This download couldn't be completed because of a storage issue. + * Typically, that's because the filesystem is missing or full. + */ + public static final int STATUS_FILE_ERROR = 492; + + /** + * This download couldn't be completed because of an HTTP + * redirect code. + */ + public static final int STATUS_UNHANDLED_REDIRECT = 493; + + /** + * This download couldn't be completed because of an + * unspecified unhandled HTTP code. + */ + public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + + /** + * This download couldn't be completed because of an + * error receiving or processing data at the HTTP level. + */ + public static final int STATUS_HTTP_DATA_ERROR = 495; + + /** + * This download couldn't be completed because of an + * HttpException while setting up the request. + */ + public static final int STATUS_HTTP_EXCEPTION = 496; + + /* + * Lists the HTTP methods that the download manager can use. + */ + + /** + * GET + */ + public static final int METHOD_GET = 0; + + /** + * POST + */ + public static final int METHOD_POST = 1; + + /** + * This download is visible but only shows in the notifications + * while it's running (a separate download UI would still show it + * after completion). + */ + public static final int VISIBILITY_VISIBLE = 0; + + /** + * This download is visible and shows in the notifications after + * completion. + */ + public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; + + /** + * This download doesn't show in the UI or in the notifications. + */ + public static final int VISIBILITY_HIDDEN = 2; +} diff --git a/core/java/android/provider/DrmStore.java b/core/java/android/provider/DrmStore.java new file mode 100644 index 0000000..db71854 --- /dev/null +++ b/core/java/android/provider/DrmStore.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2008 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.drm.mobile1.DrmRawContent; +import android.drm.mobile1.DrmRights; +import android.drm.mobile1.DrmRightsManager; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * The DRM provider contains forward locked DRM content. + * + * @hide + */ +public final class DrmStore +{ + private static final String TAG = "DrmStore"; + + public static final String AUTHORITY = "drm"; + + /** + * This is in the Manifest class of the drm provider, but that isn't visible + * in the framework. + */ + private static final String ACCESS_DRM_PERMISSION = "android.permission.ACCESS_DRM"; + + /** + * Fields for DRM database + */ + + public interface Columns extends BaseColumns { + /** + * The data stream for the file + * <P>Type: DATA STREAM</P> + */ + public static final String DATA = "_data"; + + /** + * The size of the file in bytes + * <P>Type: INTEGER (long)</P> + */ + public static final String SIZE = "_size"; + + /** + * The title of the file content + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * The MIME type of the file + * <P>Type: TEXT</P> + */ + public static final String MIME_TYPE = "mime_type"; + + } + + public interface Images extends Columns { + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/images"); + } + + public interface Audio extends Columns { + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/audio"); + } + + /** + * Utility function for inserting a file into the DRM content provider. + * + * @param cr The content resolver to use + * @param file The file to insert + * @param title The title for the content (or null) + * @return uri to the DRM record or null + */ + public static final Intent addDrmFile(ContentResolver cr, File file, String title) { + FileInputStream fis = null; + OutputStream os = null; + Intent result = null; + + try { + fis = new FileInputStream(file); + DrmRawContent content = new DrmRawContent(fis, (int) file.length(), + DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING); + String mimeType = content.getContentType(); + + DrmRightsManager manager = manager = DrmRightsManager.getInstance(); + DrmRights rights = manager.queryRights(content); + InputStream stream = content.getContentInputStream(rights); + long size = stream.available(); + + Uri contentUri = null; + if (mimeType.startsWith("audio/")) { + contentUri = DrmStore.Audio.CONTENT_URI; + } else if (mimeType.startsWith("image/")) { + contentUri = DrmStore.Images.CONTENT_URI; + } else { + Log.w(TAG, "unsupported mime type " + mimeType); + } + + if (contentUri != null) { + ContentValues values = new ContentValues(3); + // compute title from file name, if it is not specified + if (title == null) { + title = file.getName(); + int lastDot = title.lastIndexOf('.'); + if (lastDot > 0) { + title = title.substring(0, lastDot); + } + } + values.put(DrmStore.Columns.TITLE, title); + values.put(DrmStore.Columns.SIZE, size); + values.put(DrmStore.Columns.MIME_TYPE, mimeType); + + Uri uri = cr.insert(contentUri, values); + if (uri != null) { + os = cr.openOutputStream(uri); + + byte[] buffer = new byte[1000]; + int count; + + while ((count = stream.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + result = new Intent(); + result.setDataAndType(uri, mimeType); + + } + } + } catch (Exception e) { + Log.e(TAG, "pushing file failed", e); + } finally { + try { + if (fis != null) + fis.close(); + if (os != null) + os.close(); + } catch (IOException e) { + Log.e(TAG, "IOException in DrmTest.onCreate()", e); + } + } + + return result; + } + + /** + * Utility function to enforce any permissions required to access DRM + * content. + * + * @param context A context used for checking calling permission. + */ + public static void enforceAccessDrmPermission(Context context) { + if (context.checkCallingOrSelfPermission(ACCESS_DRM_PERMISSION) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires DRM permission"); + } + } + +} diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java new file mode 100644 index 0000000..038ba21 --- /dev/null +++ b/core/java/android/provider/Gmail.java @@ -0,0 +1,2355 @@ +/* + * Copyright (C) 2007 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 android.provider; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; +import com.google.android.collect.Sets; + +import android.content.AsyncQueryHandler; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.text.style.CharacterStyle; +import android.text.util.Regex; +import android.util.Config; +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Observable; +import java.util.Observer; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A thin wrapper over the content resolver for accessing the gmail provider. + * + * @hide + */ +public final class Gmail { + public static final String GMAIL_AUTH_SERVICE = "mail"; + // These constants come from google3/java/com/google/caribou/backend/MailLabel.java. + public static final String LABEL_SENT = "^f"; + public static final String LABEL_INBOX = "^i"; + public static final String LABEL_DRAFT = "^r"; + public static final String LABEL_UNREAD = "^u"; + public static final String LABEL_TRASH = "^k"; + public static final String LABEL_SPAM = "^s"; + public static final String LABEL_STARRED = "^t"; + public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz' + public static final String LABEL_VOICEMAIL = "^vm"; + public static final String LABEL_ALL = "^all"; + // These constants (starting with "^^") are only used locally and are not understood by the + // server. + public static final String LABEL_VOICEMAIL_INBOX = "^^vmi"; + public static final String LABEL_CACHED = "^^cached"; + public static final String LABEL_OUTBOX = "^^out"; + + public static final String AUTHORITY = "gmail-ls"; + private static final String TAG = "gmail-ls"; + private static final String AUTHORITY_PLUS_CONVERSATIONS = + "content://" + AUTHORITY + "/conversations/"; + private static final String AUTHORITY_PLUS_LABELS = + "content://" + AUTHORITY + "/labels/"; + private static final String AUTHORITY_PLUS_MESSAGES = + "content://" + AUTHORITY + "/messages/"; + private static final String AUTHORITY_PLUS_SETTINGS = + "content://" + AUTHORITY + "/settings/"; + + public static final Uri BASE_URI = Uri.parse( + "content://" + AUTHORITY); + private static final Uri LABELS_URI = + Uri.parse(AUTHORITY_PLUS_LABELS); + private static final Uri CONVERSATIONS_URI = + Uri.parse(AUTHORITY_PLUS_CONVERSATIONS); + private static final Uri SETTINGS_URI = + Uri.parse(AUTHORITY_PLUS_SETTINGS); + + /** Separates email addresses in strings in the database. */ + public static final String EMAIL_SEPARATOR = "\n"; + public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern.compile(EMAIL_SEPARATOR); + + /** + * Space-separated lists have separators only between items. + */ + private static final char SPACE_SEPARATOR = ' '; + public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" "); + + /** + * Comma-separated lists have separators between each item, before the first and after the last + * item. The empty list is <tt>,</tt>. + * + * <p>This makes them easier to modify with SQL since it is not a special case to add or + * remove the last item. Having a separator on each side of each value also makes it safe to use + * SQL's REPLACE to remove an item from a string by using REPLACE(',value,', ','). + * + * <p>We could use the same separator for both lists but this makes it easier to remember which + * kind of list one is dealing with. + */ + private static final char COMMA_SEPARATOR = ','; + public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(","); + + /** Separates attachment info parts in strings in the database. */ + public static final String ATTACHMENT_INFO_SEPARATOR = "\n"; + public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN = + Pattern.compile(ATTACHMENT_INFO_SEPARATOR); + + public static final Character SENDER_LIST_SEPARATOR = '\n'; + public static final String SENDER_LIST_TOKEN_ELIDED = "e"; + public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n"; + public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d"; + public static final String SENDER_LIST_TOKEN_LITERAL = "l"; + public static final String SENDER_LIST_TOKEN_SENDING = "s"; + public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f"; + + /** Used for finding status in a cursor's extras. */ + public static final String EXTRA_STATUS = "status"; + + public static final String RESPOND_INPUT_COMMAND = "command"; + public static final String COMMAND_RETRY = "retry"; + public static final String COMMAND_ACTIVATE = "activate"; + public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse"; + public static final String COMMAND_RESPONSE_OK = "ok"; + public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand"; + + public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin"; + public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras"; + + private static final Pattern NAME_ADDRESS_PATTERN = Pattern.compile("\"(.*)\""); + private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern.compile("([^<]+)@"); + + private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap(); + public static final SimpleStringSplitter sSenderListSplitter = + new SimpleStringSplitter(SENDER_LIST_SEPARATOR); + public static String[] sSenderFragments = new String[8]; + + /** + * Returns the name in an address string + * @param addressString such as "bobby" <bob@example.com> + * @return returns the quoted name in the addressString, otherwise the username from the email + * address + */ + public static String getNameFromAddressString(String addressString) { + Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString); + if (namedAddressMatch.find()) { + String name = namedAddressMatch.group(1); + if (name.length() > 0) return name; + addressString = + addressString.substring(namedAddressMatch.end(), addressString.length()); + } + + Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN.matcher(addressString); + if (unnamedAddressMatch.find()) { + return unnamedAddressMatch.group(1); + } + + return addressString; + } + + /** + * Returns the email address in an address string + * @param addressString such as "bobby" <bob@example.com> + * @return returns the email address, such as bob@example.com from the example above + */ + public static String getEmailFromAddressString(String addressString) { + String result = addressString; + Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(addressString); + if (match.find()) { + result = addressString.substring(match.start(), match.end()); + } + + return result; + } + + /** + * Returns whether the label is user-defined (versus system-defined labels such as inbox, whose + * names start with "^"). + */ + public static boolean isLabelUserDefined(String label) { + // TODO: label should never be empty so we should be able to say [label.charAt(0) != '^']. + // However, it's a release week and I'm too scared to make that change. + return !label.startsWith("^"); + } + + private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets.newHashSet( + Gmail.LABEL_INBOX, + Gmail.LABEL_UNREAD, + Gmail.LABEL_TRASH, + Gmail.LABEL_SPAM, + Gmail.LABEL_STARRED); + + /** + * Returns whether the label is user-settable. For example, labels such as LABEL_DRAFT should + * only be set internally. + */ + public static boolean isLabelUserSettable(String label) { + return USER_SETTABLE_BUILTIN_LABELS.contains(label) || isLabelUserDefined(label); + } + + /** + * Returns the set of labels using the raw labels from a previous getRawLabels() + * as input. + * @return a copy of the set of labels. To add or remove labels call + * MessageCursor.addOrRemoveLabel on each message in the conversation. + */ + public static Set<Long> getLabelIdsFromLabelIdsString( + TextUtils.StringSplitter splitter) { + Set<Long> labelIds = Sets.newHashSet(); + for (String labelIdString : splitter) { + labelIds.add(Long.valueOf(labelIdString)); + } + return labelIds; + } + + /** + * @deprecated remove when the activities stop using canonical names to identify labels + */ + public static Set<String> getCanonicalNamesFromLabelIdsString( + LabelMap labelMap, TextUtils.StringSplitter splitter) { + Set<String> canonicalNames = Sets.newHashSet(); + for (long labelId : getLabelIdsFromLabelIdsString(splitter)) { + final String canonicalName = labelMap.getCanonicalName(labelId); + // We will sometimes see labels that the label map does not yet know about or that + // do not have names yet. + if (!TextUtils.isEmpty(canonicalName)) { + canonicalNames.add(canonicalName); + } else { + Log.w(TAG, "getCanonicalNamesFromLabelIdsString skipping label id: " + labelId); + } + } + return canonicalNames; + } + + /** + * @return a StringSplitter that is configured to split message label id strings + */ + public static TextUtils.StringSplitter newMessageLabelIdsSplitter() { + return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR); + } + + /** + * @return a StringSplitter that is configured to split conversation label id strings + */ + public static TextUtils.StringSplitter newConversationLabelIdsSplitter() { + return new CommaStringSplitter(); + } + + /** + * A splitter for strings of the form described in the docs for COMMA_SEPARATOR. + */ + private static class CommaStringSplitter extends TextUtils.SimpleStringSplitter { + + public CommaStringSplitter() { + super(COMMA_SEPARATOR); + } + + @Override + public void setString(String string) { + // The string should always be at least a single comma. + super.setString(string.substring(1)); + } + } + + /** + * Creates a single string of the form that getLabelIdsFromLabelIdsString can split. + */ + public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) { + StringBuilder sb = new StringBuilder(); + sb.append(COMMA_SEPARATOR); + for (Long labelId : labelIds) { + sb.append(labelId); + sb.append(COMMA_SEPARATOR); + } + return sb.toString(); + } + + public static final class ConversationColumns { + public static final String ID = "_id"; + public static final String SUBJECT = "subject"; + public static final String SNIPPET = "snippet"; + public static final String FROM = "fromAddress"; + public static final String DATE = "date"; + public static final String PERSONAL_LEVEL = "personalLevel"; + /** A list of label names with a space after each one (including the last one). This makes + * it easier remove individual labels from this list using SQL. */ + public static final String LABEL_IDS = "labelIds"; + public static final String NUM_MESSAGES = "numMessages"; + public static final String MAX_MESSAGE_ID = "maxMessageId"; + public static final String HAS_ATTACHMENTS = "hasAttachments"; + public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors"; + public static final String FORCE_ALL_UNREAD = "forceAllUnread"; + + private ConversationColumns() {} + } + + public static final class MessageColumns { + + public static final String ID = "_id"; + public static final String MESSAGE_ID = "messageId"; + public static final String CONVERSATION_ID = "conversation"; + public static final String SUBJECT = "subject"; + public static final String SNIPPET = "snippet"; + public static final String FROM = "fromAddress"; + public static final String TO = "toAddresses"; + public static final String CC = "ccAddresses"; + public static final String BCC = "bccAddresses"; + public static final String REPLY_TO = "replyToAddresses"; + public static final String DATE_SENT_MS = "dateSentMs"; + public static final String DATE_RECEIVED_MS = "dateReceivedMs"; + public static final String LIST_INFO = "listInfo"; + public static final String PERSONAL_LEVEL = "personalLevel"; + public static final String BODY = "body"; + public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources"; + public static final String LABEL_IDS = "labelIds"; + public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos"; + public static final String ERROR = "error"; + + // Fake columns used only for saving or sending messages. + public static final String FAKE_SAVE = "save"; + public static final String FAKE_REF_MESSAGE_ID = "refMessageId"; + + private MessageColumns() {} + } + + public static final class LabelColumns { + public static final String CANONICAL_NAME = "canonicalName"; + public static final String NAME = "name"; + public static final String NUM_CONVERSATIONS = "numConversations"; + public static final String NUM_UNREAD_CONVERSATIONS = + "numUnreadConversations"; + + private LabelColumns() {} + } + + public static final class SettingsColumns { + public static final String LABELS_INCLUDED = "labelsIncluded"; + public static final String LABELS_PARTIAL = "labelsPartial"; + public static final String CONVERSATION_AGE_DAYS = + "conversationAgeDays"; + public static final String MAX_ATTACHMENET_SIZE_MB = + "maxAttachmentSize"; + } + + // These are the projections that we need when getting cursors from the + // content provider. + private static String[] CONVERSATION_PROJECTION = { + ConversationColumns.ID, + ConversationColumns.SUBJECT, + ConversationColumns.SNIPPET, + ConversationColumns.FROM, + ConversationColumns.DATE, + ConversationColumns.PERSONAL_LEVEL, + ConversationColumns.LABEL_IDS, + ConversationColumns.NUM_MESSAGES, + ConversationColumns.MAX_MESSAGE_ID, + ConversationColumns.HAS_ATTACHMENTS, + ConversationColumns.HAS_MESSAGES_WITH_ERRORS, + ConversationColumns.FORCE_ALL_UNREAD}; + private static String[] MESSAGE_PROJECTION = { + MessageColumns.ID, + MessageColumns.MESSAGE_ID, + MessageColumns.CONVERSATION_ID, + MessageColumns.SUBJECT, + MessageColumns.SNIPPET, + MessageColumns.FROM, + MessageColumns.TO, + MessageColumns.CC, + MessageColumns.BCC, + MessageColumns.REPLY_TO, + MessageColumns.DATE_SENT_MS, + MessageColumns.DATE_RECEIVED_MS, + MessageColumns.LIST_INFO, + MessageColumns.PERSONAL_LEVEL, + MessageColumns.BODY, + MessageColumns.EMBEDS_EXTERNAL_RESOURCES, + MessageColumns.LABEL_IDS, + MessageColumns.JOINED_ATTACHMENT_INFOS, + MessageColumns.ERROR}; + private static String[] LABEL_PROJECTION = { + BaseColumns._ID, + LabelColumns.CANONICAL_NAME, + LabelColumns.NAME, + LabelColumns.NUM_CONVERSATIONS, + LabelColumns.NUM_UNREAD_CONVERSATIONS}; + private static String[] SETTINGS_PROJECTION = { + SettingsColumns.LABELS_INCLUDED, + SettingsColumns.LABELS_PARTIAL, + SettingsColumns.CONVERSATION_AGE_DAYS, + SettingsColumns.MAX_ATTACHMENET_SIZE_MB, + }; + + private ContentResolver mContentResolver; + + public Gmail(ContentResolver contentResolver) { + mContentResolver = contentResolver; + } + + /** + * Returns source if source is non-null. Returns the empty string otherwise. + */ + private static String toNonnullString(String source) { + if (source == null) { + return ""; + } else { + return source; + } + } + + /** + * Wraps a Cursor in a ConversationCursor + * + * @param account the account the cursor is associated with + * @param cursor The Cursor to wrap + * @return a new ConversationCursor + */ + public ConversationCursor getConversationCursorForCursor(String account, Cursor cursor) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + return new ConversationCursor(this, account, cursor); + } + + /** + * Asynchronously gets a cursor over all conversations matching a query. The + * query is in Gmail's query syntax. When the operation is complete the handler's + * onQueryComplete() method is called with the resulting Cursor. + * + * @param account run the query on this account + * @param handler An AsyncQueryHanlder that will be used to run the query + * @param token The token to pass to startQuery, which will be passed back to onQueryComplete + * @param query a query in Gmail's query syntax + */ + public void runQueryForConversations(String account, AsyncQueryHandler handler, int token, + String query) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI, account), + CONVERSATION_PROJECTION, query, null, null); + } + + /** + * Synchronously gets a cursor over all conversations matching a query. The + * query is in Gmail's query syntax. + * + * @param account run the query on this account + * @param query a query in Gmail's query syntax + */ + public ConversationCursor getConversationCursorForQuery(String account, String query) { + Cursor cursor = mContentResolver.query( + Uri.withAppendedPath(CONVERSATIONS_URI, account), CONVERSATION_PROJECTION, + query, null, null); + return new ConversationCursor(this, account, cursor); + } + + /** + * Gets a message cursor over the single message with the given id. + * + * @param account get the cursor for messages in this account + * @param messageId the id of the message + * @return a cursor over the message + */ + public MessageCursor getMessageCursorForMessageId(String account, long messageId) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); + Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null, null, null); + return new MessageCursor(this, mContentResolver, account, cursor); + } + + /** + * Gets a message cursor over the messages that match the query. Note that + * this simply finds all of the messages that match and returns them. It + * does not return all messages in conversations where any message matches. + * + * @param account get the cursor for messages in this account + * @param query a query in GMail's query syntax. Currently only queries of + * the form [label:<label>] are supported + * @return a cursor over the messages + */ + public MessageCursor getLocalMessageCursorForQuery(String account, String query) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"); + Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query, null, null); + return new MessageCursor(this, mContentResolver, account, cursor); + } + + /** + * Gets a cursor over all of the messages in a conversation. + * + * @param account get the cursor for messages in this account + * @param conversationId the id of the converstion to fetch messages for + * @return a cursor over messages in the conversation + */ + public MessageCursor getMessageCursorForConversationId(String account, long conversationId) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Uri uri = Uri.parse( + AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/messages"); + Cursor cursor = mContentResolver.query( + uri, MESSAGE_PROJECTION, null, null, null); + return new MessageCursor(this, mContentResolver, account, cursor); + } + + /** + * Expunge the indicated message. One use of this is to discard drafts. + * + * @param account the account of the message id + * @param messageId the id of the message to expunge + */ + public void expungeMessage(String account, long messageId) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); + mContentResolver.delete(uri, null, null); + } + + /** + * Adds or removes the label on the conversation. + * + * @param account the account of the conversation + * @param conversationId the conversation + * @param maxMessageId the highest message id to whose labels should be changed + * @param label the label to add or remove + * @param add true to add the label, false to remove it + * @throws NonexistentLabelException thrown if the label does not exist + */ + public void addOrRemoveLabelOnConversation( + String account, long conversationId, long maxMessageId, String label, + boolean add) + throws NonexistentLabelException { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + if (add) { + Uri uri = Uri.parse( + AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/labels"); + ContentValues values = new ContentValues(); + values.put(LabelColumns.CANONICAL_NAME, label); + values.put(ConversationColumns.MAX_MESSAGE_ID, maxMessageId); + mContentResolver.insert(uri, values); + } else { + String encodedLabel; + try { + encodedLabel = URLEncoder.encode(label, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + Uri uri = Uri.parse( + AUTHORITY_PLUS_CONVERSATIONS + account + "/" + + conversationId + "/labels/" + encodedLabel); + mContentResolver.delete( + uri, ConversationColumns.MAX_MESSAGE_ID, new String[]{"" + maxMessageId}); + } + } + + /** + * Adds or removes the label on the message. + * + * @param contentResolver the content resolver. + * @param account the account of the message + * @param conversationId the conversation containing the message + * @param messageId the id of the message to whose labels should be changed + * @param label the label to add or remove + * @param add true to add the label, false to remove it + * @throws NonexistentLabelException thrown if the label does not exist + */ + public static void addOrRemoveLabelOnMessage(ContentResolver contentResolver, String account, + long conversationId, long messageId, String label, boolean add) { + + // conversationId is unused but we want to start passing it whereever we pass a message id. + if (add) { + Uri uri = Uri.parse( + AUTHORITY_PLUS_MESSAGES + account + "/" + messageId + "/labels"); + ContentValues values = new ContentValues(); + values.put(LabelColumns.CANONICAL_NAME, label); + contentResolver.insert(uri, values); + } else { + String encodedLabel; + try { + encodedLabel = URLEncoder.encode(label, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + Uri uri = Uri.parse( + AUTHORITY_PLUS_MESSAGES + account + "/" + messageId + + "/labels/" + encodedLabel); + contentResolver.delete(uri, null, null); + } + } + + /** + * The mail provider will send an intent when certain changes happen in certain labels. + * Currently those labels are inbox and voicemail. + * + * <p>The intent will have the action ACTION_PROVIDER_CHANGED and the extras mentioned below. + * The data for the intent will be content://gmail-ls/unread/<name of label>. + * + * <p>The goal is to support the following user experience:<ul> + * <li>When present the new mail indicator reports the number of unread conversations in the + * inbox (or some other label).</li> + * <li>When the user views the inbox the indicator is removed immediately. They do not have to + * read all of the conversations.</li> + * <li>If more mail arrives the indicator reappears and shows the total number of unread + * conversations in the inbox.</li> + * <li>If the user reads the new conversations on the web the indicator disappears on the + * phone since there is no unread mail in the inbox that the user hasn't seen.</li> + * <li>The phone should vibrate/etc when it transitions from having no unseen unread inbox + * mail to having some.</li> + */ + + /** The account in which the change occurred. */ + static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account"; + + /** The number of unread conversations matching the label. */ + static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count"; + + /** Whether to get the user's attention, perhaps by vibrating. */ + static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention"; + + /** + * A label that is attached to all of the conversations being notified about. This enables the + * receiver of a notification to get a list of matching conversations. + */ + static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel"; + + /** + * Settings for which conversations should be synced to the phone. + * Conversations are synced if any message matches any of the following + * criteria: + * + * <ul> + * <li>the message has a label in the include set</li> + * <li>the message is no older than conversationAgeDays and has a label in the partial set. + * </li> + * <li>also, pending changes on the server: the message has no user-controllable labels.</li> + * </ul> + * + * <p>A user-controllable label is a user-defined label or star, inbox, + * trash, spam, etc. LABEL_UNREAD is not considered user-controllable. + */ + public static class Settings { + public long conversationAgeDays; + public long maxAttachmentSizeMb; + public String[] labelsIncluded; + public String[] labelsPartial; + } + + /** + * Returns the settings. + * @param account the account whose setting should be retrieved + */ + public Settings getSettings(String account) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Settings settings = new Settings(); + Cursor cursor = mContentResolver.query( + Uri.withAppendedPath(SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null); + cursor.moveToNext(); + settings.labelsIncluded = TextUtils.split(cursor.getString(0), SPACE_SEPARATOR_PATTERN); + settings.labelsPartial = TextUtils.split(cursor.getString(1), SPACE_SEPARATOR_PATTERN); + settings.conversationAgeDays = Long.parseLong(cursor.getString(2)); + settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3)); + cursor.close(); + return settings; + } + + /** + * Sets the settings. A sync will be scheduled automatically. + */ + public void setSettings(String account, Settings settings) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + ContentValues values = new ContentValues(); + values.put( + SettingsColumns.LABELS_INCLUDED, + TextUtils.join(" ", settings.labelsIncluded)); + values.put( + SettingsColumns.LABELS_PARTIAL, + TextUtils.join(" ", settings.labelsPartial)); + values.put( + SettingsColumns.CONVERSATION_AGE_DAYS, + settings.conversationAgeDays); + values.put( + SettingsColumns.MAX_ATTACHMENET_SIZE_MB, + settings.maxAttachmentSizeMb); + mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account), values, null, null); + } + + /** + * Uses sender instructions to build a formatted string. + * + * <p>Sender list instructions contain compact information about the sender list. Most work that + * can be done without knowing how much room will be availble for the sender list is done when + * creating the instructions. + * + * <p>The instructions string consists of tokens separated by SENDER_LIST_SEPARATOR. Here are + * the tokens, one per line:<ul> + * <li><tt>n</tt></li> + * <li><em>int</em>, the number of non-draft messages in the conversation</li> + * <li><tt>d</tt</li> + * <li><em>int</em>, the number of drafts in the conversation</li> + * <li><tt>l</tt></li> + * <li><em>literal html to be included in the output</em></li> + * <li><tt>s</tt> indicates that the message is sending (in the outbox without errors)</li> + * <li><tt>f</tt> indicates that the message failed to send (in the outbox with errors)</li> + * <li><em>for each message</em><ul> + * <li><em>int</em>, 0 for read, 1 for unread</li> + * <li><em>int</em>, the priority of the message. Zero is the most important</li> + * <li><em>text</em>, the sender text or blank for messages from 'me'</li> + * </ul></li> + * <li><tt>e</tt> to indicate that one or more messages have been elided</li> + * + * <p>The instructions indicate how many messages and drafts are in the conversation and then + * describe the most important messages in order, indicating the priority of each message and + * whether the message is unread. + * + * @param instructions instructions as described above + * @param sb the SpannableStringBuilder to append to + * @param maxChars the number of characters available to display the text + * @param unreadStyle the CharacterStyle for unread messages, or null + * @param draftsStyle the CharacterStyle for draft messages, or null + * @param sendingString the string to use when there are messages scheduled to be sent + * @param sendFailedString the string to use when there are messages that mailed to send + * @param meString the string to use for messages sent by this user + * @param draftString the string to use for "Draft" + * @param draftPluralString the string to use for "Drafts" + */ + public static void getSenderSnippet( + String instructions, SpannableStringBuilder sb, int maxChars, + CharacterStyle unreadStyle, + CharacterStyle draftsStyle, + CharSequence meString, CharSequence draftString, CharSequence draftPluralString, + CharSequence sendingString, CharSequence sendFailedString, + boolean forceAllUnread, boolean forceAllRead) { + assert !(forceAllUnread && forceAllRead); + boolean unreadStatusIsForced = forceAllUnread || forceAllRead; + boolean forcedUnreadStatus = forceAllUnread; + + // Measure each fragment. It's ok to iterate over the entire set of fragments because it is + // never a long list, even if there are many senders. + final Map<Integer, Integer> priorityToLength = sPriorityToLength; + priorityToLength.clear(); + + int maxFoundPriority = Integer.MIN_VALUE; + String numMessagesFragment = ""; + CharSequence draftsFragment = ""; + CharSequence sendingFragment = ""; + CharSequence sendFailedFragment = ""; + + sSenderListSplitter.setString(instructions); + int numFragments = 0; + String[] fragments = sSenderFragments; + int currentSize = fragments.length; + while (sSenderListSplitter.hasNext()) { + fragments[numFragments++] = sSenderListSplitter.next(); + if (numFragments == currentSize) { + sSenderFragments = new String[2 * currentSize]; + System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize); + currentSize *= 2; + fragments = sSenderFragments; + } + } + + for (int i = 0; i < numFragments;) { + String fragment0 = fragments[i++]; + if ("".equals(fragment0)) { + // This should be the final fragment. + } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { + // ignore + } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { + numMessagesFragment = " (" + fragments[i++] + ")"; + } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { + String numDraftsString = fragments[i++]; + int numDrafts = Integer.parseInt(numDraftsString); + draftsFragment = numDrafts == 1 ? draftString : + draftPluralString + " (" + numDraftsString + ")"; + } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) { + sb.append(Html.fromHtml(fragments[i++])); + return; + } else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { + sendingFragment = sendingString; + } else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { + sendFailedFragment = sendFailedString; + } else { + String priorityString = fragments[i++]; + CharSequence nameString = fragments[i++]; + if (nameString.length() == 0) nameString = meString; + int priority = Integer.parseInt(priorityString); + priorityToLength.put(priority, nameString.length()); + maxFoundPriority = Math.max(maxFoundPriority, priority); + } + } + + // Don't allocate fixedFragment unless we need it + SpannableStringBuilder fixedFragment = null; + int fixedFragmentLength = 0; + if (draftsFragment.length() != 0) { + if (fixedFragment == null) { + fixedFragment = new SpannableStringBuilder(); + } + fixedFragment.append(draftsFragment); + if (draftsStyle != null) { + fixedFragment.setSpan( + CharacterStyle.wrap(draftsStyle), + 0, fixedFragment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (sendingFragment.length() != 0) { + if (fixedFragment == null) { + fixedFragment = new SpannableStringBuilder(); + } + if (fixedFragment.length() != 0) fixedFragment.append(", "); + fixedFragment.append(sendingFragment); + } + if (sendFailedFragment.length() != 0) { + if (fixedFragment == null) { + fixedFragment = new SpannableStringBuilder(); + } + if (fixedFragment.length() != 0) fixedFragment.append(", "); + fixedFragment.append(sendFailedFragment); + } + + if (fixedFragment != null) { + fixedFragmentLength = fixedFragment.length(); + } + + final boolean normalMessagesExist = + numMessagesFragment.length() != 0 || maxFoundPriority != Integer.MIN_VALUE; + String preFixedFragement = ""; + if (normalMessagesExist && fixedFragmentLength != 0) { + preFixedFragement = ", "; + } + int maxPriorityToInclude = -1; // inclusive + int numCharsUsed = + numMessagesFragment.length() + preFixedFragement.length() + fixedFragmentLength; + int numSendersUsed = 0; + while (maxPriorityToInclude < maxFoundPriority) { + if (priorityToLength.containsKey(maxPriorityToInclude + 1)) { + int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1); + if (numCharsUsed > 0) length += 2; + // We must show at least two senders if they exist. If we don't have space for both + // then we will truncate names. + if (length > maxChars && numSendersUsed >= 2) { + break; + } + numCharsUsed = length; + numSendersUsed++; + } + maxPriorityToInclude++; + } + + int numCharsToRemovePerWord = 0; + if (numCharsUsed > maxChars) { + numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed; + } + + boolean elided = false; + for (int i = 0; i < numFragments;) { + String fragment0 = fragments[i++]; + if ("".equals(fragment0)) { + // This should be the final fragment. + } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { + elided = true; + } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { + i++; + } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { + i++; + } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { + } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { + } else { + final String unreadString = fragment0; + final String priorityString = fragments[i++]; + String nameString = fragments[i++]; + if (nameString.length() == 0) nameString = meString.toString(); + if (numCharsToRemovePerWord != 0) { + nameString = nameString.substring( + 0, Math.max(nameString.length() - numCharsToRemovePerWord, 0)); + } + final boolean unread = unreadStatusIsForced + ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0; + final int priority = Integer.parseInt(priorityString); + if (priority <= maxPriorityToInclude) { + if (sb.length() != 0) { + sb.append(elided ? " .. " : ", "); + } + elided = false; + int pos = sb.length(); + sb.append(nameString); + if (unread && unreadStyle != null) { + sb.setSpan(CharacterStyle.wrap(unreadStyle), + pos, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + elided = true; + } + } + } + sb.append(numMessagesFragment); + if (fixedFragmentLength != 0) { + sb.append(preFixedFragement); + sb.append(fixedFragment); + } + } + + /** + * This is a cursor that only defines methods to move throught the results + * and register to hear about changes. All access to the data is left to + * subinterfaces. + */ + public static class MailCursor extends ContentObserver { + + // A list of observers of this cursor. + private Set<MailCursorObserver> mObservers; + + // Updated values are accumulated here before being written out if the + // cursor is asked to persist the changes. + private ContentValues mUpdateValues; + + protected Cursor mCursor; + protected String mAccount; + + public Cursor getCursor() { + return mCursor; + } + + /** + * Constructs the MailCursor given a regular cursor, registering as a + * change observer of the cursor. + * @param account the account the cursor is associated with + * @param cursor the underlying cursor + */ + protected MailCursor(String account, Cursor cursor) { + super(new Handler()); + mObservers = new HashSet<MailCursorObserver>(); + mCursor = cursor; + mAccount = account; + if (mCursor != null) mCursor.registerContentObserver(this); + } + + /** + * Gets the account associated with this cursor. + * @return the account. + */ + public String getAccount() { + return mAccount; + } + + protected void checkThread() { + // Turn this on when activity code no longer runs in the sync thread + // after notifications of changes. +// Thread currentThread = Thread.currentThread(); +// if (currentThread != mThread) { +// throw new RuntimeException("Accessed from the wrong thread"); +// } + } + + /** + * Lazily constructs a map of update values to apply to the database + * if requested. This map is cleared out when we move to a different + * item in the result set. + * + * @return a map of values to be applied by an update. + */ + protected ContentValues getUpdateValues() { + if (mUpdateValues == null) { + mUpdateValues = new ContentValues(); + } + return mUpdateValues; + } + + /** + * Called whenever mCursor is changed to point to a different row. + * Subclasses should override this if they need to clear out state + * when this happens. + * + * Subclasses must call the inherited version if they override this. + */ + protected void onCursorPositionChanged() { + mUpdateValues = null; + } + + // ********* MailCursor + + /** + * Returns the numbers of rows in the cursor. + * + * @return the number of rows in the cursor. + */ + final public int count() { + if (mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + /** + * @return the current position of this cursor, or -1 if this cursor + * has not been initialized. + */ + final public int position() { + if (mCursor != null) { + return mCursor.getPosition(); + } else { + return -1; + } + } + + /** + * Move the cursor to an absolute position. The valid + * range of vaues is -1 <= position <= count. + * + * <p>This method will return true if the request destination was + * reachable, otherwise it returns false. + * + * @param position the zero-based position to move to. + * @return whether the requested move fully succeeded. + */ + final public boolean moveTo(int position) { + checkCursor(); + checkThread(); + boolean moved = mCursor.moveToPosition(position); + if (moved) onCursorPositionChanged(); + return moved; + } + + /** + * Move the cursor to the next row. + * + * <p>This method will return false if the cursor is already past the + * last entry in the result set. + * + * @return whether the move succeeded. + */ + final public boolean next() { + checkCursor(); + checkThread(); + boolean moved = mCursor.moveToNext(); + if (moved) onCursorPositionChanged(); + return moved; + } + + /** + * Release all resources and locks associated with the cursor. The + * cursor will not be valid after this function is called. + */ + final public void release() { + if (mCursor != null) { + mCursor.unregisterContentObserver(this); + mCursor.deactivate(); + } + } + + final public void registerContentObserver(ContentObserver observer) { + mCursor.registerContentObserver(observer); + } + + final public void unregisterContentObserver(ContentObserver observer) { + mCursor.unregisterContentObserver(observer); + } + + final public void registerDataSetObserver(DataSetObserver observer) { + mCursor.registerDataSetObserver(observer); + } + + final public void unregisterDataSetObserver(DataSetObserver observer) { + mCursor.unregisterDataSetObserver(observer); + } + + /** + * Register an observer to hear about changes to the cursor. + * + * @param observer the observer to register + */ + final public void registerObserver(MailCursorObserver observer) { + mObservers.add(observer); + } + + /** + * Unregister an observer. + * + * @param observer the observer to unregister + */ + final public void unregisterObserver(MailCursorObserver observer) { + mObservers.remove(observer); + } + + // ********* ContentObserver + + @Override + final public boolean deliverSelfNotifications() { + return false; + } + + @Override + public void onChange(boolean selfChange) { + if (Config.DEBUG) { + Log.d(TAG, "MailCursor is notifying " + mObservers.size() + " observers"); + } + for (MailCursorObserver o: mObservers) { + o.onCursorChanged(this); + } + } + + protected void checkCursor() { + if (mCursor == null) { + throw new IllegalStateException( + "cannot read from an insertion cursor"); + } + } + + /** + * Returns the string value of the column, or "" if the value is null. + */ + protected String getStringInColumn(int columnIndex) { + checkCursor(); + return toNonnullString(mCursor.getString(columnIndex)); + } + } + + /** + * A MailCursor observer is notified of changes to the result set of a + * cursor. + */ + public interface MailCursorObserver { + + /** + * Called when the result set of a cursor has changed. + * + * @param cursor the cursor whose result set has changed. + */ + void onCursorChanged(MailCursor cursor); + } + + /** + * Thrown when an operation is requested with a label that does not exist. + * + * TODO: this is here because I wanted a checked exception. However, I don't + * think that that is appropriate. In fact, I don't think that we should + * throw an exception at all because the label might have been valid when + * the caller presented it to the user but removed as a result of a sync. + * Maybe we should kill this and eat the errors. + */ + public static class NonexistentLabelException extends Exception { + // TODO: Add label name? + } + + /** + * A cursor over labels. + */ + public final class LabelCursor extends MailCursor { + + private int mNameIndex; + private int mNumConversationsIndex; + private int mNumUnreadConversationsIndex; + + private LabelCursor(String account, Cursor cursor) { + super(account, cursor); + + mNameIndex = mCursor.getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME); + mNumConversationsIndex = + mCursor.getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS); + mNumUnreadConversationsIndex = mCursor.getColumnIndexOrThrow( + LabelColumns.NUM_UNREAD_CONVERSATIONS); + } + + /** + * Gets the canonical name of the current label. + * + * @return the current label's name. + */ + public String getName() { + return getStringInColumn(mNameIndex); + } + + /** + * Gets the number of conversations with this label. + * + * @return the number of conversations with this label. + */ + public int getNumConversations() { + return mCursor.getInt(mNumConversationsIndex); + } + + /** + * Gets the number of unread conversations with this label. + * + * @return the number of unread conversations with this label. + */ + public int getNumUnreadConversations() { + return mCursor.getInt(mNumUnreadConversationsIndex); + } + } + + /** + * This is a map of labels. TODO: make it observable. + */ + public static final class LabelMap extends Observable { + private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); + + private ContentQueryMap mQueryMap; + private SortedSet<String> mSortedUserLabels; + private Map<String, Long> mCanonicalNameToId; + + private long mLabelIdSent; + private long mLabelIdInbox; + private long mLabelIdDraft; + private long mLabelIdUnread; + private long mLabelIdTrash; + private long mLabelIdSpam; + private long mLabelIdStarred; + private long mLabelIdChat; + private long mLabelIdVoicemail; + private long mLabelIdVoicemailInbox; + private long mLabelIdCached; + private long mLabelIdOutbox; + + private boolean mLabelsSynced = false; + + public LabelMap(ContentResolver contentResolver, String account, boolean keepUpdated) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + Cursor cursor = contentResolver.query( + Uri.withAppendedPath(LABELS_URI, account), LABEL_PROJECTION, null, null, null); + init(cursor, keepUpdated); + } + + public LabelMap(Cursor cursor, boolean keepUpdated) { + init(cursor, keepUpdated); + } + + private void init(Cursor cursor, boolean keepUpdated) { + mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID, keepUpdated, null); + mSortedUserLabels = new TreeSet<String>(java.text.Collator.getInstance()); + mCanonicalNameToId = Maps.newHashMap(); + updateDataStructures(); + mQueryMap.addObserver(new Observer() { + public void update(Observable observable, Object data) { + updateDataStructures(); + setChanged(); + notifyObservers(); + } + }); + } + + /** + * @return whether at least some labels have been synced. + */ + public boolean labelsSynced() { + return mLabelsSynced; + } + + /** + * Updates the data structures that are maintained separately from mQueryMap after the query + * map has changed. + */ + private void updateDataStructures() { + mSortedUserLabels.clear(); + mCanonicalNameToId.clear(); + for (Map.Entry<String, ContentValues> row : mQueryMap.getRows().entrySet()) { + long labelId = Long.valueOf(row.getKey()); + String canonicalName = row.getValue().getAsString(LabelColumns.CANONICAL_NAME); + if (isLabelUserDefined(canonicalName)) { + mSortedUserLabels.add(canonicalName); + } + mCanonicalNameToId.put(canonicalName, labelId); + + if (LABEL_SENT.equals(canonicalName)) { + mLabelIdSent = labelId; + } else if (LABEL_INBOX.equals(canonicalName)) { + mLabelIdInbox = labelId; + } else if (LABEL_DRAFT.equals(canonicalName)) { + mLabelIdDraft = labelId; + } else if (LABEL_UNREAD.equals(canonicalName)) { + mLabelIdUnread = labelId; + } else if (LABEL_TRASH.equals(canonicalName)) { + mLabelIdTrash = labelId; + } else if (LABEL_SPAM.equals(canonicalName)) { + mLabelIdSpam = labelId; + } else if (LABEL_STARRED.equals(canonicalName)) { + mLabelIdStarred = labelId; + } else if (LABEL_CHAT.equals(canonicalName)) { + mLabelIdChat = labelId; + } else if (LABEL_VOICEMAIL.equals(canonicalName)) { + mLabelIdVoicemail = labelId; + } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) { + mLabelIdVoicemailInbox = labelId; + } else if (LABEL_CACHED.equals(canonicalName)) { + mLabelIdCached = labelId; + } else if (LABEL_OUTBOX.equals(canonicalName)) { + mLabelIdOutbox = labelId; + } + mLabelsSynced = mLabelIdSent != 0 + && mLabelIdInbox != 0 + && mLabelIdDraft != 0 + && mLabelIdUnread != 0 + && mLabelIdTrash != 0 + && mLabelIdSpam != 0 + && mLabelIdStarred != 0 + && mLabelIdChat != 0 + && mLabelIdVoicemail != 0; + } + } + + public long getLabelIdSent() { + checkLabelsSynced(); + return mLabelIdSent; + } + + public long getLabelIdInbox() { + checkLabelsSynced(); + return mLabelIdInbox; + } + + public long getLabelIdDraft() { + checkLabelsSynced(); + return mLabelIdDraft; + } + + public long getLabelIdUnread() { + checkLabelsSynced(); + return mLabelIdUnread; + } + + public long getLabelIdTrash() { + checkLabelsSynced(); + return mLabelIdTrash; + } + + public long getLabelIdSpam() { + checkLabelsSynced(); + return mLabelIdSpam; + } + + public long getLabelIdStarred() { + checkLabelsSynced(); + return mLabelIdStarred; + } + + public long getLabelIdChat() { + checkLabelsSynced(); + return mLabelIdChat; + } + + public long getLabelIdVoicemail() { + checkLabelsSynced(); + return mLabelIdVoicemail; + } + + public long getLabelIdVoicemailInbox() { + checkLabelsSynced(); + return mLabelIdVoicemailInbox; + } + + public long getLabelIdCached() { + checkLabelsSynced(); + return mLabelIdCached; + } + + public long getLabelIdOutbox() { + checkLabelsSynced(); + return mLabelIdOutbox; + } + + private void checkLabelsSynced() { + if (!labelsSynced()) { + throw new IllegalStateException("LabelMap not initalized"); + } + } + + /** Returns the list of user-defined labels in alphabetical order. */ + public SortedSet<String> getSortedUserLabels() { + return mSortedUserLabels; + } + + private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS = + Lists.newArrayList( + LABEL_INBOX, LABEL_STARRED, LABEL_CHAT, LABEL_SENT, + LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL, + LABEL_SPAM, LABEL_TRASH); + + public static List<String> getSortedUserMeaningfulSystemLabels() { + return SORTED_USER_MEANINGFUL_SYSTEM_LABELS; + } + + private static final Set<String> FORCED_INCLUDED_LABELS = + Sets.newHashSet(LABEL_OUTBOX, LABEL_DRAFT); + + public static Set<String> getForcedIncludedLabels() { + return FORCED_INCLUDED_LABELS; + } + + private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS = + Sets.newHashSet(LABEL_INBOX); + + public static Set<String> getForcedIncludedOrPartialLabels() { + return FORCED_INCLUDED_OR_PARTIAL_LABELS; + } + + private static final Set<String> FORCED_UNSYNCED_LABELS = + Sets.newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH); + + public static Set<String> getForcedUnsyncedLabels() { + return FORCED_UNSYNCED_LABELS; + } + + /** + * Returns the number of conversation with a given label. + * @deprecated + */ + public int getNumConversations(String label) { + return getNumConversations(getLabelId(label)); + } + + /** Returns the number of conversation with a given label. */ + public int getNumConversations(long labelId) { + return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_CONVERSATIONS); + } + + /** + * Returns the number of unread conversation with a given label. + * @deprecated + */ + public int getNumUnreadConversations(String label) { + return getNumUnreadConversations(getLabelId(label)); + } + + /** Returns the number of unread conversation with a given label. */ + public int getNumUnreadConversations(long labelId) { + return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS); + } + + /** + * @return the canonical name for a label + */ + public String getCanonicalName(long labelId) { + return getLabelIdValues(labelId).getAsString(LabelColumns.CANONICAL_NAME); + } + + /** + * @return the human name for a label + */ + public String getName(long labelId) { + return getLabelIdValues(labelId).getAsString(LabelColumns.NAME); + } + + /** + * @return whether a given label is known + */ + public boolean hasLabel(long labelId) { + return mQueryMap.getRows().containsKey(Long.toString(labelId)); + } + + /** + * @return returns the id of a label given the canonical name + * @deprecated this is only needed because most of the UI uses label names instead of ids + */ + public long getLabelId(String canonicalName) { + if (mCanonicalNameToId.containsKey(canonicalName)) { + return mCanonicalNameToId.get(canonicalName); + } else { + throw new IllegalArgumentException("Unknown canonical name: " + canonicalName); + } + } + + private ContentValues getLabelIdValues(long labelId) { + final ContentValues values = mQueryMap.getValues(Long.toString(labelId)); + if (values != null) { + return values; + } else { + return EMPTY_CONTENT_VALUES; + } + } + + /** Force the map to requery. This should not be necessary outside tests. */ + public void requery() { + mQueryMap.requery(); + } + + public void close() { + mQueryMap.close(); + } + } + + private Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap(); + + public LabelMap getLabelMap(String account) { + Gmail.LabelMap labelMap = mLabelMaps.get(account); + if (labelMap == null) { + labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */); + mLabelMaps.put(account, labelMap); + } + return labelMap; + } + + public enum PersonalLevel { + NOT_TO_ME(0), + TO_ME_AND_OTHERS(1), + ONLY_TO_ME(2); + + private int mLevel; + + PersonalLevel(int level) { + mLevel = level; + } + + public int toInt() { + return mLevel; + } + + public static PersonalLevel fromInt(int level) { + switch (level) { + case 0: return NOT_TO_ME; + case 1: return TO_ME_AND_OTHERS; + case 2: return ONLY_TO_ME; + default: + throw new IllegalArgumentException( + level + " is not a personal level"); + } + } + } + + /** + * Indicates a version of an attachment. + */ + public enum AttachmentRendition { + /** + * The full version of an attachment if it can be handled on the device, otherwise the + * preview. + */ + BEST, + + /** A smaller or simpler version of the attachment, such as a scaled-down image or an HTML + * version of a document. Not always available. + */ + SIMPLE, + } + + /** + * The columns that can be requested when querying an attachment's download URI. See + * getAttachmentDownloadUri. + */ + public static final class AttachmentColumns implements BaseColumns { + + /** Contains a STATUS value from {@link android.provider.Downloads} */ + public static final String STATUS = "status"; + + /** + * The name of the file to open (with ContentProvider.open). If this is empty then continue + * to use the attachment's URI. + * + * TODO: I'm not sure that we need this. See the note in CL 66853-p9. + */ + public static final String FILENAME = "filename"; + } + + /** + * We track where an attachment came from so that we know how to download it and include it + * in new messages. + */ + public enum AttachmentOrigin { + /** Extras are "<conversationId>-<messageId>-<partId>". */ + SERVER_ATTACHMENT, + /** Extras are "<path>". */ + LOCAL_FILE; + + private static final String SERVER_EXTRAS_SEPARATOR = "_"; + + public static String serverExtras( + long conversationId, long messageId, String partId) { + return conversationId + SERVER_EXTRAS_SEPARATOR + + messageId + SERVER_EXTRAS_SEPARATOR + partId; + } + + /** + * @param extras extras as returned by serverExtras + * @return an array of conversationId, messageId, partId (all as strings) + */ + public static String[] splitServerExtras(String extras) { + return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR); + } + + public static String localFileExtras(Uri path) { + return path.toString(); + } + } + + public static final class Attachment { + /** Identifies the attachment uniquely when combined wih a message id.*/ + public String partId; + + /** The intended filename of the attachment.*/ + public String name; + + /** The native content type.*/ + public String contentType; + + /** The size of the attachment in its native form.*/ + public int size; + + /** + * The content type of the simple version of the attachment. Blank if no simple version is + * available. + */ + public String simpleContentType; + + public AttachmentOrigin origin; + + public String originExtras; + + public String toJoinedString() { + return TextUtils.join( + "|", Lists.newArrayList(partId == null ? "" : partId, + name.replace("|", ""), contentType, + size, simpleContentType, + origin.toString(), originExtras)); + } + + public static Attachment parseJoinedString(String joinedString) { + String[] fragments = TextUtils.split(joinedString, "\\|"); + int i = 0; + Attachment attachment = new Attachment(); + attachment.partId = fragments[i++]; + if (TextUtils.isEmpty(attachment.partId)) { + attachment.partId = null; + } + attachment.name = fragments[i++]; + attachment.contentType = fragments[i++]; + attachment.size = Integer.parseInt(fragments[i++]); + attachment.simpleContentType = fragments[i++]; + attachment.origin = AttachmentOrigin.valueOf(fragments[i++]); + attachment.originExtras = fragments[i++]; + return attachment; + } + } + + /** + * Any given attachment can come in two different renditions (see + * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to the sd card or to a + * cache. The gmail provider automatically syncs some attachments to the cache. Other + * attachments can be downloaded on demand. Attachments in the cache will be purged as needed to + * save space. Attachments on the SD card must be managed by the user or other software. + * + * @param account which account to use + * @param messageId the id of the mesage with the attachment + * @param attachment the attachment + * @param rendition the desired rendition + * @param saveToSd whether the attachment should be saved to (or loaded from) the sd card or + * @return the URI to ask the content provider to open in order to open an attachment. + */ + public static Uri getAttachmentUri( + String account, long messageId, Attachment attachment, + AttachmentRendition rendition, boolean saveToSd) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("account is empty"); + } + if (attachment.origin == AttachmentOrigin.LOCAL_FILE) { + return Uri.parse(attachment.originExtras); + } else { + return Uri.parse( + AUTHORITY_PLUS_MESSAGES).buildUpon() + .appendPath(account).appendPath(Long.toString(messageId)) + .appendPath("attachments").appendPath(attachment.partId) + .appendPath(rendition.toString()) + .appendPath(Boolean.toString(saveToSd)) + .build(); + } + } + + /** + * Return the URI to query in order to find out whether an attachment is downloaded. + * + * <p>Querying this will also start a download if necessary. The cursor returned by querying + * this URI can contain the columns in {@link android.provider.Gmail.AttachmentColumns}. + * + * <p>Deleting this URI will cancel the download if it was not started automatically by the + * provider. It will also remove bookkeeping for saveToSd downloads. + * + * @param attachmentUri the attachment URI as returned by getAttachmentUri. The URI's authority + * Gmail.AUTHORITY. If it is not then you should open the file directly. + */ + public static Uri getAttachmentDownloadUri(Uri attachmentUri) { + if (!"content".equals(attachmentUri.getScheme())) { + throw new IllegalArgumentException("Uri's scheme must be 'content': " + attachmentUri); + } + return attachmentUri.buildUpon().appendPath("download").build(); + } + + public enum CursorStatus { + LOADED, + LOADING, + ERROR, // A network error occurred. + } + + /** + * A cursor over messages. + */ + public static final class MessageCursor extends MailCursor { + + private LabelMap mLabelMap; + + private ContentResolver mContentResolver; + + /** + * Only valid if mCursor == null, in which case we are inserting a new + * message. + */ + long mInReplyToLocalMessageId; + boolean mPreserveAttachments; + + private int mIdIndex; + private int mConversationIdIndex; + private int mSubjectIndex; + private int mSnippetIndex; + private int mFromIndex; + private int mToIndex; + private int mCcIndex; + private int mBccIndex; + private int mReplyToIndex; + private int mDateSentMsIndex; + private int mDateReceivedMsIndex; + private int mListInfoIndex; + private int mPersonalLevelIndex; + private int mBodyIndex; + private int mBodyEmbedsExternalResourcesIndex; + private int mLabelIdsIndex; + private int mJoinedAttachmentInfosIndex; + private int mErrorIndex; + + private TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter(); + + public MessageCursor(Gmail gmail, ContentResolver cr, String account, Cursor cursor) { + super(account, cursor); + mLabelMap = gmail.getLabelMap(account); + if (cursor == null) { + throw new IllegalArgumentException( + "null cursor passed to MessageCursor()"); + } + + mContentResolver = cr; + + mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID); + mConversationIdIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID); + mSubjectIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SUBJECT); + mSnippetIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SNIPPET); + mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM); + mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO); + mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC); + mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC); + mReplyToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.REPLY_TO); + mDateSentMsIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS); + mDateReceivedMsIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS); + mListInfoIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LIST_INFO); + mPersonalLevelIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL); + mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY); + mBodyEmbedsExternalResourcesIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES); + mLabelIdsIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LABEL_IDS); + mJoinedAttachmentInfosIndex = + mCursor.getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS); + mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR); + + mInReplyToLocalMessageId = 0; + mPreserveAttachments = false; + } + + protected MessageCursor(ContentResolver cr, String account, long inReplyToMessageId, + boolean preserveAttachments) { + super(account, null); + mContentResolver = cr; + mInReplyToLocalMessageId = inReplyToMessageId; + mPreserveAttachments = preserveAttachments; + } + + @Override + protected void onCursorPositionChanged() { + super.onCursorPositionChanged(); + } + + public CursorStatus getStatus() { + Bundle extras = mCursor.getExtras(); + String stringStatus = extras.getString(EXTRA_STATUS); + return CursorStatus.valueOf(stringStatus); + } + + /** Retry a network request after errors. */ + public void retry() { + Bundle input = new Bundle(); + input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY); + Bundle output = mCursor.respond(input); + String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); + assert COMMAND_RESPONSE_OK.equals(response); + } + + /** + * Gets the message id of the current message. Note that this is an + * immutable local message (not, for example, GMail's message id, which + * is immutable). + * + * @return the message's id + */ + public long getMessageId() { + checkCursor(); + return mCursor.getLong(mIdIndex); + } + + /** + * Gets the message's conversation id. This must be immutable. (For + * example, with GMail this should be the original conversation id + * rather than the default notion of converation id.) + * + * @return the message's conversation id + */ + public long getConversationId() { + checkCursor(); + return mCursor.getLong(mConversationIdIndex); + } + + /** + * Gets the message's subject. + * + * @return the message's subject + */ + public String getSubject() { + return getStringInColumn(mSubjectIndex); + } + + /** + * Gets the message's snippet (the short piece of the body). The snippet + * is generated from the body and cannot be set directly. + * + * @return the message's snippet + */ + public String getSnippet() { + return getStringInColumn(mSnippetIndex); + } + + /** + * Gets the message's from address. + * + * @return the message's from address + */ + public String getFromAddress() { + return getStringInColumn(mFromIndex); + } + + /** + * Returns the addresses for the key, if it has been updated, or index otherwise. + */ + private String[] getAddresses(String key, int index) { + ContentValues updated = getUpdateValues(); + String addresses; + if (updated.containsKey(key)) { + addresses = (String)getUpdateValues().get(key); + } else { + addresses = getStringInColumn(index); + } + + return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN); + } + + /** + * Gets the message's to addresses. + * @return the message's to addresses + */ + public String[] getToAddresses() { + return getAddresses(MessageColumns.TO, mToIndex); + } + + /** + * Gets the message's cc addresses. + * @return the message's cc addresses + */ + public String[] getCcAddresses() { + return getAddresses(MessageColumns.CC, mCcIndex); + } + + /** + * Gets the message's bcc addresses. + * @return the message's bcc addresses + */ + public String[] getBccAddresses() { + return getAddresses(MessageColumns.BCC, mBccIndex); + } + + /** + * Gets the message's replyTo address. + * + * @return the message's replyTo address + */ + public String[] getReplyToAddress() { + return TextUtils.split(getStringInColumn(mReplyToIndex), EMAIL_SEPARATOR_PATTERN); + } + + public long getDateSentMs() { + checkCursor(); + return mCursor.getLong(mDateSentMsIndex); + } + + public long getDateReceivedMs() { + checkCursor(); + return mCursor.getLong(mDateReceivedMsIndex); + } + + public String getListInfo() { + return getStringInColumn(mListInfoIndex); + } + + public PersonalLevel getPersonalLevel() { + checkCursor(); + int personalLevelInt = mCursor.getInt(mPersonalLevelIndex); + return PersonalLevel.fromInt(personalLevelInt); + } + + /** + * @deprecated + */ + public boolean getExpanded() { + return true; + } + + /** + * Gets the message's body. + * + * @return the message's body + */ + public String getBody() { + return getStringInColumn(mBodyIndex); + } + + /** + * @return whether the message's body contains embedded references to external resources. In + * that case the resources should only be displayed if the user explicitly asks for them to + * be + */ + public boolean getBodyEmbedsExternalResources() { + checkCursor(); + return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0; + } + + /** + * @return a copy of the set of label ids + */ + public Set<Long> getLabelIds() { + String labelNames = mCursor.getString(mLabelIdsIndex); + mLabelIdsSplitter.setString(labelNames); + return getLabelIdsFromLabelIdsString(mLabelIdsSplitter); + } + + /** + * @return a joined string of labels separated by spaces. + */ + public String getRawLabelIds() { + return mCursor.getString(mLabelIdsIndex); + } + + /** + * Adds a label to a message (if add is true) or removes it (if add is + * false). + * + * @param label the label to add or remove + * @param add whether to add or remove the label + * @throws NonexistentLabelException thrown if the named label does not + * exist + */ + public void addOrRemoveLabel(String label, boolean add) throws NonexistentLabelException { + addOrRemoveLabelOnMessage(mContentResolver, mAccount, getConversationId(), + getMessageId(), label, add); + } + + public ArrayList<Attachment> getAttachmentInfos() { + ArrayList<Attachment> attachments = Lists.newArrayList(); + + String joinedAttachmentInfos = mCursor.getString(mJoinedAttachmentInfosIndex); + if (joinedAttachmentInfos != null) { + for (String joinedAttachmentInfo : + TextUtils.split(joinedAttachmentInfos, ATTACHMENT_INFO_SEPARATOR_PATTERN)) { + + Attachment attachment = Attachment.parseJoinedString(joinedAttachmentInfo); + attachments.add(attachment); + } + } + return attachments; + } + + /** + * @return the error text for the message. Error text gets set if the server rejects a + * message that we try to save or send. If there is error text then the message is no longer + * scheduled to be saved or sent. Calling save() or send() will clear any error as well as + * scheduling another atempt to save or send the message. + */ + public String getErrorText() { + return mCursor.getString(mErrorIndex); + } + } + + /** + * A helper class for creating or updating messags. Use the putXxx methods to provide initial or + * new values for the message. Then save or send the message. To save or send an existing + * message without making other changes to it simply provide an emty ContentValues. + */ + public static class MessageModification { + + /** + * Sets the message's subject. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param subject the new subject + */ + public static void putSubject(ContentValues values, String subject) { + values.put(MessageColumns.SUBJECT, subject); + } + + /** + * Sets the message's to address. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param toAddresses the new to addresses + */ + public static void putToAddresses(ContentValues values, String[] toAddresses) { + values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR, toAddresses)); + } + + /** + * Sets the message's cc address. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param ccAddresses the new cc addresses + */ + public static void putCcAddresses(ContentValues values, String[] ccAddresses) { + values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR, ccAddresses)); + } + + /** + * Sets the message's bcc address. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param bccAddresses the new bcc addresses + */ + public static void putBccAddresses(ContentValues values, String[] bccAddresses) { + values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR, bccAddresses)); + } + + /** + * Saves a new body for the message. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param body the new body of the message + */ + public static void putBody(ContentValues values, String body) { + values.put(MessageColumns.BODY, body); + } + + /** + * Sets the attachments on a message. Only valid for drafts. + * + * @param values the ContentValues that will be used to create or update the message + * @param attachments + */ + public static void putAttachments(ContentValues values, List<Attachment> attachments) { + values.put( + MessageColumns.JOINED_ATTACHMENT_INFOS, joinedAttachmentsString(attachments)); + } + + /** + * Create a new message and save it as a draft or send it. + * + * @param contentResolver the content resolver to use + * @param account the account to use + * @param values the values for the new message + * @param refMessageId the message that is being replied to or forwarded + * @param save whether to save or send the message + * @return the id of the new message + */ + public static long sendOrSaveNewMessage( + ContentResolver contentResolver, String account, + ContentValues values, long refMessageId, boolean save) { + values.put(MessageColumns.FAKE_SAVE, save); + values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId); + Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"); + Uri result = contentResolver.insert(uri, values); + return ContentUris.parseId(result); + } + + /** + * Update an existing draft and save it as a new draft or send it. + * + * @param contentResolver the content resolver to use + * @param account the account to use + * @param messageId the id of the message to update + * @param updateValues the values to change. Unspecified fields will not be altered + * @param save whether to resave the message as a draft or send it + */ + public static void sendOrSaveExistingMessage( + ContentResolver contentResolver, String account, long messageId, + ContentValues updateValues, boolean save) { + updateValues.put(MessageColumns.FAKE_SAVE, save); + updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0); + Uri uri = Uri.parse( + AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); + contentResolver.update(uri, updateValues, null, null); + } + + /** + * The string produced here is parsed by Gmail.MessageCursor#getAttachmentInfos. + */ + public static String joinedAttachmentsString(List<Gmail.Attachment> attachments) { + StringBuilder attachmentsSb = new StringBuilder(); + for (Gmail.Attachment attachment : attachments) { + if (attachmentsSb.length() != 0) { + attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR); + } + attachmentsSb.append(attachment.toJoinedString()); + } + return attachmentsSb.toString(); + } + + } + + /** + * A cursor over conversations. + * + * "Conversation" refers to the information needed to populate a list of + * conversations, not all of the messages in a conversation. + */ + public static final class ConversationCursor extends MailCursor { + + private LabelMap mLabelMap; + + private int mConversationIdIndex; + private int mSubjectIndex; + private int mSnippetIndex; + private int mFromIndex; + private int mDateIndex; + private int mPersonalLevelIndex; + private int mLabelIdsIndex; + private int mNumMessagesIndex; + private int mMaxMessageIdIndex; + private int mHasAttachmentsIndex; + private int mHasMessagesWithErrorsIndex; + private int mForceAllUnreadIndex; + + private TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter(); + + private ConversationCursor(Gmail gmail, String account, Cursor cursor) { + super(account, cursor); + mLabelMap = gmail.getLabelMap(account); + + mConversationIdIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.ID); + mSubjectIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SUBJECT); + mSnippetIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SNIPPET); + mFromIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.FROM); + mDateIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.DATE); + mPersonalLevelIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL); + mLabelIdsIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.LABEL_IDS); + mNumMessagesIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES); + mMaxMessageIdIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID); + mHasAttachmentsIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS); + mHasMessagesWithErrorsIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS); + mForceAllUnreadIndex = + mCursor.getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD); + } + + @Override + protected void onCursorPositionChanged() { + super.onCursorPositionChanged(); + } + + public CursorStatus getStatus() { + Bundle extras = mCursor.getExtras(); + String stringStatus = extras.getString(EXTRA_STATUS); + return CursorStatus.valueOf(stringStatus); + } + + /** Retry a network request after errors. */ + public void retry() { + Bundle input = new Bundle(); + input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY); + Bundle output = mCursor.respond(input); + String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); + assert COMMAND_RESPONSE_OK.equals(response); + } + + /** + * When a conversation cursor is created it becomes the active network cursor, which means + * that it will fetch results from the network if it needs to in order to show all mail that + * matches its query. If you later want to requery an older cursor and would like that + * cursor to be the active cursor you need to call this method before requerying. + */ + public void becomeActiveNetworkCursor() { + Bundle input = new Bundle(); + input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE); + Bundle output = mCursor.respond(input); + String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); + assert COMMAND_RESPONSE_OK.equals(response); + } + + /** + * Gets the conversation id. This must be immutable. (For example, with + * GMail this should be the original conversation id rather than the + * default notion of converation id.) + * + * @return the conversation id + */ + public long getConversationId() { + return mCursor.getLong(mConversationIdIndex); + } + + /** + * Returns the instructions for building from snippets. Pass this to getFromSnippetHtml + * in order to actually build the snippets. + * @return snippet instructions for use by getFromSnippetHtml() + */ + public String getFromSnippetInstructions() { + return getStringInColumn(mFromIndex); + } + + /** + * Gets the conversation's subject. + * + * @return the subject + */ + public String getSubject() { + return getStringInColumn(mSubjectIndex); + } + + /** + * Gets the conversation's snippet. + * + * @return the snippet + */ + public String getSnippet() { + return getStringInColumn(mSnippetIndex); + } + + /** + * Get's the conversation's personal level. + * + * @return the personal level. + */ + public PersonalLevel getPersonalLevel() { + int personalLevelInt = mCursor.getInt(mPersonalLevelIndex); + return PersonalLevel.fromInt(personalLevelInt); + } + + /** + * @return a copy of the set of labels. To add or remove labels call + * MessageCursor.addOrRemoveLabel on each message in the conversation. + * @deprecated use getLabelIds + */ + public Set<String> getLabels() { + return getLabels(getRawLabelIds(), mLabelMap); + } + + /** + * @return a copy of the set of labels. To add or remove labels call + * MessageCursor.addOrRemoveLabel on each message in the conversation. + */ + public Set<Long> getLabelIds() { + mLabelIdsSplitter.setString(getRawLabelIds()); + return getLabelIdsFromLabelIdsString(mLabelIdsSplitter); + } + + /** + * Returns the set of labels using the raw labels from a previous getRawLabels() + * as input. + * @return a copy of the set of labels. To add or remove labels call + * MessageCursor.addOrRemoveLabel on each message in the conversation. + */ + public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) { + mLabelIdsSplitter.setString(rawLabelIds); + return getCanonicalNamesFromLabelIdsString(labelMap, mLabelIdsSplitter); + } + + /** + * @return a joined string of labels separated by spaces. Use + * getLabels(rawLabels) to convert this to a Set of labels. + */ + public String getRawLabelIds() { + return mCursor.getString(mLabelIdsIndex); + } + + /** + * @return the number of messages in the conversation + */ + public int getNumMessages() { + return mCursor.getInt(mNumMessagesIndex); + } + + /** + * @return the max message id in the conversation + */ + public long getMaxMessageId() { + return mCursor.getLong(mMaxMessageIdIndex); + } + + public long getDateMs() { + return mCursor.getLong(mDateIndex); + } + + public boolean hasAttachments() { + return mCursor.getInt(mHasAttachmentsIndex) != 0; + } + + public boolean hasMessagesWithErrors() { + return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0; + } + + public boolean getForceAllUnread() { + return !mCursor.isNull(mForceAllUnreadIndex) + && mCursor.getInt(mForceAllUnreadIndex) != 0; + } + } +} diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java new file mode 100644 index 0000000..8ca97e1 --- /dev/null +++ b/core/java/android/provider/Im.java @@ -0,0 +1,1937 @@ +/* + * Copyright (C) 2007 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 android.provider; + +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; + +import java.util.HashMap; + +/** + * The IM provider stores all information about roster contacts, chat messages, presence, etc. + * + * @hide + */ +public class Im { + /** + * no public constructor since this is a utility class + */ + private Im() {} + + /** + * The Columns for IM providers (i.e. AIM, Y!, GTalk) + */ + public interface ProviderColumns { + /** + * The name of the IM provider + */ + String NAME = "name"; + + /** + * The full name of the provider + */ + String FULLNAME = "fullname"; + + /** + * The url users should visit to create a new account for this provider + */ + String SIGNUP_URL = "signup_url"; + } + + /** + * Known names corresponding to the {@link ProviderColumns#NAME} column + */ + public interface ProviderNames { + // + //NOTE: update Contacts.java with new providers when they're added. + // + String YAHOO = "Yahoo"; + String GTALK = "GTalk"; + String MSN = "MSN"; + String ICQ = "ICQ"; + String AIM = "AIM"; + } + + /** + * The ProviderCategories definitions are used for the Intent category for the Intent + * + * Intent intent = new Intent(Intent.ACTION_SENDTO, + * Uri.fromParts("im", data, null)). + * addCategory(category); + */ + public interface ProviderCategories { + String GTALK = "com.android.im.category.GTALK"; + String AIM = "com.android.im.category.AIM"; + String MSN = "com.android.im.category.MSN"; + String YAHOO = "com.android.im.category.YAHOO"; + String ICQ = "com.android.im.category.ICQ"; + } + + /** + * This table contains the IM providers + */ + public static final class Provider implements BaseColumns, ProviderColumns { + private Provider() {} + + public static final long getProviderIdForName(ContentResolver cr, String providerName) { + String[] selectionArgs = new String[1]; + selectionArgs[0] = providerName; + + Cursor cursor = cr.query(CONTENT_URI, + PROVIDER_PROJECTION, + NAME+"=?", + selectionArgs, null); + + long retVal = 0; + try { + if (cursor.moveToFirst()) { + retVal = cursor.getLong(cursor.getColumnIndexOrThrow(_ID)); + } + } finally { + cursor.close(); + } + + return retVal; + } + + public static final String getProviderNameForId(ContentResolver cr, long providerId) { + Cursor cursor = cr.query(CONTENT_URI, + PROVIDER_PROJECTION, + _ID + "=" + providerId, + null, null); + + String retVal = null; + try { + if (cursor.moveToFirst()) { + retVal = cursor.getString(cursor.getColumnIndexOrThrow(NAME)); + } + } finally { + cursor.close(); + } + + return retVal; + } + + /** + * This returns the provider name given a provider category. + * + * @param providerCategory the provider category defined in {@link ProviderCategories}. + * @return the corresponding provider name defined in {@link ProviderNames}. + */ + public static String getProviderNameForCategory(String providerCategory) { + if (providerCategory != null) { + if (providerCategory.equalsIgnoreCase(ProviderCategories.GTALK)) { + return ProviderNames.GTALK; + } else if (providerCategory.equalsIgnoreCase(ProviderCategories.AIM)) { + return ProviderNames.AIM; + } else if (providerCategory.equalsIgnoreCase(ProviderCategories.MSN)) { + return ProviderNames.MSN; + } else if (providerCategory.equalsIgnoreCase(ProviderCategories.YAHOO)) { + return ProviderNames.YAHOO; + } else if (providerCategory.equalsIgnoreCase(ProviderCategories.ICQ)) { + return ProviderNames.ICQ; + } + } + + return null; + } + + /** + * This returns the provider category given a provider name. + * + * @param providername the provider name defined in {@link ProviderNames}. + * @return the provider category defined in {@link ProviderCategories}. + */ + public static String getProviderCategoryFromName(String providername) { + if (providername != null) { + if (providername.equalsIgnoreCase(Im.ProviderNames.GTALK)) { + return Im.ProviderCategories.GTALK; + } else if (providername.equalsIgnoreCase(Im.ProviderNames.AIM)) { + return Im.ProviderCategories.AIM; + } else if (providername.equalsIgnoreCase(Im.ProviderNames.MSN)) { + return Im.ProviderCategories.MSN; + } else if (providername.equalsIgnoreCase(Im.ProviderNames.YAHOO)) { + return Im.ProviderCategories.YAHOO; + } else if (providername.equalsIgnoreCase(Im.ProviderNames.ICQ)) { + return Im.ProviderCategories.ICQ; + } + } + + return null; + } + + private static final String[] PROVIDER_PROJECTION = new String[] { + _ID, + NAME + }; + + public static final String ACTIVE_ACCOUNT_ID = "account_id"; + public static final String ACTIVE_ACCOUNT_USERNAME = "account_username"; + public static final String ACTIVE_ACCOUNT_PW = "account_pw"; + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/providers"); + + public static final Uri CONTENT_URI_WITH_ACCOUNT = + Uri.parse("content://im/providers/account"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-providers"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + } + + /** + * The columns for IM accounts. There can be more than one account for each IM provider. + */ + public interface AccountColumns { + /** + * The name of the account + * <P>Type: TEXT</P> + */ + String NAME = "name"; + + /** + * The IM provider for this account + * <P>Type: INTEGER</P> + */ + String PROVIDER = "provider"; + + /** + * The username for this account + * <P>Type: TEXT</P> + */ + String USERNAME = "username"; + + /** + * The password for this account + * <P>Type: TEXT</P> + */ + String PASSWORD = "pw"; + + /** + * A boolean value indicates if the account is active. + * <P>Type: INTEGER</P> + */ + String ACTIVE = "active"; + + /** + * A boolean value indicates if the account is locked (not editable) + * <P>Type: INTEGER</P> + */ + String LOCKED = "locked"; + + /** + * A boolean value to indicate whether this account is kept signed in. + * <P>Type: INTEGER</P> + */ + String KEEP_SIGNED_IN = "keep_signed_in"; + + /** + * A boolean value indiciating the last login state for this account + * <P>Type: INTEGER</P> + */ + String LAST_LOGIN_STATE = "last_login_state"; + } + + /** + * This table contains the IM accounts. + */ + public static final class Account implements BaseColumns, AccountColumns { + private Account() {} + + public static final long getProviderIdForAccount(ContentResolver cr, long accountId) { + Cursor cursor = cr.query(CONTENT_URI, + PROVIDER_PROJECTION, + _ID + "=" + accountId, + null /* selection args */, + null /* sort order */); + + long providerId = 0; + + try { + if (cursor.moveToFirst()) { + providerId = cursor.getLong(PROVIDER_COLUMN); + } + } finally { + cursor.close(); + } + + return providerId; + } + + private static final String[] PROVIDER_PROJECTION = new String[] { PROVIDER }; + private static final int PROVIDER_COLUMN = 0; + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/accounts"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * account. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-accounts"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * account. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-accounts"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + } + + /** + * Columns from the Contacts table. + */ + public interface ContactsColumns { + /** + * The username + * <P>Type: TEXT</P> + */ + String USERNAME = "username"; + + /** + * The nickname or display name + * <P>Type: TEXT</P> + */ + String NICKNAME = "nickname"; + + /** + * The IM provider for this contact + * <P>Type: INTEGER</P> + */ + String PROVIDER = "provider"; + + /** + * The account (within a IM provider) for this contact + * <P>Type: INTEGER</P> + */ + String ACCOUNT = "account"; + + /** + * The contactList this contact belongs to + * <P>Type: INTEGER</P> + */ + String CONTACTLIST = "contactList"; + + /** + * Contact type + * <P>Type: INTEGER</P> + */ + String TYPE = "type"; + + /** + * normal IM contact + */ + int TYPE_NORMAL = 0; + /** + * temporary contact, someone not in the list of contacts that we + * subscribe presence for. Usually created because of the user is + * having a chat session with this contact. + */ + int TYPE_TEMPORARY = 1; + /** + * temporary contact created for group chat. + */ + int TYPE_GROUP = 2; + /** + * blocked contact. + */ + int TYPE_BLOCKED = 3; + /** + * the contact is hidden. The client should always display this contact to the user. + */ + int TYPE_HIDDEN = 4; + /** + * the contact is pinned. The client should always display this contact to the user. + */ + int TYPE_PINNED = 5; + + /** + * Contact subscription status + * <P>Type: INTEGER</P> + */ + String SUBSCRIPTION_STATUS = "subscriptionStatus"; + + /** + * no pending subscription + */ + int SUBSCRIPTION_STATUS_NONE = 0; + /** + * requested to subscribe + */ + int SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING = 1; + /** + * requested to unsubscribe + */ + int SUBSCRIPTION_STATUS_UNSUBSCRIBE_PENDING = 2; + + /** + * Contact subscription type + * <P>Type: INTEGER </P> + */ + String SUBSCRIPTION_TYPE = "subscriptionType"; + + /** + * The user and contact have no interest in each other's presence. + */ + int SUBSCRIPTION_TYPE_NONE = 0; + /** + * The user wishes to stop receiving presence updates from the contact. + */ + int SUBSCRIPTION_TYPE_REMOVE = 1; + /** + * The user is interested in receiving presence updates from the contact. + */ + int SUBSCRIPTION_TYPE_TO = 2; + /** + * The contact is interested in receiving presence updates from the user. + */ + int SUBSCRIPTION_TYPE_FROM = 3; + /** + * The user and contact have a mutual interest in each other's presence. + */ + int SUBSCRIPTION_TYPE_BOTH = 4; + /** + * This is a special type reserved for pending subscription requests + */ + int SUBSCRIPTION_TYPE_INVITATIONS = 5; + + /** + * Quick Contact: derived from Google Contact Extension's "message_count" attribute. + * <P>Type: INTEGER</P> + */ + String QUICK_CONTACT = "qc"; + + /** + * Google Contact Extension attribute + * + * Rejected: a boolean value indicating whether a subscription request from + * this client was ever rejected by the user. "true" indicates that it has. + * This is provided so that a client can block repeated subscription requests. + * <P>Type: INTEGER</P> + */ + String REJECTED = "rejected"; + } + + /** + * This table contains contacts. + */ + public static final class Contacts implements BaseColumns, + ContactsColumns, PresenceColumns, ChatsColumns { + /** + * no public constructor since this is a utility class + */ + private Contacts() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/contacts"); + + /** + * The content:// style URL for contacts joined with presence + */ + public static final Uri CONTENT_URI_WITH_PRESENCE = + Uri.parse("content://im/contactsWithPresence"); + + /** + * The content:// style URL for barebone contacts, not joined with any other table + */ + public static final Uri CONTENT_URI_CONTACTS_BAREBONE = + Uri.parse("content://im/contactsBarebone"); + + /** + * The content:// style URL for contacts who have an open chat session + */ + public static final Uri CONTENT_URI_CHAT_CONTACTS = + Uri.parse("content://im/contacts/chatting"); + + /** + * The content:// style URL for contacts who have been blocked + */ + public static final Uri CONTENT_URI_BLOCKED_CONTACTS = + Uri.parse("content://im/contacts/blocked"); + + /** + * The content:// style URL for contacts by provider and account + */ + public static final Uri CONTENT_URI_CONTACTS_BY = + Uri.parse("content://im/contacts"); + + /** + * The content:// style URL for contacts by provider and account, + * and who have an open chat session + */ + public static final Uri CONTENT_URI_CHAT_CONTACTS_BY = + Uri.parse("content://im/contacts/chatting"); + + /** + * The content:// style URL for contacts by provider and account, + * and who are online + */ + public static final Uri CONTENT_URI_ONLINE_CONTACTS_BY = + Uri.parse("content://im/contacts/online"); + + /** + * The content:// style URL for contacts by provider and account, + * and who are offline + */ + public static final Uri CONTENT_URI_OFFLINE_CONTACTS_BY = + Uri.parse("content://im/contacts/offline"); + + /** + * The content:// style URL for operations on bulk contacts + */ + public static final Uri BULK_CONTENT_URI = + Uri.parse("content://im/bulk_contacts"); + + /** + * The content:// style URL for the count of online contacts in each + * contact list by provider and account. + */ + public static final Uri CONTENT_URI_ONLINE_COUNT = + Uri.parse("content://im/contacts/onlineCount"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-contacts"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-contacts"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = + "subscriptionType DESC, last_message_date DESC," + + " mode DESC, nickname COLLATE UNICODE ASC"; + + public static final String CHATS_CONTACT = "chats_contact"; + + public static final String AVATAR_HASH = "avatars_hash"; + + public static final String AVATAR_DATA = "avatars_data"; + } + + /** + * Columns from the ContactList table. + */ + public interface ContactListColumns { + String NAME = "name"; + String PROVIDER = "provider"; + String ACCOUNT = "account"; + } + + /** + * This table contains the contact lists. + */ + public static final class ContactList implements BaseColumns, + ContactListColumns { + private ContactList() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/contactLists"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-contactLists"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-contactLists"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name COLLATE UNICODE ASC"; + + public static final String PROVIDER_NAME = "provider_name"; + + public static final String ACCOUNT_NAME = "account_name"; + } + + /** + * Columns from the BlockedList table. + */ + public interface BlockedListColumns { + /** + * The username of the blocked contact. + * <P>Type: TEXT</P> + */ + String USERNAME = "username"; + + /** + * The nickname of the blocked contact. + * <P>Type: TEXT</P> + */ + String NICKNAME = "nickname"; + + /** + * The provider id of the blocked contact. + * <P>Type: INT</P> + */ + String PROVIDER = "provider"; + + /** + * The account id of the blocked contact. + * <P>Type: INT</P> + */ + String ACCOUNT = "account"; + } + + /** + * This table contains blocked lists + */ + public static final class BlockedList implements BaseColumns, BlockedListColumns { + private BlockedList() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/blockedList"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-blockedList"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-blockedList"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "nickname ASC"; + + public static final String PROVIDER_NAME = "provider_name"; + + public static final String ACCOUNT_NAME = "account_name"; + + public static final String AVATAR_DATA = "avatars_data"; + } + + /** + * Columns from the contactsEtag table + */ + public interface ContactsEtagColumns { + /** + * The roster etag, computed by the server, stored on the client. There is one etag + * per account roster. + * <P>Type: TEXT</P> + */ + String ETAG = "etag"; + + /** + * The account id for the etag. + * <P> Type: INTEGER </P> + */ + String ACCOUNT = "account"; + } + + public static final class ContactsEtag implements BaseColumns, ContactsEtagColumns { + private ContactsEtag() {} + + public static final Cursor query(ContentResolver cr, + String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, null); + } + + public static final Cursor query(ContentResolver cr, + String[] projection, String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? null : orderBy); + } + + public static final String getRosterEtag(ContentResolver resolver, long accountId) { + String retVal = null; + + Cursor c = resolver.query(CONTENT_URI, + CONTACT_ETAG_PROJECTION, + ACCOUNT + "=" + accountId, + null /* selection args */, + null /* sort order */); + + try { + if (c.moveToFirst()) { + retVal = c.getString(COLUMN_ETAG); + } + } finally { + c.close(); + } + + return retVal; + } + + private static final String[] CONTACT_ETAG_PROJECTION = new String[] { + Im.ContactsEtag.ETAG // 0 + }; + + private static int COLUMN_ETAG = 0; + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/contactsEtag"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-contactsEtag"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-contactsEtag"; + } + + /** + * Message type definition + */ + public interface MessageType { + int OUTGOING = 0; + int INCOMING = 1; + int PRESENCE_AVAILABLE = 2; + int PRESENCE_AWAY = 3; + int PRESENCE_DND = 4; + int PRESENCE_UNAVAILABLE = 5; + int CONVERT_TO_GROUPCHAT = 6; + int STATUS = 7; + int POSTPONED = 8; + } + + /** + * The common columns for both one-to-one chat messages or group chat messages. + */ + public interface BaseMessageColumns { + /** + * The user this message belongs to + * <P>Type: TEXT</P> + */ + String CONTACT = "contact"; + + /** + * The body + * <P>Type: TEXT</P> + */ + String BODY = "body"; + + /** + * The date this message is sent or received + * <P>Type: INTEGER</P> + */ + String DATE = "date"; + + /** + * Message Type, see {@link MessageType} + * <P>Type: INTEGER</P> + */ + String TYPE = "type"; + + /** + * Error Code: 0 means no error. + * <P>Type: INTEGER </P> + */ + String ERROR_CODE = "err_code"; + + /** + * Error Message + * <P>Type: TEXT</P> + */ + String ERROR_MESSAGE = "err_msg"; + + /** + * Packet ID, auto assigned by the GTalkService for outgoing messages or the + * GTalk server for incoming messages. The packet id field is optional for messages, + * so it could be null. + * <P>Type: STRING</P> + */ + String PACKET_ID = "packet_id"; + } + + /** + * Columns from the Messages table. + */ + public interface MessagesColumns extends BaseMessageColumns{ + /** + * The provider id + * <P> Type: INTEGER </P> + */ + String PROVIDER = "provider"; + + /** + * The account id + * <P> Type: INTEGER </P> + */ + String ACCOUNT = "account"; + } + + /** + * This table contains messages. + */ + public static final class Messages implements BaseColumns, MessagesColumns { + /** + * no public constructor since this is a utility class + */ + private Messages() {} + + /** + * Gets the Uri to query messages by contact. + * + * @param providerId the provider id of the contact. + * @param accountId the account id of the contact. + * @param username the user name of the contact. + * @return the Uri + */ + public static final Uri getContentUriByContact(long providerId, + long accountId, String username) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY.buildUpon(); + ContentUris.appendId(builder, providerId); + ContentUris.appendId(builder, accountId); + builder.appendPath(username); + return builder.build(); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/messages"); + + /** + * The content:// style URL for messages by provider and account + */ + public static final Uri CONTENT_URI_MESSAGES_BY = + Uri.parse("content://im/messagesBy"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-messages"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-messages"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date ASC"; + + } + + /** + * Columns for the GroupMember table. + */ + public interface GroupMemberColumns { + /** + * The id of the group this member belongs to. + * <p>Type: INTEGER</p> + */ + String GROUP = "groupId"; + + /** + * The full name of this member. + * <p>Type: TEXT</p> + */ + String USERNAME = "username"; + + /** + * The nick name of this member. + * <p>Type: TEXT</p> + */ + String NICKNAME = "nickname"; + } + + public final static class GroupMembers implements GroupMemberColumns { + private GroupMembers(){} + + public static final Uri CONTENT_URI = + Uri.parse("content://im/groupMembers"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * group members. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-groupMembers"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * group member. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-groupMembers"; + } + + /** + * Columns from the Invitation table. + */ + public interface InvitationColumns { + /** + * The provider id. + * <p>Type: INTEGER</p> + */ + String PROVIDER = "providerId"; + + /** + * The account id. + * <p>Type: INTEGER</p> + */ + String ACCOUNT = "accountId"; + + /** + * The invitation id. + * <p>Type: TEXT</p> + */ + String INVITE_ID = "inviteId"; + + /** + * The name of the sender of the invitation. + * <p>Type: TEXT</p> + */ + String SENDER = "sender"; + + /** + * The name of the group which the sender invite you to join. + * <p>Type: TEXT</p> + */ + String GROUP_NAME = "groupName"; + + /** + * A note + * <p>Type: TEXT</p> + */ + String NOTE = "note"; + + /** + * The current status of the invitation. + * <p>Type: TEXT</p> + */ + String STATUS = "status"; + + int STATUS_PENDING = 0; + int STATUS_ACCEPTED = 1; + int STATUS_REJECTED = 2; + } + + /** + * This table contains the invitations received from others. + */ + public final static class Invitation implements InvitationColumns, + BaseColumns { + private Invitation() { + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/invitations"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * invitations. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-invitations"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * invitation. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-invitations"; + } + + /** + * Columns from the GroupMessages table + */ + public interface GroupMessageColumns extends BaseMessageColumns { + /** + * The group this message belongs to + * <p>Type: TEXT</p> + */ + String GROUP = "groupId"; + } + + /** + * This table contains group messages. + */ + public final static class GroupMessages implements BaseColumns, + GroupMessageColumns { + private GroupMessages() {} + + /** + * Gets the Uri to query group messages by group. + * + * @param groupId the group id. + * @return the Uri + */ + public static final Uri getContentUriByGroup(long groupId) { + Uri.Builder builder = CONTENT_URI_GROUP_MESSAGES_BY.buildUpon(); + ContentUris.appendId(builder, groupId); + return builder.build(); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/groupMessages"); + + /** + * The content:// style URL for group messages by provider and account + */ + public static final Uri CONTENT_URI_GROUP_MESSAGES_BY = + Uri.parse("content://im/groupMessagesBy"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * group messages. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-groupMessages"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * group message. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-groupMessages"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date ASC"; + } + + /** + * Columns from the Avatars table + */ + public interface AvatarsColumns { + /** + * The contact this avatar belongs to + * <P>Type: TEXT</P> + */ + String CONTACT = "contact"; + + String PROVIDER = "provider_id"; + + String ACCOUNT = "account_id"; + + /** + * The hash of the image data + * <P>Type: TEXT</P> + */ + String HASH = "hash"; + + /** + * raw image data + * <P>Type: BLOB</P> + */ + String DATA = "data"; + } + + /** + * This table contains avatars. + */ + public static final class Avatars implements BaseColumns, AvatarsColumns { + /** + * no public constructor since this is a utility class + */ + private Avatars() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/avatars"); + + /** + * The content:// style URL for avatars by provider, account and contact + */ + public static final Uri CONTENT_URI_AVATARS_BY = + Uri.parse("content://im/avatarsBy"); + + /** + * The MIME type of {@link #CONTENT_URI} providing the avatars + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-avatars"; + + /** + * The MIME type of a {@link #CONTENT_URI} + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-avatars"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "contact ASC"; + + } + + /** + * Columns shared between the IM and contacts presence tables + */ + interface CommonPresenceColumns { + /** + * The priority, an integer, used by XMPP presence + * <P>Type: INTEGER</P> + */ + String PRIORITY = "priority"; + + /** + * The server defined status. + * <P>Type: INTEGER (one of the values below)</P> + */ + String PRESENCE_STATUS = "mode"; + + /** + * Presence Status definition + */ + int OFFLINE = 0; + int INVISIBLE = 1; + int AWAY = 2; + int IDLE = 3; + int DO_NOT_DISTURB = 4; + int AVAILABLE = 5; + + /** + * The user defined status line. + * <P>Type: TEXT</P> + */ + String PRESENCE_CUSTOM_STATUS = "status"; + } + + /** + * Columns from the Presence table. + */ + public interface PresenceColumns extends CommonPresenceColumns { + /** + * The contact id + * <P>Type: INTEGER</P> + */ + String CONTACT_ID = "contact_id"; + + /** + * The contact's JID resource, only relevant for XMPP contact + * <P>Type: TEXT</P> + */ + String JID_RESOURCE = "jid_resource"; + + /** + * The contact's client type + */ + String CLIENT_TYPE = "client_type"; + + /** + * client type definitions + */ + int CLIENT_TYPE_DEFAULT = 0; + int CLIENT_TYPE_MOBILE = 1; + int CLIENT_TYPE_ANDROID = 2; + } + + /** + * Contains presence infomation for contacts. + */ + public static final class Presence implements BaseColumns, PresenceColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/presence"); + + /** + * The content URL for IM presences for an account + */ + public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/presence/account"); + + /** + * The content:// style URL for operations on bulk contacts + */ + public static final Uri BULK_CONTENT_URI = Uri.parse("content://im/bulk_presence"); + + /** + * The content:// style URL for seeding presences for a given account id. + */ + public static final Uri SEED_PRESENCE_BY_ACCOUNT_CONTENT_URI = + Uri.parse("content://im/seed_presence/account"); + + /** + * The MIME type of a {@link #CONTENT_URI} providing a directory of presence + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-presence"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "mode DESC"; + } + + /** + * Columns from the Chats table. + */ + public interface ChatsColumns { + /** + * The contact ID this chat belongs to. The value is a long. + * <P>Type: TEXT</P> + */ + String CONTACT_ID = "contact_id"; + + /** + * The GTalk JID resource. The value is a string. + */ + String JID_RESOURCE = "jid_resource"; + + /** + * Whether this is a groupchat or not. + */ + // TODO: remove this column since we already have a tag in contacts + // table to indicate it's a group chat. + String GROUP_CHAT = "groupchat"; + + /** + * The last unread message. This both indicates that there is an + * unread message, and what the message is. + * + * <P>Type: TEXT</P> + */ + String LAST_UNREAD_MESSAGE = "last_unread_message"; + + /** + * The last message timestamp + * <P>Type: INT</P> + */ + String LAST_MESSAGE_DATE = "last_message_date"; + + /** + * A message that is being composed. This indicates that there was a + * message being composed when the chat screen was shutdown, and what the + * message is. + * + * <P>Type: TEXT</P> + */ + String UNSENT_COMPOSED_MESSAGE = "unsent_composed_message"; + } + + /** + * Contains ongoing chat sessions. + */ + public static final class Chats implements BaseColumns, ChatsColumns { + /** + * no public constructor since this is a utility class + */ + private Chats() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/chats"); + + /** + * The content URL for all chats that belong to the account + */ + public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/chats/account"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of chats. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-chats"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single chat. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-chats"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "last_message_date ASC"; + } + + /** + * Columns from session cookies table. Used for IMPS. + */ + public static interface SessionCookiesColumns { + String NAME = "name"; + String VALUE = "value"; + String PROVIDER = "provider"; + String ACCOUNT = "account"; + } + + /** + * Contains IMPS session cookies. + */ + public static class SessionCookies implements SessionCookiesColumns, BaseColumns { + private SessionCookies() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/sessionCookies"); + + /** + * The content:// style URL for session cookies by provider and account + */ + public static final Uri CONTENT_URI_SESSION_COOKIES_BY = + Uri.parse("content://im/sessionCookiesBy"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android-dir/im-sessionCookies"; + } + + /** + * Columns from ProviderSettings table + */ + public static interface ProviderSettingsColumns { + /** + * The id in database of the related provider + * + * <P>Type: INT</P> + */ + String PROVIDER = "provider"; + + /** + * The name of the setting + * <P>Type: TEXT</P> + */ + String NAME = "name"; + + /** + * The value of the setting + * <P>Type: TEXT</P> + */ + String VALUE = "value"; + } + + public static class ProviderSettings implements ProviderSettingsColumns { + private ProviderSettings() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/providerSettings"); + + /** + * The MIME type of {@link #CONTENT_URI} providing provider settings + */ + public static final String CONTENT_TYPE = "vnd.android-dir/im-providerSettings"; + + /** + * A boolean value to indicate whether this provider should show the offline contacts + */ + public static final String SHOW_OFFLINE_CONTACTS = "show_offline_contacts"; + + /** controls whether or not the GTalk service automatically connect to server. */ + public static final String SETTING_AUTOMATICALLY_CONNECT_GTALK = "gtalk_auto_connect"; + + /** controls whether or not the IM service will be automatically started after boot */ + public static final String SETTING_AUTOMATICALLY_START_SERVICE = "auto_start_service"; + + /** controls whether or not the offline contacts will be hided */ + public static final String SETTING_HIDE_OFFLINE_CONTACTS = "hide_offline_contacts"; + + /** controls whether or not enable the IM notification */ + public static final String SETTING_ENABLE_NOTIFICATION = "enable_notification"; + + /** specifies whether or not to vibrate */ + public static final String SETTING_VIBRATE = "vibrate"; + + /** specifies the Uri string of the ringtone */ + public static final String SETTING_RINGTONE = "ringtone"; + + /** specifies the Uri of the default ringtone */ + public static final String SETTING_RINGTONE_DEFAULT = + "content://settings/system/notification_sound"; + + /** specifies whether or not to show mobile indicator to friends */ + public static final String SETTING_SHOW_MOBILE_INDICATOR = "mobile_indicator"; + + /** + * Used for reliable message queue (RMQ). This is for storing the last rmq id received + * from the GTalk server + */ + public static final String LAST_RMQ_RECEIVED = "last_rmq_rec"; + + /** + * Query the settings of the provider specified by id + * + * @param cr + * the relative content resolver + * @param providerId + * the specified id of provider + * @return a HashMap which contains all the settings for the specified + * provider + */ + public static HashMap<String, String> queryProviderSettings(ContentResolver cr, + long providerId) { + HashMap<String, String> settings = new HashMap<String, String>(); + + String[] projection = { NAME, VALUE }; + Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), projection, null, null, null); + if (c == null) { + return null; + } + + while(c.moveToNext()) { + settings.put(c.getString(0), c.getString(1)); + } + + c.close(); + + return settings; + } + + /** + * Get the string value of setting which is specified by provider id and the setting name. + * + * @param cr The ContentResolver to use to access the settings table. + * @param providerId The id of the provider. + * @param settingName The name of the setting. + * @return The value of the setting if the setting exist, otherwise return null. + */ + public static String getStringValue(ContentResolver cr, long providerId, String settingName) { + String ret = null; + Cursor c = getSettingValue(cr, providerId, settingName); + if (c != null) { + ret = c.getString(0); + c.close(); + } + + return ret; + } + + /** + * Get the boolean value of setting which is specified by provider id and the setting name. + * + * @param cr The ContentResolver to use to access the settings table. + * @param providerId The id of the provider. + * @param settingName The name of the setting. + * @return The value of the setting if the setting exist, otherwise return false. + */ + public static boolean getBooleanValue(ContentResolver cr, long providerId, String settingName) { + boolean ret = false; + Cursor c = getSettingValue(cr, providerId, settingName); + if (c != null) { + ret = c.getInt(0) != 0; + c.close(); + } + return ret; + } + + private static Cursor getSettingValue(ContentResolver cr, long providerId, String settingName) { + Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), new String[]{VALUE}, NAME + "=?", + new String[]{settingName}, null); + if (c != null) { + if (!c.moveToFirst()) { + c.close(); + return null; + } + } + return c; + } + + /** + * Save a long value of setting in the table providerSetting. + * + * @param cr The ContentProvider used to access the providerSetting table. + * @param providerId The id of the provider. + * @param name The name of the setting. + * @param value The value of the setting. + */ + public static void putLongValue(ContentResolver cr, long providerId, String name, + long value) { + ContentValues v = new ContentValues(3); + v.put(PROVIDER, providerId); + v.put(NAME, name); + v.put(VALUE, value); + + cr.insert(CONTENT_URI, v); + } + + /** + * Save a boolean value of setting in the table providerSetting. + * + * @param cr The ContentProvider used to access the providerSetting table. + * @param providerId The id of the provider. + * @param name The name of the setting. + * @param value The value of the setting. + */ + public static void putBooleanValue(ContentResolver cr, long providerId, String name, + boolean value) { + ContentValues v = new ContentValues(3); + v.put(PROVIDER, providerId); + v.put(NAME, name); + v.put(VALUE, Boolean.toString(value)); + + cr.insert(CONTENT_URI, v); + } + + /** + * Save a string value of setting in the table providerSetting. + * + * @param cr The ContentProvider used to access the providerSetting table. + * @param providerId The id of the provider. + * @param name The name of the setting. + * @param value The value of the setting. + */ + public static void putStringValue(ContentResolver cr, long providerId, String name, + String value) { + ContentValues v = new ContentValues(3); + v.put(PROVIDER, providerId); + v.put(NAME, name); + v.put(VALUE, value); + + cr.insert(CONTENT_URI, v); + } + + /** + * A convenience method to set whether or not the GTalk service should be started + * automatically. + * + * @param contentResolver The ContentResolver to use to access the settings table + * @param autoConnect Whether the GTalk service should be started automatically. + */ + public static void setAutomaticallyConnectGTalk(ContentResolver contentResolver, + long providerId, boolean autoConnect) { + putBooleanValue(contentResolver, providerId, SETTING_AUTOMATICALLY_CONNECT_GTALK, + autoConnect); + } + + /** + * A convenience method to set whether or not the offline contacts should be hided + * + * @param contentResolver The ContentResolver to use to access the setting table + * @param hideOfflineContacts Whether the offline contacts should be hided + */ + public static void setHideOfflineContacts(ContentResolver contentResolver, + long providerId, boolean hideOfflineContacts) { + putBooleanValue(contentResolver, providerId, SETTING_HIDE_OFFLINE_CONTACTS, + hideOfflineContacts); + } + + /** + * A convenience method to set whether or not enable the IM notification. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param enable Whether enable the IM notification + */ + public static void setEnableNotification(ContentResolver contentResolver, long providerId, + boolean enable) { + putBooleanValue(contentResolver, providerId, SETTING_ENABLE_NOTIFICATION, enable); + } + + /** + * A convenience method to set whether or not to vibrate. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param vibrate Whether or not to vibrate + */ + public static void setVibrate(ContentResolver contentResolver, long providerId, + boolean vibrate) { + putBooleanValue(contentResolver, providerId, SETTING_VIBRATE, vibrate); + } + + /** + * A convenience method to set the Uri String of the ringtone. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param ringtoneUri The Uri String of the ringtone to be set. + */ + public static void setRingtoneURI(ContentResolver contentResolver, long providerId, + String ringtoneUri) { + putStringValue(contentResolver, providerId, SETTING_RINGTONE, ringtoneUri); + } + + /** + * A convenience method to set whether or not to show mobile indicator. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param showMobileIndicator Whether or not to show mobile indicator. + */ + public static void setShowMobileIndicator(ContentResolver contentResolver, long providerId, + boolean showMobileIndicator) { + putBooleanValue(contentResolver, providerId, SETTING_SHOW_MOBILE_INDICATOR, + showMobileIndicator); + } + + public static class QueryMap extends ContentQueryMap { + private ContentResolver mContentResolver; + private long mProviderId; + + public QueryMap(ContentResolver contentResolver, long providerId, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, + new String[] {NAME,VALUE}, + PROVIDER + "=" + providerId, + null, // no selection args + null), // no sort order + NAME, keepUpdated, handlerForUpdateNotifications); + mContentResolver = contentResolver; + mProviderId = providerId; + } + + /** + * Set if the GTalk service should automatically connect to server. + * + * @param autoConnect if the GTalk service should auto connect to server. + */ + public void setAutomaticallyConnectToGTalkServer(boolean autoConnect) { + ProviderSettings.setAutomaticallyConnectGTalk(mContentResolver, mProviderId, + autoConnect); + } + + /** + * Check if the GTalk service should automatically connect to server. + * @return if the GTalk service should automatically connect to server. + */ + public boolean getAutomaticallyConnectToGTalkServer() { + return getBoolean(SETTING_AUTOMATICALLY_CONNECT_GTALK, + true /* default to automatically sign in */); + } + + /** + * Set whether or not the offline contacts should be hided. + * + * @param hideOfflineContacts Whether or not the offline contacts should be hided. + */ + public void setHideOfflineContacts(boolean hideOfflineContacts) { + ProviderSettings.setHideOfflineContacts(mContentResolver, mProviderId, + hideOfflineContacts); + } + + /** + * Check if the offline contacts should be hided. + * + * @return Whether or not the offline contacts should be hided. + */ + public boolean getHideOfflineContacts() { + return getBoolean(SETTING_HIDE_OFFLINE_CONTACTS, + false/* by default not hide the offline contacts*/); + } + + /** + * Set whether or not enable the IM notification. + * + * @param enable Whether or not enable the IM notification. + */ + public void setEnableNotification(boolean enable) { + ProviderSettings.setEnableNotification(mContentResolver, mProviderId, enable); + } + + /** + * Check if the IM notification is enabled. + * + * @return Whether or not enable the IM notification. + */ + public boolean getEnableNotification() { + return getBoolean(SETTING_ENABLE_NOTIFICATION, + true/* by default enable the notification */); + } + + /** + * Set whether or not to vibrate on IM notification. + * + * @param vibrate Whether or not to vibrate. + */ + public void setVibrate(boolean vibrate) { + ProviderSettings.setVibrate(mContentResolver, mProviderId, vibrate); + } + + /** + * Gets whether or not to vibrate on IM notification. + * + * @return Whether or not to vibrate. + */ + public boolean getVibrate() { + return getBoolean(SETTING_VIBRATE, false /* by default disable vibrate */); + } + + /** + * Set the Uri for the ringtone. + * + * @param ringtoneUri The Uri of the ringtone to be set. + */ + public void setRingtoneURI(String ringtoneUri) { + ProviderSettings.setRingtoneURI(mContentResolver, mProviderId, ringtoneUri); + } + + /** + * Get the Uri String of the current ringtone. + * + * @return The Uri String of the current ringtone. + */ + public String getRingtoneURI() { + return getString(SETTING_RINGTONE, SETTING_RINGTONE_DEFAULT); + } + + /** + * Set whether or not to show mobile indicator to friends. + * + * @param showMobile whether or not to show mobile indicator. + */ + public void setShowMobileIndicator(boolean showMobile) { + ProviderSettings.setShowMobileIndicator(mContentResolver, mProviderId, showMobile); + } + + /** + * Gets whether or not to show mobile indicator. + * + * @return Whether or not to show mobile indicator. + */ + public boolean getShowMobileIndicator() { + return getBoolean(SETTING_SHOW_MOBILE_INDICATOR, + true /* by default show mobile indicator */); + } + + /** + * Convenience function for retrieving a single settings value + * as a boolean. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined. + */ + private boolean getBoolean(String name, boolean def) { + ContentValues values = getValues(name); + return values != null ? values.getAsBoolean(VALUE) : def; + } + + /** + * Convenience function for retrieving a single settings value + * as a String. + * + * @param name The name of the setting to retrieve. + * @param def The value to return if the setting is not defined. + * @return The setting's current value or 'def' if it is not defined. + */ + private String getString(String name, String def) { + ContentValues values = getValues(name); + return values != null ? values.getAsString(VALUE) : def; + } + + /** + * Convenience function for retrieving a single settings value + * as an Integer. + * + * @param name The name of the setting to retrieve. + * @param def The value to return if the setting is not defined. + * @return The setting's current value or 'def' if it is not defined. + */ + private int getInteger(String name, int def) { + ContentValues values = getValues(name); + return values != null ? values.getAsInteger(VALUE) : def; + } + } + + } + + /** + * Columns from OutgoingRmq table + */ + public interface OutgoingRmqColumns { + String RMQ_ID = "rmq_id"; + String TYPE = "type"; + String TIMESTAMP = "ts"; + String DATA = "data"; + } + + /** + * The table for storing outgoing rmq packets. + */ + public static final class OutgoingRmq implements BaseColumns, OutgoingRmqColumns { + private static String[] RMQ_ID_PROJECTION = new String[] { + RMQ_ID, + }; + + /** + * queryHighestRmqId + * + * @param resolver the content resolver + * @return the highest rmq id assigned to the rmq packet, or 0 if there are no rmq packets + * in the OutgoingRmq table. + */ + public static final long queryHighestRmqId(ContentResolver resolver) { + Cursor cursor = resolver.query(Im.OutgoingRmq.CONTENT_URI_FOR_HIGHEST_RMQ_ID, + RMQ_ID_PROJECTION, + null, // selection + null, // selection args + null // sort + ); + + long retVal = 0; + try { + //if (DBG) log("initializeRmqid: cursor.count= " + cursor.count()); + + if (cursor.moveToFirst()) { + retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID)); + } + } finally { + cursor.close(); + } + + return retVal; + } + + /** + * The content:// style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/outgoingRmqMessages"); + + /** + * The content:// style URL for the highest rmq id for the outgoing rmq messages + */ + public static final Uri CONTENT_URI_FOR_HIGHEST_RMQ_ID = + Uri.parse("content://im/outgoingHighestRmqId"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "rmq_id ASC"; + } + + /** + * Columns for the LastRmqId table, which stores a single row for the last client rmq id + * sent to the server. + */ + public interface LastRmqIdColumns { + String RMQ_ID = "rmq_id"; + } + + /** + * The table for storing the last client rmq id sent to the server. + */ + public static final class LastRmqId implements BaseColumns, LastRmqIdColumns { + private static String[] PROJECTION = new String[] { + RMQ_ID, + }; + + /** + * queryLastRmqId + * + * queries the last rmq id saved in the LastRmqId table. + * + * @param resolver the content resolver. + * @return the last rmq id stored in the LastRmqId table, or 0 if not found. + */ + public static final long queryLastRmqId(ContentResolver resolver) { + Cursor cursor = resolver.query(Im.LastRmqId.CONTENT_URI, + PROJECTION, + null, // selection + null, // selection args + null // sort + ); + + long retVal = 0; + try { + if (cursor.moveToFirst()) { + retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID)); + } + } finally { + cursor.close(); + } + + return retVal; + } + + /** + * saveLastRmqId + * + * saves the rmqId to the lastRmqId table. This will override the existing row if any, + * as we only keep one row of data in this table. + * + * @param resolver the content resolver. + * @param rmqId the rmq id to be saved. + */ + public static final void saveLastRmqId(ContentResolver resolver, long rmqId) { + ContentValues values = new ContentValues(); + + // always replace the first row. + values.put(_ID, 1); + values.put(RMQ_ID, rmqId); + resolver.insert(CONTENT_URI, values); + } + + /** + * The content:// style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/lastRmqId"); + } +} diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java new file mode 100644 index 0000000..d99ad36 --- /dev/null +++ b/core/java/android/provider/MediaStore.java @@ -0,0 +1,1224 @@ +/* + * Copyright (C) 2007 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 android.provider; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.ContentUris; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.text.Collator; + +/** + * The Media provider contains meta data for all available media on both internal + * and external storage devices. + */ +public final class MediaStore +{ + private final static String TAG = "MediaStore"; + + public static final String AUTHORITY = "media"; + + private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; + + /** + * Standard Intent action that can be sent to have the media application + * capture an image and return it. The image is returned as a Bitmap + * object in the extra field. + * @hide + */ + public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; + + /** + * Common fields for most MediaProvider tables + */ + + public interface MediaColumns extends BaseColumns { + /** + * The data stream for the file + * <P>Type: DATA STREAM</P> + */ + public static final String DATA = "_data"; + + /** + * The size of the file in bytes + * <P>Type: INTEGER (long)</P> + */ + public static final String SIZE = "_size"; + + /** + * The display name of the file + * <P>Type: TEXT</P> + */ + public static final String DISPLAY_NAME = "_display_name"; + + /** + * The title of the content + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * The time the file was added to the media provider + * Units are seconds since 1970. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_ADDED = "date_added"; + + /** + * The time the file was last modified + * Units are seconds since 1970. + * NOTE: This is for internal use by the media scanner. Do not modify this field. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_MODIFIED = "date_modified"; + + /** + * The MIME type of the file + * <P>Type: TEXT</P> + */ + public static final String MIME_TYPE = "mime_type"; + } + + /** + * Contains meta data for all available images. + */ + public static final class Images + { + public interface ImageColumns extends MediaColumns { + /** + * The description of the image + * <P>Type: TEXT</P> + */ + public static final String DESCRIPTION = "description"; + + /** + * The picasa id of the image + * <P>Type: TEXT</P> + */ + public static final String PICASA_ID = "picasa_id"; + + /** + * Whether the video should be published as public or private + * <P>Type: INTEGER</P> + */ + public static final String IS_PRIVATE = "isprivate"; + + /** + * The latitude where the image was captured. + * <P>Type: DOUBLE</P> + */ + public static final String LATITUDE = "latitude"; + + /** + * The longitude where the image was captured. + * <P>Type: DOUBLE</P> + */ + public static final String LONGITUDE = "longitude"; + + /** + * The date & time that the image was taken in units + * of milliseconds since jan 1, 1970. + * <P>Type: INTEGER</P> + */ + public static final String DATE_TAKEN = "datetaken"; + + /** + * The orientation for the image expressed as degrees. + * Only degrees 0, 90, 180, 270 will work. + * <P>Type: INTEGER</P> + */ + public static final String ORIENTATION = "orientation"; + + /** + * The mini thumb id. + * <P>Type: INTEGER</P> + */ + public static final String MINI_THUMB_MAGIC = "mini_thumb_magic"; + + /** + * The bucket id of the image + * <P>Type: TEXT</P> + */ + public static final String BUCKET_ID = "bucket_id"; + + /** + * The bucket display name of the image + * <P>Type: TEXT</P> + */ + public static final String BUCKET_DISPLAY_NAME = "bucket_display_name"; + } + + public static final class Media implements ImageColumns { + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) + { + return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, + String where, String orderBy) + { + return cr.query(uri, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, + String selection, String [] selectionArgs, String orderBy) + { + return cr.query(uri, projection, selection, + selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * Retrieves an image for the given url as a {@link Bitmap}. + * + * @param cr The content resolver to use + * @param url The url of the image + * @throws FileNotFoundException + * @throws IOException + */ + public static final Bitmap getBitmap(ContentResolver cr, Uri url) + throws FileNotFoundException, IOException + { + InputStream input = cr.openInputStream(url); + Bitmap bitmap = BitmapFactory.decodeStream(input); + input.close(); + return bitmap; + } + + /** + * Insert an image and create a thumbnail for it. + * + * @param cr The content resolver to use + * @param imagePath The path to the image to insert + * @param name The name of the image + * @param description The description of the image + * @return The URL to the newly created image + * @throws FileNotFoundException + */ + public static final String insertImage(ContentResolver cr, String imagePath, String name, + String description) throws FileNotFoundException + { + // Check if file exists with a FileInputStream + FileInputStream stream = new FileInputStream(imagePath); + try { + return insertImage(cr, BitmapFactory.decodeFile(imagePath), name, description); + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + private static final Bitmap StoreThumbnail( + ContentResolver cr, + Bitmap source, + long id, + float width, float height, + int kind) { + // create the matrix to scale it + Matrix matrix = new Matrix(); + + float scaleX = width / source.getWidth(); + float scaleY = height / source.getHeight(); + + matrix.setScale(scaleX, scaleY); + + Bitmap thumb = Bitmap.createBitmap(source, 0, 0, + source.getWidth(), + source.getHeight(), matrix, + true); + + ContentValues values = new ContentValues(4); + values.put(Images.Thumbnails.KIND, kind); + values.put(Images.Thumbnails.IMAGE_ID, (int)id); + values.put(Images.Thumbnails.HEIGHT, thumb.getHeight()); + values.put(Images.Thumbnails.WIDTH, thumb.getWidth()); + + Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values); + + try { + OutputStream thumbOut = cr.openOutputStream(url); + + thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut); + thumbOut.close(); + return thumb; + } + catch (FileNotFoundException ex) { + return null; + } + catch (IOException ex) { + return null; + } + } + + /** + * Insert an image and create a thumbnail for it. + * + * @param cr The content resolver to use + * @param source The stream to use for the image + * @param title The name of the image + * @param description The description of the image + * @return The URL to the newly created image, or <code>null</code> if the image failed to be stored + * for any reason. + */ + public static final String insertImage(ContentResolver cr, Bitmap source, + String title, String description) + { + ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, title); + values.put(Images.Media.DESCRIPTION, description); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + + Uri url = null; + String stringUrl = null; /* value to be returned */ + + try + { + url = cr.insert(EXTERNAL_CONTENT_URI, values); + + if (source != null) { + OutputStream imageOut = cr.openOutputStream(url); + try { + source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut); + } finally { + imageOut.close(); + } + + long id = ContentUris.parseId(url); + Bitmap miniThumb = StoreThumbnail(cr, source, id, 320F, 240F, Images.Thumbnails.MINI_KIND); + Bitmap microThumb = StoreThumbnail(cr, miniThumb, id, 50F, 50F, Images.Thumbnails.MICRO_KIND); + } else { + Log.e(TAG, "Failed to create thumbnail, removing original"); + cr.delete(url, null, null); + url = null; + } + } catch (Exception e) { + Log.e(TAG, "Failed to insert image", e); + if (url != null) { + cr.delete(url, null, null); + url = null; + } + } + + if (url != null) { + stringUrl = url.toString(); + } + + return stringUrl; + } + + /** + * Get the content:// style URI for the image media table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the image media table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/images/media"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type of of this directory of + * images. Note that each entry in this directory will have a standard + * image MIME type as appropriate -- for example, image/jpeg. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + } + + public static class Thumbnails implements BaseColumns + { + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) + { + return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, String[] projection) + { + return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, String[] projection) + { + return cr.query(EXTERNAL_CONTENT_URI, projection, + IMAGE_ID + " = " + origId + " AND " + KIND + " = " + + kind, null, null); + } + + /** + * Get the content:// style URI for the image media table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the image media table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/images/thumbnails"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "image_id ASC"; + + /** + * The data stream for the thumbnail + * <P>Type: DATA STREAM</P> + */ + public static final String DATA = "_data"; + + /** + * The original image for the thumbnal + * <P>Type: INTEGER (ID from Images table)</P> + */ + public static final String IMAGE_ID = "image_id"; + + /** + * The kind of the thumbnail + * <P>Type: INTEGER (One of the values below)</P> + */ + public static final String KIND = "kind"; + + public static final int MINI_KIND = 1; + public static final int FULL_SCREEN_KIND = 2; + public static final int MICRO_KIND = 3; + + /** + * The width of the thumbnal + * <P>Type: INTEGER (long)</P> + */ + public static final String WIDTH = "width"; + + /** + * The height of the thumbnail + * <P>Type: INTEGER (long)</P> + */ + public static final String HEIGHT = "height"; + } + } + + /** + * Container for all audio content. + */ + public static final class Audio { + /** + * Columns for audio file that show up in multiple tables. + */ + public interface AudioColumns extends MediaColumns { + + /** + * A non human readable key calculated from the TITLE, used for + * searching, sorting and grouping + * <P>Type: TEXT</P> + */ + public static final String TITLE_KEY = "title_key"; + + /** + * The duration of the audio file, in ms + * <P>Type: INTEGER (long)</P> + */ + public static final String DURATION = "duration"; + + /** + * The id of the artist who created the audio file, if any + * <P>Type: INTEGER (long)</P> + */ + public static final String ARTIST_ID = "artist_id"; + + /** + * The artist who created the audio file, if any + * <P>Type: TEXT</P> + */ + public static final String ARTIST = "artist"; + + /** + * A non human readable key calculated from the ARTIST, used for + * searching, sorting and grouping + * <P>Type: TEXT</P> + */ + public static final String ARTIST_KEY = "artist_key"; + + /** + * The composer of the audio file, if any + * <P>Type: TEXT</P> + */ + public static final String COMPOSER = "composer"; + + /** + * The id of the album the audio file is from, if any + * <P>Type: INTEGER (long)</P> + */ + public static final String ALBUM_ID = "album_id"; + + /** + * The album the audio file is from, if any + * <P>Type: TEXT</P> + */ + public static final String ALBUM = "album"; + + /** + * A non human readable key calculated from the ALBUM, used for + * searching, sorting and grouping + * <P>Type: TEXT</P> + */ + public static final String ALBUM_KEY = "album_key"; + + /** + * A URI to the album art, if any + * <P>Type: TEXT</P> + */ + public static final String ALBUM_ART = "album_art"; + + /** + * The track number of this song on the album, if any. + * This number encodes both the track number and the + * disc number. For multi-disc sets, this number will + * be 1xxx for tracks on the first disc, 2xxx for tracks + * on the second disc, etc. + * <P>Type: INTEGER</P> + */ + public static final String TRACK = "track"; + + /** + * The year the audio file was recorded, if any + * <P>Type: INTEGER</P> + */ + public static final String YEAR = "year"; + + /** + * Non-zero if the audio file is music + * <P>Type: INTEGER (boolean)</P> + */ + public static final String IS_MUSIC = "is_music"; + + /** + * Non-zero id the audio file may be a ringtone + * <P>Type: INTEGER (boolean)</P> + */ + public static final String IS_RINGTONE = "is_ringtone"; + + /** + * Non-zero id the audio file may be an alarm + * <P>Type: INTEGER (boolean)</P> + */ + public static final String IS_ALARM = "is_alarm"; + + /** + * Non-zero id the audio file may be a notification sound + * <P>Type: INTEGER (boolean)</P> + */ + public static final String IS_NOTIFICATION = "is_notification"; + } + + /** + * Converts a name to a "key" that can be used for grouping, sorting + * and searching. + * The rules that govern this conversion are: + * - remove 'special' characters like ()[]'!?., + * - remove leading/trailing spaces + * - convert everything to lowercase + * - remove leading "the ", "an " and "a " + * - remove trailing ", the|an|a" + * - remove accents. This step leaves us with CollationKey data, + * which is not human readable + * + * @param name The artist or album name to convert + * @return The "key" for the given name. + */ + public static String keyFor(String name) { + if (name != null) { + if (name.equals(android.media.MediaFile.UNKNOWN_STRING)) { + return "\001"; + } + name = name.trim().toLowerCase(); + if (name.startsWith("the ")) { + name = name.substring(4); + } + if (name.startsWith("an ")) { + name = name.substring(3); + } + if (name.startsWith("a ")) { + name = name.substring(2); + } + if (name.endsWith(", the") || name.endsWith(",the") || + name.endsWith(", an") || name.endsWith(",an") || + name.endsWith(", a") || name.endsWith(",a")) { + name = name.substring(0, name.lastIndexOf(',')); + } + name = name.replaceAll("[\\[\\]\\(\\)'.,?!]", "").trim(); + if (name.length() > 0) { + // Insert a separator between the characters to avoid + // matches on a partial character. If we ever change + // to start-of-word-only matches, this can be removed. + StringBuilder b = new StringBuilder(); + b.append('.'); + int nl = name.length(); + for (int i = 0; i < nl; i++) { + b.append(name.charAt(i)); + b.append('.'); + } + name = b.toString(); + return DatabaseUtils.getCollationKey(name); + } else { + return ""; + } + } + return null; + } + + public static final class Media implements AudioColumns { + /** + * Get the content:// style URI for the audio media table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the audio media table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/media"); + } + + public static Uri getContentUriForPath(String path) { + return (path.startsWith(Environment.getExternalStorageDirectory().getPath()) ? + EXTERNAL_CONTENT_URI : INTERNAL_CONTENT_URI); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = TITLE; + + /** + * Activity Action: Start SoundRecorder application. + * <p>Input: nothing. + * <p>Output: An uri to the recorded sound stored in the Media Library + * if the recording was successful. + * + */ + public static final String RECORD_SOUND_ACTION = + "android.provider.MediaStore.RECORD_SOUND"; + } + + /** + * Columns representing an audio genre + */ + public interface GenresColumns { + /** + * The name of the genre + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + } + + /** + * Contains all genres for audio files + */ + public static final class Genres implements BaseColumns, GenresColumns { + /** + * Get the content:// style URI for the audio genres table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the audio genres table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/genres"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre"; + + /** + * The MIME type for entries in this table. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = NAME; + + /** + * Sub-directory of each genre containing all members. + */ + public static final class Members implements AudioColumns { + + public static final Uri getContentUri(String volumeName, + long genreId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/genres/" + genreId + "/members"); + } + + /** + * A subdirectory of each genre containing all member audio files. + */ + public static final String CONTENT_DIRECTORY = "members"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = TITLE; + + /** + * The ID of the audio file + * <P>Type: INTEGER (long)</P> + */ + public static final String AUDIO_ID = "audio_id"; + + /** + * The ID of the genre + * <P>Type: INTEGER (long)</P> + */ + public static final String GENRE_ID = "genre_id"; + } + } + + /** + * Columns representing a playlist + */ + public interface PlaylistsColumns { + /** + * The name of the playlist + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The data stream for the playlist file + * <P>Type: DATA STREAM</P> + */ + public static final String DATA = "_data"; + + /** + * The time the file was added to the media provider + * Units are seconds since 1970. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_ADDED = "date_added"; + + /** + * The time the file was last modified + * Units are seconds since 1970. + * NOTE: This is for internal use by the media scanner. Do not modify this field. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_MODIFIED = "date_modified"; + } + + /** + * Contains playlists for audio files + */ + public static final class Playlists implements BaseColumns, + PlaylistsColumns { + /** + * Get the content:// style URI for the audio playlists table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the audio playlists table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/playlists"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist"; + + /** + * The MIME type for entries in this table. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = NAME; + + /** + * Sub-directory of each playlist containing all members. + */ + public static final class Members implements AudioColumns { + public static final Uri getContentUri(String volumeName, + long playlistId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/playlists/" + playlistId + "/members"); + } + + /** + * The ID within the playlist. + */ + public static final String _ID = "_id"; + + /** + * A subdirectory of each playlist containing all member audio + * files. + */ + public static final String CONTENT_DIRECTORY = "members"; + + /** + * The ID of the audio file + * <P>Type: INTEGER (long)</P> + */ + public static final String AUDIO_ID = "audio_id"; + + /** + * The ID of the playlist + * <P>Type: INTEGER (long)</P> + */ + public static final String PLAYLIST_ID = "playlist_id"; + + /** + * The order of the songs in the playlist + * <P>Type: INTEGER (long)></P> + */ + public static final String PLAY_ORDER = "play_order"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = PLAY_ORDER; + } + } + + /** + * Columns representing an artist + */ + public interface ArtistColumns { + /** + * The artist who created the audio file, if any + * <P>Type: TEXT</P> + */ + public static final String ARTIST = "artist"; + + /** + * A non human readable key calculated from the ARTIST, used for + * searching, sorting and grouping + * <P>Type: TEXT</P> + */ + public static final String ARTIST_KEY = "artist_key"; + + /** + * The number of albums in the database for this artist + */ + public static final String NUMBER_OF_ALBUMS = "number_of_albums"; + + /** + * The number of albums in the database for this artist + */ + public static final String NUMBER_OF_TRACKS = "number_of_tracks"; + } + + /** + * Contains artists for audio files + */ + public static final class Artists implements BaseColumns, ArtistColumns { + /** + * Get the content:// style URI for the artists table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the audio artists table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/artists"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists"; + + /** + * The MIME type for entries in this table. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = ARTIST_KEY; + + /** + * Sub-directory of each artist containing all albums on which + * a song by the artist appears. + */ + public static final class Albums implements AlbumColumns { + public static final Uri getContentUri(String volumeName, + long artistId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/artists/" + artistId + "/albums"); + } + } + } + + /** + * Columns representing an album + */ + public interface AlbumColumns { + + /** + * The id for the album + * <P>Type: INTEGER</P> + */ + public static final String ALBUM_ID = "album_id"; + + /** + * The album on which the audio file appears, if any + * <P>Type: TEXT</P> + */ + public static final String ALBUM = "album"; + + /** + * The artist whose songs appear on this album + * <P>Type: TEXT</P> + */ + public static final String ARTIST = "artist"; + + /** + * The number of songs on this album + * <P>Type: INTEGER</P> + */ + public static final String NUMBER_OF_SONGS = "numsongs"; + + /** + * The year in which the earliest and latest songs + * on this album were released. These will often + * be the same, but for compilation albums they + * might differ. + * <P>Type: INTEGER</P> + */ + public static final String FIRST_YEAR = "minyear"; + public static final String LAST_YEAR = "maxyear"; + + /** + * A non human readable key calculated from the ALBUM, used for + * searching, sorting and grouping + * <P>Type: TEXT</P> + */ + public static final String ALBUM_KEY = "album_key"; + + /** + * Cached album art. + * <P>Type: TEXT</P> + */ + public static final String ALBUM_ART = "album_art"; + } + + /** + * Contains artists for audio files + */ + public static final class Albums implements BaseColumns, AlbumColumns { + /** + * Get the content:// style URI for the albums table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the audio albums table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/audio/albums"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums"; + + /** + * The MIME type for entries in this table. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = ALBUM_KEY; + } + } + + public static final class Video { + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) + { + return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); + } + + public interface VideoColumns extends MediaColumns { + + /** + * The duration of the video file, in ms + * <P>Type: INTEGER (long)</P> + */ + public static final String DURATION = "duration"; + + /** + * The artist who created the video file, if any + * <P>Type: TEXT</P> + */ + public static final String ARTIST = "artist"; + + /** + * The album the video file is from, if any + * <P>Type: TEXT</P> + */ + public static final String ALBUM = "album"; + + /** + * The resolution of the video file, formatted as "XxY" + * <P>Type: TEXT</P> + */ + public static final String RESOLUTION = "resolution"; + + /** + * The description of the video recording + * <P>Type: TEXT</P> + */ + public static final String DESCRIPTION = "description"; + + /** + * Whether the video should be published as public or private + * <P>Type: INTEGER</P> + */ + public static final String IS_PRIVATE = "isprivate"; + + /** + * The user-added tags associated with a video + * <P>Type: TEXT</P> + */ + public static final String TAGS = "tags"; + + /** + * The YouTube category of the video + * <P>Type: TEXT</P> + */ + public static final String CATEGORY = "category"; + + /** + * The language of the video + * <P>Type: TEXT</P> + */ + public static final String LANGUAGE = "language"; + + /** + * The latitude where the image was captured. + * <P>Type: DOUBLE</P> + */ + public static final String LATITUDE = "latitude"; + + /** + * The longitude where the image was captured. + * <P>Type: DOUBLE</P> + */ + public static final String LONGITUDE = "longitude"; + + /** + * The date & time that the image was taken in units + * of milliseconds since jan 1, 1970. + * <P>Type: INTEGER</P> + */ + public static final String DATE_TAKEN = "datetaken"; + + /** + * The mini thumb id. + * <P>Type: INTEGER</P> + */ + public static final String MINI_THUMB_MAGIC = "mini_thumb_magic"; + } + + public static final class Media implements VideoColumns { + /** + * Get the content:// style URI for the video media table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the video media table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/video/media"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The MIME type for this table. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = TITLE; + } + } + + /** + * Uri for querying the state of the media scanner. + */ + public static Uri getMediaScannerUri() { + return Uri.parse(CONTENT_AUTHORITY_SLASH + "none/media_scanner"); + } + + /** + * Name of current volume being scanned by the media scanner. + */ + public static final String MEDIA_SCANNER_VOLUME = "volume"; +} diff --git a/core/java/android/provider/OpenableColumns.java b/core/java/android/provider/OpenableColumns.java new file mode 100644 index 0000000..f548bae --- /dev/null +++ b/core/java/android/provider/OpenableColumns.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2007 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 android.provider; + +/** + * These are standard columns for openable URIs. (See + * {@link android.content.Intent#CATEGORY_OPENABLE}.) If possible providers that have openable URIs + * should support these columns. To find the content type of a URI use + * {@link android.content.ContentResolver#getType(android.net.Uri)} as normal. + */ +public interface OpenableColumns { + + /** + * The human-friendly name of file. If this is not provided then the name should default to the + * the last segment of the file's URI. + */ + public static final String DISPLAY_NAME = "_display_name"; + + /** + * The number of bytes in the file identified by the openable URI. Null if unknown. + */ + public static final String SIZE = "_size"; +} diff --git a/core/java/android/provider/SearchRecentSuggestions.java b/core/java/android/provider/SearchRecentSuggestions.java new file mode 100644 index 0000000..1439b26 --- /dev/null +++ b/core/java/android/provider/SearchRecentSuggestions.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SearchRecentSuggestionsProvider; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +/** + * This is a utility class providing access to + * {@link android.content.SearchRecentSuggestionsProvider}. + * + * <p>Unlike some utility classes, this one must be instantiated and properly initialized, so that + * it can be configured to operate with the search suggestions provider that you have created. + * + * <p>Typically, you will do this in your searchable activity, each time you receive an incoming + * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent. The code to record each + * incoming query is as follows: + * <pre class="prettyprint"> + * SearchSuggestions suggestions = new SearchSuggestions(this, + * MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE); + * suggestions.saveRecentQuery(queryString, null); + * </pre> + * + * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in + * samples/ApiDemos/app. + */ +public class SearchRecentSuggestions { + // debugging support + private static final String LOG_TAG = "SearchSuggestions"; + // DELETE ME (eventually) + private static final int DBG_SUGGESTION_TIMESTAMPS = 0; + + // This is a superset of all possible column names (need not all be in table) + private static class SuggestionColumns implements BaseColumns { + public static final String DISPLAY1 = "display1"; + public static final String DISPLAY2 = "display2"; + public static final String QUERY = "query"; + public static final String DATE = "date"; + } + + /* if you change column order you must also change indices below */ + /** + * This is the database projection that can be used to view saved queries, when + * configured for one-line operation. + */ + public static final String[] QUERIES_PROJECTION_1LINE = new String[] { + SuggestionColumns._ID, + SuggestionColumns.DATE, + SuggestionColumns.QUERY, + SuggestionColumns.DISPLAY1, + }; + /* if you change column order you must also change indices below */ + /** + * This is the database projection that can be used to view saved queries, when + * configured for two-line operation. + */ + public static final String[] QUERIES_PROJECTION_2LINE = new String[] { + SuggestionColumns._ID, + SuggestionColumns.DATE, + SuggestionColumns.QUERY, + SuggestionColumns.DISPLAY1, + SuggestionColumns.DISPLAY2, + }; + + /* these indices depend on QUERIES_PROJECTION_xxx */ + /** Index into the provided query projections. For use with Cursor.update methods. */ + public static final int QUERIES_PROJECTION_DATE_INDEX = 1; + /** Index into the provided query projections. For use with Cursor.update methods. */ + public static final int QUERIES_PROJECTION_QUERY_INDEX = 2; + /** Index into the provided query projections. For use with Cursor.update methods. */ + public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3; + /** Index into the provided query projections. For use with Cursor.update methods. */ + public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4; // only when 2line active + + /* columns needed to determine whether to truncate history */ + private static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { + SuggestionColumns._ID, SuggestionColumns.DATE + }; + + /* + * Set a cap on the count of items in the suggestions table, to + * prevent db and layout operations from dragging to a crawl. Revisit this + * cap when/if db/layout performance improvements are made. + */ + private static final int MAX_HISTORY_COUNT = 250; + + // client-provided configuration values + private Context mContext; + private String mAuthority; + private boolean mTwoLineDisplay; + private Uri mSuggestionsUri; + private String[] mQueriesProjection; + + /** + * Although provider utility classes are typically static, this one must be constructed + * because it needs to be initialized using the same values that you provided in your + * {@link android.content.SearchRecentSuggestionsProvider}. + * + * @param authority This must match the authority that you've declared in your manifest. + * @param mode You can use mode flags here to determine certain functional aspects of your + * database. Note, this value should not change from run to run, because when it does change, + * your suggestions database may be wiped. + * + * @see android.content.SearchRecentSuggestionsProvider + * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions + */ + public SearchRecentSuggestions(Context context, String authority, int mode) { + if (TextUtils.isEmpty(authority) || + ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) { + throw new IllegalArgumentException(); + } + // unpack mode flags + mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES)); + + // saved values + mContext = context; + mAuthority = new String(authority); + + // derived values + mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); + + if (mTwoLineDisplay) { + mQueriesProjection = QUERIES_PROJECTION_2LINE; + } else { + mQueriesProjection = QUERIES_PROJECTION_1LINE; + } + } + + /** + * Add a query to the recent queries list. + * + * @param queryString The string as typed by the user. This string will be displayed as + * the suggestion, and if the user clicks on the suggestion, this string will be sent to your + * searchable activity (as a new search query). + * @param line2 If you have configured your recent suggestions provider with + * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can + * pass a second line of text here. It will be shown in a smaller font, below the primary + * suggestion. When typing, matches in either line of text will be displayed in the list. + * If you did not configure two-line mode, or if a given suggestion does not have any + * additional text to display, you can pass null here. + */ + public void saveRecentQuery(String queryString, String line2) { + if (TextUtils.isEmpty(queryString)) { + return; + } + if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) { + throw new IllegalArgumentException(); + } + + ContentResolver cr = mContext.getContentResolver(); + long now = System.currentTimeMillis(); + + // Use content resolver (not cursor) to insert/update this query + try { + ContentValues values = new ContentValues(); + values.put(SuggestionColumns.DISPLAY1, queryString); + if (mTwoLineDisplay) { + values.put(SuggestionColumns.DISPLAY2, line2); + } + values.put(SuggestionColumns.QUERY, queryString); + values.put(SuggestionColumns.DATE, now); + cr.insert(mSuggestionsUri, values); + } catch (RuntimeException e) { + Log.e(LOG_TAG, "saveRecentQuery", e); + } + + // Shorten the list (if it has become too long) + truncateHistory(cr, MAX_HISTORY_COUNT); + } + + /** + * Completely delete the history. Use this call to implement a "clear history" UI. + */ + public void clearHistory() { + ContentResolver cr = mContext.getContentResolver(); + truncateHistory(cr, 0); + } + + /** + * Reduces the length of the history table, to prevent it from growing too large. + * + * @param cr Convenience copy of the content resolver. + * @param maxEntries Max entries to leave in the table. 0 means remove all entries. + */ + protected void truncateHistory(ContentResolver cr, int maxEntries) { + if (maxEntries < 0) { + throw new IllegalArgumentException(); + } + + try { + // null means "delete all". otherwise "delete but leave n newest" + String selection = null; + if (maxEntries > 0) { + selection = "_id IN " + + "(SELECT _id FROM suggestions" + + " ORDER BY " + SuggestionColumns.DATE + " DESC" + + " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")"; + } + cr.delete(mSuggestionsUri, selection, null); + } catch (RuntimeException e) { + Log.e(LOG_TAG, "truncateHistory", e); + } + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java new file mode 100644 index 0000000..6897bd5 --- /dev/null +++ b/core/java/android/provider/Settings.java @@ -0,0 +1,2073 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import com.google.android.collect.Maps; + +import org.apache.commons.codec.binary.Base64; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.SQLException; +import android.net.Uri; +import android.os.*; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.AndroidException; +import android.util.Log; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; + + +/** + * The Settings provider contains global system-level device preferences. + */ +public final class Settings { + + // Intent actions for Settings + + /** + * Activity Action: Show system settings. + * <p> + * Input: Nothing. + * <p> + * Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SETTINGS = "android.settings.SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of APNs. + * <p> + * Input: Nothing. + * <p> + * Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_APN_SETTINGS = "android.settings.APN_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of current location + * sources. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_LOCATION_SOURCE_SETTINGS = + "android.settings.LOCATION_SOURCE_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of wireless controls + * such as Wi-Fi, Bluetooth and Mobile networks. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_WIRELESS_SETTINGS = + "android.settings.WIRELESS_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of security and + * location privacy. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SECURITY_SETTINGS = + "android.settings.SECURITY_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of Wi-Fi. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_WIFI_SETTINGS = + "android.settings.WIFI_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of Bluetooth. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_BLUETOOTH_SETTINGS = + "android.settings.BLUETOOTH_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of date and time. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_DATE_SETTINGS = + "android.settings.DATE_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of sound and volume. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SOUND_SETTINGS = + "android.settings.SOUND_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of display. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_DISPLAY_SETTINGS = + "android.settings.DISPLAY_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of locale. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_LOCALE_SETTINGS = + "android.settings.LOCALE_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of application-related settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_APPLICATION_SETTINGS = + "android.settings.APPLICATION_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of sync settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SYNC_SETTINGS = + "android.settings.SYNC_SETTINGS"; + + // End of Intent actions for Settings + + private static final String JID_RESOURCE_PREFIX = "android"; + + public static final String AUTHORITY = "settings"; + + private static final String TAG = "Settings"; + + private static String sJidResource = null; + + public static class SettingNotFoundException extends AndroidException { + public SettingNotFoundException(String msg) { + super(msg); + } + } + + /** + * Common base for tables of name/value settings. + * + * + */ + public static class NameValueTable implements BaseColumns { + public static final String NAME = "name"; + public static final String VALUE = "value"; + + protected static boolean putString(ContentResolver resolver, Uri uri, + String name, String value) { + // The database will take care of replacing duplicates. + try { + ContentValues values = new ContentValues(); + values.put(NAME, name); + values.put(VALUE, value); + resolver.insert(uri, values); + return true; + } catch (SQLException e) { + Log.e(TAG, "Can't set key " + name + " in " + uri, e); + return false; + } + } + + public static Uri getUriFor(Uri uri, String name) { + return Uri.withAppendedPath(uri, name); + } + } + + private static class NameValueCache { + private final String mVersionSystemProperty; + private final HashMap<String, String> mValues = Maps.newHashMap(); + private long mValuesVersion = 0; + private final Uri mUri; + + NameValueCache(String versionSystemProperty, Uri uri) { + mVersionSystemProperty = versionSystemProperty; + mUri = uri; + } + + String getString(ContentResolver cr, String name) { + long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0); + if (mValuesVersion != newValuesVersion) { + mValues.clear(); + mValuesVersion = newValuesVersion; + } + if (!mValues.containsKey(name)) { + String value = null; + Cursor c = null; + try { + c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, + Settings.NameValueTable.NAME + "=?", new String[]{name}, null); + if (c.moveToNext()) value = c.getString(0); + mValues.put(name, value); + } catch (SQLException e) { + // SQL error: return null, but don't cache it. + Log.e(TAG, "Can't get key " + name + " from " + mUri, e); + } finally { + if (c != null) c.close(); + } + return value; + } else { + return mValues.get(name); + } + } + } + + /** + * System settings, containing miscellaneous system preferences. This + * table holds simple name/value pairs. There are convenience + * functions for accessing individual settings entries. + */ + public static final class System extends NameValueTable { + public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version"; + + private static volatile NameValueCache mNameValueCache = null; + + /** + * Look up a name in the database. + * @param resolver to access the database with + * @param name to look up in the table + * @return the corresponding value, or null if not present + */ + public synchronized static String getString(ContentResolver resolver, String name) { + if (mNameValueCache == null) { + mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); + } + return mNameValueCache.getString(resolver, name); + } + + /** + * Store a name/value pair into the database. + * @param resolver to access the database with + * @param name to store + * @param value to associate with the name + * @return true if the value was set, false on database errors + */ + public static boolean putString(ContentResolver resolver, + String name, String value) { + return putString(resolver, CONTENT_URI, name, value); + } + + /** + * Construct the content URI for a particular name/value pair, + * useful for monitoring changes with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI, or null if not present + */ + public static Uri getUriFor(String name) { + return getUriFor(CONTENT_URI, name); + } + + /** + * Convenience function for retrieving a single system settings value + * as an integer. Note that internally setting values are always + * stored as strings; this function converts the string to an integer + * for you. The default value will be returned if the setting is + * not defined or not an integer. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid integer. + */ + public static int getInt(ContentResolver cr, String name, int def) { + String v = getString(cr, name); + try { + return v != null ? Integer.parseInt(v) : def; + } catch (NumberFormatException e) { + return def; + } + } + + /** + * Convenience function for retrieving a single system settings value + * as an integer. Note that internally setting values are always + * stored as strings; this function converts the string to an integer + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not an integer. + * + * @return The setting's current value. + */ + public static int getInt(ContentResolver cr, String name) + throws SettingNotFoundException { + String v = getString(cr, name); + try { + return Integer.parseInt(v); + } catch (NumberFormatException e) { + throw new SettingNotFoundException(name); + } + } + + /** + * Convenience function for updating a single settings value as an + * integer. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + public static boolean putInt(ContentResolver cr, String name, int value) { + return putString(cr, name, Integer.toString(value)); + } + + /** + * Convenience function for retrieving a single system settings value + * as a floating point number. Note that internally setting values are + * always stored as strings; this function converts the string to an + * float for you. The default value will be returned if the setting + * is not defined or not a valid float. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid float. + */ + public static float getFloat(ContentResolver cr, String name, float def) { + String v = getString(cr, name); + try { + return v != null ? Float.parseFloat(v) : def; + } catch (NumberFormatException e) { + return def; + } + } + + /** + * Convenience function for retrieving a single system settings value + * as a float. Note that internally setting values are always + * stored as strings; this function converts the string to a float + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not a float. + * + * @return The setting's current value. + */ + public static float getFloat(ContentResolver cr, String name) + throws SettingNotFoundException { + String v = getString(cr, name); + try { + return Float.parseFloat(v); + } catch (NumberFormatException e) { + throw new SettingNotFoundException(name); + } + } + + /** + * Convenience function for updating a single settings value as a + * floating point number. This will either create a new entry in the + * table if the given name does not exist, or modify the value of the + * existing row with that name. Note that internally setting values + * are always stored as strings, so this function converts the given + * value to a string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + public static boolean putFloat(ContentResolver cr, String name, float value) { + return putString(cr, name, Float.toString(value)); + } + + /** + * Convenience function to read all of the current + * configuration-related settings into a + * {@link Configuration} object. + * + * @param cr The ContentResolver to access. + * @param outConfig Where to place the configuration settings. + */ + public static void getConfiguration(ContentResolver cr, Configuration outConfig) { + outConfig.fontScale = Settings.System.getFloat( + cr, FONT_SCALE, outConfig.fontScale); + if (outConfig.fontScale < 0) { + outConfig.fontScale = 1; + } + } + + /** + * Convenience function to write a batch of configuration-related + * settings from a {@link Configuration} object. + * + * @param cr The ContentResolver to access. + * @param config The settings to write. + * @return true if the values were set, false on database errors + */ + public static boolean putConfiguration(ContentResolver cr, Configuration config) { + return Settings.System.putFloat(cr, FONT_SCALE, config.fontScale); + } + + public static boolean getShowGTalkServiceStatus(ContentResolver cr) { + return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0; + } + + public static void setShowGTalkServiceStatus(ContentResolver cr, boolean flag) { + putInt(cr, SHOW_GTALK_SERVICE_STATUS, flag ? 1 : 0); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/system"); + + /** + * Whether we keep the device on while the device is plugged in. + * 0=no 1=yes + */ + public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in"; + + /** + * What happens when the user presses the end call button if they're not + * on a call.<br/> + * <b>Values:</b><br/> + * 0 - The end button does nothing.<br/> + * 1 - The end button goes to the home screen.<br/> + * 2 - The end button puts the device to sleep and locks the keyguard.<br/> + * 3 - The end button goes to the home screen. If the user is already on the + * home screen, it puts the device to sleep. + */ + public static final String END_BUTTON_BEHAVIOR = "end_button_behavior"; + + /** + * Whether Airplane Mode is on. + */ + public static final String AIRPLANE_MODE_ON = "airplane_mode_on"; + + /** + * Constant for use in AIRPLANE_MODE_RADIOS to specify Bluetooth radio. + */ + public static final String RADIO_BLUETOOTH = "bluetooth"; + + /** + * Constant for use in AIRPLANE_MODE_RADIOS to specify Wi-Fi radio. + */ + public static final String RADIO_WIFI = "wifi"; + + /** + * Constant for use in AIRPLANE_MODE_RADIOS to specify Cellular radio. + */ + public static final String RADIO_CELL = "cell"; + + /** + * A comma separated list of radios that need to be disabled when airplane mode + * is on. This overrides WIFI_ON and BLUETOOTH_ON, if Wi-Fi and bluetooth are + * included in the comma separated list. + */ + public static final String AIRPLANE_MODE_RADIOS = "airplane_mode_radios"; + + /** + * Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this. + */ + public static final String WIFI_ON = "wifi_on"; + + /** + * Whether to notify the user of open networks. + * <p> + * If not connected and the scan results have an open network, we will + * put this notification up. If we attempt to connect to a network or + * the open network(s) disappear, we remove the notification. When we + * show the notification, we will not show it again for + * {@link #WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time. + */ + public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = + "wifi_networks_available_notification_on"; + + /** + * Delay (in seconds) before repeating the Wi-Fi networks available notification. + * Connecting to a network will reset the timer. + */ + public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = + "wifi_networks_available_repeat_delay"; + + /** + * When the number of open networks exceeds this number, the + * least-recently-used excess networks will be removed. + */ + public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; + + /** + * Whether the Wi-Fi watchdog is enabled. + */ + public static final String WIFI_WATCHDOG_ON = "wifi_watchdog_on"; + + /** + * The number of access points required for a network in order for the + * watchdog to monitor it. + */ + public static final String WIFI_WATCHDOG_AP_COUNT = "wifi_watchdog_ap_count"; + + /** + * The number of initial pings to perform that *may* be ignored if they + * fail. Again, if these fail, they will *not* be used in packet loss + * calculation. For example, one network always seemed to time out for + * the first couple pings, so this is set to 3 by default. + */ + public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = "wifi_watchdog_initial_ignored_ping_count"; + + /** + * The number of pings to test if an access point is a good connection. + */ + public static final String WIFI_WATCHDOG_PING_COUNT = "wifi_watchdog_ping_count"; + + /** + * The timeout per ping. + */ + public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms"; + + /** + * The delay between pings. + */ + public static final String WIFI_WATCHDOG_PING_DELAY_MS = "wifi_watchdog_ping_delay_ms"; + + /** + * The acceptable packet loss percentage (range 0 - 100) before trying + * another AP on the same network. + */ + public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE = + "wifi_watchdog_acceptable_packet_loss_percentage"; + + /** + * The maximum number of access points (per network) to attempt to test. + * If this number is reached, the watchdog will no longer monitor the + * initial connection state for the network. This is a safeguard for + * networks containing multiple APs whose DNS does not respond to pings. + */ + public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = "wifi_watchdog_max_ap_checks"; + + /** + * Whether the Wi-Fi watchdog is enabled for background checking even + * after it thinks the user has connected to a good access point. + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED = + "wifi_watchdog_background_check_enabled"; + + /** + * The timeout for a background ping + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS = + "wifi_watchdog_background_check_timeout_ms"; + + /** + * The delay between background checks. + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = + "wifi_watchdog_background_check_delay_ms"; + + /** + * Whether to use static IP and other static network attributes. + * <p> + * Set to 1 for true and 0 for false. + */ + public static final String WIFI_USE_STATIC_IP = "wifi_use_static_ip"; + + /** + * The static IP address. + * <p> + * Example: "192.168.1.51" + */ + public static final String WIFI_STATIC_IP = "wifi_static_ip"; + + /** + * If using static IP, the gateway's IP address. + * <p> + * Example: "192.168.1.1" + */ + public static final String WIFI_STATIC_GATEWAY = "wifi_static_gateway"; + + /** + * If using static IP, the net mask. + * <p> + * Example: "255.255.255.0" + */ + public static final String WIFI_STATIC_NETMASK = "wifi_static_netmask"; + + /** + * If using static IP, the primary DNS's IP address. + * <p> + * Example: "192.168.1.1" + */ + public static final String WIFI_STATIC_DNS1 = "wifi_static_dns1"; + + /** + * If using static IP, the secondary DNS's IP address. + * <p> + * Example: "192.168.1.2" + */ + public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2"; + + /** + * User preference for which network(s) should be used. Only the + * connectivity service should touch this. + */ + public static final String NETWORK_PREFERENCE = "network_preference"; + + /** + * Whether bluetooth is enabled/disabled + * 0=disabled. 1=enabled. + */ + public static final String BLUETOOTH_ON = "bluetooth_on"; + + /** + * Determines whether remote devices may discover and/or connect to + * this device. + * <P>Type: INT</P> + * 2 -- discoverable and connectable + * 1 -- connectable but not discoverable + * 0 -- neither connectable nor discoverable + */ + public static final String BLUETOOTH_DISCOVERABILITY = + "bluetooth_discoverability"; + + /** + * Bluetooth discoverability timeout. If this value is nonzero, then + * Bluetooth becomes discoverable for a certain number of seconds, + * after which is becomes simply connectable. The value is in seconds. + */ + public static final String BLUETOOTH_DISCOVERABILITY_TIMEOUT = + "bluetooth_discoverability_timeout"; + + /** + * Whether autolock is enabled (0 = false, 1 = true) + */ + public static final String LOCK_PATTERN_ENABLED = "lock_pattern_autolock"; + + /** + * Whether the device has been provisioned (0 = false, 1 = true) + */ + public static final String DEVICE_PROVISIONED = "device_provisioned"; + + /** + * Whether lock pattern is visible as user enters (0 = false, 1 = true) + */ + public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; + + + /** + * A formatted string of the next alarm that is set, or the empty string + * if there is no alarm set. + */ + public static final String NEXT_ALARM_FORMATTED = "next_alarm_formatted"; + + /** + * Comma-separated list of location providers that activities may access. + */ + public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; + + /** + * Whether or not data roaming is enabled. (0 = false, 1 = true) + */ + public static final String DATA_ROAMING = "data_roaming"; + + /** + * Scaling factor for fonts, float. + */ + public static final String FONT_SCALE = "font_scale"; + + /** + * Name of an application package to be debugged. + */ + public static final String DEBUG_APP = "debug_app"; + + /** + * If 1, when launching DEBUG_APP it will wait for the debugger before + * starting user code. If 0, it will run normally. + */ + public static final String WAIT_FOR_DEBUGGER = "wait_for_debugger"; + + /** + * Whether or not to dim the screen. 0=no 1=yes + */ + public static final String DIM_SCREEN = "dim_screen"; + + /** + * The timeout before the screen turns off. + */ + public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; + + /** + * The screen backlight brightness between 0 and 255. + */ + public static final String SCREEN_BRIGHTNESS = "screen_brightness"; + + /** + * Control whether the process CPU usage meter should be shown. + */ + public static final String SHOW_PROCESSES = "show_processes"; + + /** + * If 1, the activity manager will aggressively finish activities and + * processes as soon as they are no longer needed. If 0, the normal + * extended lifetime is used. + */ + public static final String ALWAYS_FINISH_ACTIVITIES = + "always_finish_activities"; + + + /** + * Ringer mode. This is used internally, changing this value will not + * change the ringer mode. See AudioManager. + */ + public static final String MODE_RINGER = "mode_ringer"; + + /** + * Determines which streams are affected by ringer mode changes. The + * stream type's bit should be set to 1 if it should be muted when going + * into an inaudible ringer mode. + */ + public static final String MODE_RINGER_STREAMS_AFFECTED = "mode_ringer_streams_affected"; + + /** + * Determines which streams are affected by mute. The + * stream type's bit should be set to 1 if it should be muted when a mute request + * is received. + */ + public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected"; + + /** + * Whether vibrate is on for different events. This is used internally, + * changing this value will not change the vibrate. See AudioManager. + */ + public static final String VIBRATE_ON = "vibrate_on"; + + /** + * Ringer volume. This is used internally, changing this value will not + * change the volume. See AudioManager. + */ + public static final String VOLUME_RING = "volume_ring"; + + /** + * System/notifications volume. This is used internally, changing this + * value will not change the volume. See AudioManager. + */ + public static final String VOLUME_SYSTEM = "volume_system"; + + /** + * Voice call volume. This is used internally, changing this value will + * not change the volume. See AudioManager. + */ + public static final String VOLUME_VOICE = "volume_voice"; + + /** + * Music/media/gaming volume. This is used internally, changing this + * value will not change the volume. See AudioManager. + */ + public static final String VOLUME_MUSIC = "volume_music"; + + /** + * Alarm volume. This is used internally, changing this + * value will not change the volume. See AudioManager. + */ + public static final String VOLUME_ALARM = "volume_alarm"; + + /** + * The mapping of stream type (integer) to its setting. + */ + public static final String[] VOLUME_SETTINGS = { + VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, VOLUME_ALARM + }; + + /** + * Appended to various volume related settings to record the previous + * values before they the settings were affected by a silent/vibrate + * ringer mode change. + */ + public static final String APPEND_FOR_LAST_AUDIBLE = "_last_audible"; + + /** + * Persistent store for the system-wide default ringtone URI. + * <p> + * If you need to play the default ringtone at any given time, it is recommended + * you give {@link #DEFAULT_RINGTONE_URI} to the media player. It will resolve + * to the set default ringtone at the time of playing. + * + * @see #DEFAULT_RINGTONE_URI + */ + public static final String RINGTONE = "ringtone"; + + /** + * A {@link Uri} that will point to the current default ringtone at any + * given time. + * <p> + * If the current default ringtone is in the DRM provider and the caller + * does not have permission, the exception will be a + * FileNotFoundException. + */ + public static final Uri DEFAULT_RINGTONE_URI = getUriFor(RINGTONE); + + /** + * Persistent store for the system-wide default notification sound. + * + * @see #RINGTONE + * @see #DEFAULT_NOTIFICATION_URI + */ + public static final String NOTIFICATION_SOUND = "notification_sound"; + + /** + * A {@link Uri} that will point to the current default notification + * sound at any given time. + * + * @see #DEFAULT_RINGTONE_URI + */ + public static final Uri DEFAULT_NOTIFICATION_URI = getUriFor(NOTIFICATION_SOUND); + + /** + * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off + */ + public static final String TEXT_AUTO_REPLACE = "auto_replace"; + + /** + * Setting to enable Auto Caps in text editors. 1 = On, 0 = Off + */ + public static final String TEXT_AUTO_CAPS = "auto_caps"; + + /** + * Setting to enable Auto Punctuate in text editors. 1 = On, 0 = Off. This + * feature converts two spaces to a "." and space. + */ + public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate"; + + /** + * Setting to showing password characters in text editors. 1 = On, 0 = Off + */ + public static final String TEXT_SHOW_PASSWORD = "show_password"; + /** + * USB Mass Storage Enabled + */ + public static final String USB_MASS_STORAGE_ENABLED = + "usb_mass_storage_enabled"; + + public static final String SHOW_GTALK_SERVICE_STATUS = + "SHOW_GTALK_SERVICE_STATUS"; + + /** + * Name of activity to use for wallpaper on the home screen. + */ + public static final String WALLPAPER_ACTIVITY = "wallpaper_activity"; + + /** + * Host name and port for a user-selected proxy. + */ + public static final String HTTP_PROXY = "http_proxy"; + + /** + * Value to specify if the user prefers the date, time and time zone + * to be automatically fetched from the network (NITZ). 1=yes, 0=no + */ + public static final String AUTO_TIME = "auto_time"; + + /** + * Display times as 12 or 24 hours + * 12 + * 24 + */ + public static final String TIME_12_24 = "time_12_24"; + + /** + * Date format string + * mm/dd/yyyy + * dd/mm/yyyy + * yyyy/mm/dd + */ + public static final String DATE_FORMAT = "date_format"; + + /** + * Settings classname to launch when Settings is clicked from All + * Applications. Needed because of user testing between the old + * and new Settings apps. TODO: 881807 + */ + public static final String SETTINGS_CLASSNAME = "settings_classname"; + + /** + * Whether the setup wizard has been run before (on first boot), or if + * it still needs to be run. + * + * nonzero = it has been run in the past + * 0 = it has not been run in the past + */ + public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run"; + + /** + * The Android ID (a unique 64-bit value) as a hex string. + * Identical to that obtained by calling + * GoogleLoginService.getAndroidId(); it is also placed here + * so you can get it without binding to a service. + */ + public static final String ANDROID_ID = "android_id"; + + /** + * The Logging ID (a unique 64-bit value) as a hex string. + * Used as a pseudonymous identifier for logging. + */ + public static final String LOGGING_ID = "logging_id"; + + /** + * If this setting is set (to anything), then all references + * to Gmail on the device must change to Google Mail. + */ + public static final String USE_GOOGLE_MAIL = "use_google_mail"; + + /** + * Whether the package installer should allow installation of apps downloaded from + * sources other than the Android Market (vending machine). + * + * 1 = allow installing from other sources + * 0 = only allow installing from the Android Market + */ + public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; + + /** + * Scaling factor for normal window animations. Setting to 0 will disable window + * animations. + */ + public static final String WINDOW_ANIMATION_SCALE = "window_animation_scale"; + + /** + * Scaling factor for activity transition animations. Setting to 0 will disable window + * animations. + */ + public static final String TRANSITION_ANIMATION_SCALE = "transition_animation_scale"; + + public static final String PARENTAL_CONTROL_ENABLED = + "parental_control_enabled"; + + public static final String PARENTAL_CONTROL_REDIRECT_URL = + "parental_control_redirect_url"; + + public static final String PARENTAL_CONTROL_LAST_UPDATE = + "parental_control_last_update"; + + /** + * Whether ADB is enabled. + */ + public static final String ADB_ENABLED = "adb_enabled"; + + /** + * Whether the audible DTMF tones are played by the dialer when dialing. The value is + * boolean (1 or 0). + */ + public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone"; + + /** + * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is + * boolean (1 or 0). + */ + public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled"; + } + + + /** + * Gservices settings, containing the network names for Google's + * various services. This table holds simple name/addr pairs. + * Addresses can be accessed through the getString() method. + * @hide + */ + public static final class Gservices extends NameValueTable { + public static final String SYS_PROP_SETTING_VERSION = "sys.settings_gservices_version"; + + private static volatile NameValueCache mNameValueCache = null; + private static final Object mNameValueCacheLock = new Object(); + + /** + * Look up a name in the database. + * @param resolver to access the database with + * @param name to look up in the table + * @return the corresponding value, or null if not present + */ + public static String getString(ContentResolver resolver, String name) { + synchronized (mNameValueCacheLock) { + if (mNameValueCache == null) { + mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); + } + return mNameValueCache.getString(resolver, name); + } + } + + /** + * Store a name/value pair into the database. + * @param resolver to access the database with + * @param name to store + * @param value to associate with the name + * @return true if the value was set, false on database errors + */ + public static boolean putString(ContentResolver resolver, + String name, String value) { + return putString(resolver, CONTENT_URI, name, value); + } + + /** + * Look up the value for name in the database, convert it to an int using Integer.parseInt + * and return it. If it is null or if a NumberFormatException is caught during the + * conversion then return defValue. + */ + public static int getInt(ContentResolver resolver, String name, int defValue) { + String valString = getString(resolver, name); + int value; + try { + value = valString != null ? Integer.parseInt(valString) : defValue; + } catch (NumberFormatException e) { + value = defValue; + } + return value; + } + + /** + * Look up the value for name in the database, convert it to a long using Long.parseLong + * and return it. If it is null or if a NumberFormatException is caught during the + * conversion then return defValue. + */ + public static long getLong(ContentResolver resolver, String name, long defValue) { + String valString = getString(resolver, name); + long value; + try { + value = valString != null ? Long.parseLong(valString) : defValue; + } catch (NumberFormatException e) { + value = defValue; + } + return value; + } + + /** + * Construct the content URI for a particular name/value pair, + * useful for monitoring changes with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI, or null if not present + */ + public static Uri getUriFor(String name) { + return getUriFor(CONTENT_URI, name); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/gservices"); + + /** + * MMS - URL to use for HTTP "x-wap-profile" header + */ + public static final String MMS_X_WAP_PROFILE_URL + = "mms_x_wap_profile_url"; + + /** + * YouTube - "most viewed" url + */ + public static final String YOUTUBE_MOST_VIEWED_URL + = "youtube_most_viewed_url"; + + /** + * YouTube - "most recent" url + */ + public static final String YOUTUBE_MOST_RECENT_URL + = "youtube_most_recent_url"; + + /** + * YouTube - "top favorites" url + */ + public static final String YOUTUBE_TOP_FAVORITES_URL + = "youtube_top_favorites_url"; + + /** + * YouTube - "most discussed" url + */ + public static final String YOUTUBE_MOST_DISCUSSED_URL + = "youtube_most_discussed_url"; + + /** + * YouTube - "most responded" url + */ + public static final String YOUTUBE_MOST_RESPONDED_URL + = "youtube_most_responded_url"; + + /** + * YouTube - "most linked" url + */ + public static final String YOUTUBE_MOST_LINKED_URL + = "youtube_most_linked_url"; + + /** + * YouTube - "top rated" url + */ + public static final String YOUTUBE_TOP_RATED_URL + = "youtube_top_rated_url"; + + /** + * YouTube - "recently featured" url + */ + public static final String YOUTUBE_RECENTLY_FEATURED_URL + = "youtube_recently_featured_url"; + + /** + * YouTube - my uploaded videos + */ + public static final String YOUTUBE_MY_VIDEOS_URL + = "youtube_my_videos_url"; + + /** + * YouTube - "my favorite" videos url + */ + public static final String YOUTUBE_MY_FAVORITES_URL + = "youtube_my_favorites_url"; + + /** + * YouTube - "by author" videos url -- used for My videos + */ + public static final String YOUTUBE_BY_AUTHOR_URL + = "youtube_by_author_url"; + + /** + * YouTube - save a video to favorite videos url + */ + public static final String YOUTUBE_SAVE_TO_FAVORITES_URL + = "youtube_save_to_favorites_url"; + + /** + * YouTube - "mobile" videos url + */ + public static final String YOUTUBE_MOBILE_VIDEOS_URL + = "youtube_mobile_videos_url"; + + /** + * YouTube - search videos url + */ + public static final String YOUTUBE_SEARCH_URL + = "youtube_search_url"; + + /** + * YouTube - category search videos url + */ + public static final String YOUTUBE_CATEGORY_SEARCH_URL + = "youtube_category_search_url"; + + /** + * YouTube - url to get the list of categories + */ + public static final String YOUTUBE_CATEGORY_LIST_URL + = "youtube_category_list_url"; + + /** + * YouTube - related videos url + */ + public static final String YOUTUBE_RELATED_VIDEOS_URL + = "youtube_related_videos_url"; + + /** + * YouTube - individual video url + */ + public static final String YOUTUBE_INDIVIDUAL_VIDEO_URL + = "youtube_individual_video_url"; + + /** + * YouTube - user's playlist url + */ + public static final String YOUTUBE_MY_PLAYLISTS_URL + = "youtube_my_playlists_url"; + + /** + * YouTube - user's subscriptions url + */ + public static final String YOUTUBE_MY_SUBSCRIPTIONS_URL + = "youtube_my_subscriptions_url"; + + /** + * YouTube - the url we use to contact YouTube to get a device id + */ + public static final String YOUTUBE_REGISTER_DEVICE_URL + = "youtube_register_device_url"; + + /** + * YouTube - the flag to indicate whether to use proxy + */ + public static final String YOUTUBE_USE_PROXY + = "youtube_use_proxy"; + + /** + * Event tags from the kernel event log to upload during checkin. + */ + public static final String CHECKIN_EVENTS = "checkin_events"; + + /** + * The interval (in seconds) between periodic checkin attempts. + */ + public static final String CHECKIN_INTERVAL = "checkin_interval"; + + /** + * How frequently (in seconds) to check the memory status of the + * device. + */ + public static final String MEMCHECK_INTERVAL = "memcheck_interval"; + + /** + * Max frequency (in seconds) to log memory check stats, in realtime + * seconds. This allows for throttling of logs when the device is + * running for large amounts of time. + */ + public static final String MEMCHECK_LOG_REALTIME_INTERVAL = "memcheck_log_realtime_interval"; + + /** + * Boolean indicating whether rebooting due to system memory checks + * is enabled. + */ + public static final String MEMCHECK_SYSTEM_ENABLED = "memcheck_system_enabled"; + + /** + * How many bytes the system process must be below to avoid scheduling + * a soft reboot. This reboot will happen when it is next determined + * to be a good time. + */ + public static final String MEMCHECK_SYSTEM_SOFT_THRESHOLD = "memcheck_system_soft"; + + /** + * How many bytes the system process must be below to avoid scheduling + * a hard reboot. This reboot will happen immediately. + */ + public static final String MEMCHECK_SYSTEM_HARD_THRESHOLD = "memcheck_system_hard"; + + /** + * How many bytes the phone process must be below to avoid scheduling + * a soft restart. This restart will happen when it is next determined + * to be a good time. + */ + public static final String MEMCHECK_PHONE_SOFT_THRESHOLD = "memcheck_phone_soft"; + + /** + * How many bytes the phone process must be below to avoid scheduling + * a hard restart. This restart will happen immediately. + */ + public static final String MEMCHECK_PHONE_HARD_THRESHOLD = "memcheck_phone_hard"; + + /** + * Boolean indicating whether restarting the phone process due to + * memory checks is enabled. + */ + public static final String MEMCHECK_PHONE_ENABLED = "memcheck_phone_enabled"; + + /** + * First time during the day it is okay to kill processes + * or reboot the device due to low memory situations. This number is + * in seconds since midnight. + */ + public static final String MEMCHECK_EXEC_START_TIME = "memcheck_exec_start_time"; + + /** + * Last time during the day it is okay to kill processes + * or reboot the device due to low memory situations. This number is + * in seconds since midnight. + */ + public static final String MEMCHECK_EXEC_END_TIME = "memcheck_exec_end_time"; + + /** + * How long the screen must have been off in order to kill processes + * or reboot. This number is in seconds. A value of -1 means to + * entirely disregard whether the screen is on. + */ + public static final String MEMCHECK_MIN_SCREEN_OFF = "memcheck_min_screen_off"; + + /** + * How much time there must be until the next alarm in order to kill processes + * or reboot. This number is in seconds. Note: this value must be + * smaller than {@link #MEMCHECK_RECHECK_INTERVAL} or else it will + * always see an alarm scheduled within its time. + */ + public static final String MEMCHECK_MIN_ALARM = "memcheck_min_alarm"; + + /** + * How frequently to check whether it is a good time to restart things, + * if the device is in a bad state. This number is in seconds. Note: + * this value must be larger than {@link #MEMCHECK_MIN_ALARM} or else + * the alarm to schedule the recheck will always appear within the + * minimum "do not execute now" time. + */ + public static final String MEMCHECK_RECHECK_INTERVAL = "memcheck_recheck_interval"; + + /** + * How frequently (in DAYS) to reboot the device. If 0, no reboots + * will occur. + */ + public static final String REBOOT_INTERVAL = "reboot_interval"; + + /** + * First time during the day it is okay to force a reboot of the + * device (if REBOOT_INTERVAL is set). This number is + * in seconds since midnight. + */ + public static final String REBOOT_START_TIME = "reboot_start_time"; + + /** + * The window of time (in seconds) after each REBOOT_INTERVAL in which + * a reboot can be executed. If 0, a reboot will always be executed at + * exactly the given time. Otherwise, it will only be executed if + * the device is idle within the window. + */ + public static final String REBOOT_WINDOW = "reboot_window"; + + /** + * The minimum version of the server that is required in order for the device to accept + * the server's recommendations about the initial sync settings to use. When this is unset, + * blank or can't be interpreted as an integer then we will not ask the server for a + * recommendation. + */ + public static final String GMAIL_CONFIG_INFO_MIN_SERVER_VERSION = + "gmail_config_info_min_server_version"; + + /** + * Controls whether Gmail offers a preview button for images. + */ + public static final String GMAIL_DISALLOW_IMAGE_PREVIEWS = "gmail_disallow_image_previews"; + + /** + * The timeout in milliseconds that Gmail uses when opening a connection and reading + * from it. A missing value or a value of -1 instructs Gmail to use the defaults provided + * by GoogleHttpClient. + */ + public static final String GMAIL_TIMEOUT_MS = "gmail_timeout_ms"; + + /** + * Hostname of the GTalk server. + */ + public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname"; + + /** + * Secure port of the GTalk server. + */ + public static final String GTALK_SERVICE_SECURE_PORT = "gtalk_secure_port"; + + /** + * The server configurable RMQ acking interval + */ + public static final String GTALK_SERVICE_RMQ_ACK_INTERVAL = "gtalk_rmq_ack_interval"; + + /** + * The minimum reconnect delay for short network outages or when the network is suspended + * due to phone use. + */ + public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT = + "gtalk_min_reconnect_delay_short"; + + /** + * The reconnect variant range for short network outages or when the network is suspended + * due to phone use. A random number between 0 and this constant is computed and + * added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT} to form the initial reconnect + * delay. + */ + public static final String GTALK_SERVICE_RECONNECT_VARIANT_SHORT = + "gtalk_reconnect_variant_short"; + + /** + * The minimum reconnect delay for long network outages + */ + public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG = + "gtalk_min_reconnect_delay_long"; + + /** + * The reconnect variant range for long network outages. A random number between 0 and this + * constant is computed and added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG} to + * form the initial reconnect delay. + */ + public static final String GTALK_SERVICE_RECONNECT_VARIANT_LONG = + "gtalk_reconnect_variant_long"; + + /** + * The maximum reconnect delay time, in milliseconds. + */ + public static final String GTALK_SERVICE_MAX_RECONNECT_DELAY = + "gtalk_max_reconnect_delay"; + + /** + * The network downtime that is considered "short" for the above calculations, + * in milliseconds. + */ + public static final String GTALK_SERVICE_SHORT_NETWORK_DOWNTIME = + "gtalk_short_network_downtime"; + + /** + * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet + * will reset the heartbeat timer. The away heartbeat should be used when the user is + * logged into the GTalk app, but not actively using it. + */ + public static final String GTALK_SERVICE_AWAY_HEARTBEAT_INTERVAL_MS = + "gtalk_heartbeat_ping_interval_ms"; // keep the string backward compatible + + /** + * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet + * will reset the heartbeat timer. The active heartbeat should be used when the user is + * actively using the GTalk app. + */ + public static final String GTALK_SERVICE_ACTIVE_HEARTBEAT_INTERVAL_MS = + "gtalk_active_heartbeat_ping_interval_ms"; + + /** + * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet + * will reset the heartbeat timer. The sync heartbeat should be used when the user isn't + * logged into the GTalk app, but auto-sync is enabled. + */ + public static final String GTALK_SERVICE_SYNC_HEARTBEAT_INTERVAL_MS = + "gtalk_sync_heartbeat_ping_interval_ms"; + + /** + * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet + * will reset the heartbeat timer. The no sync heartbeat should be used when the user isn't + * logged into the GTalk app, and auto-sync is not enabled. + */ + public static final String GTALK_SERVICE_NOSYNC_HEARTBEAT_INTERVAL_MS = + "gtalk_nosync_heartbeat_ping_interval_ms"; + + /** + * How long we wait to receive a heartbeat ping acknowledgement (or another packet) + * from the GTalk server, before deeming the connection dead. + */ + public static final String GTALK_SERVICE_HEARTBEAT_ACK_TIMEOUT_MS = + "gtalk_heartbeat_ack_timeout_ms"; + + /** + * How long after screen is turned off before we consider the user to be idle. + */ + public static final String GTALK_SERVICE_IDLE_TIMEOUT_MS = + "gtalk_idle_timeout_ms"; + + /** + * By default, GTalkService will always connect to the server regardless of the auto-sync + * setting. However, if this parameter is true, then GTalkService will only connect + * if auto-sync is enabled. Using the GTalk app will trigger the connection too. + */ + public static final String GTALK_SERVICE_CONNECT_ON_AUTO_SYNC = + "gtalk_connect_on_auto_sync"; + + /** + * GTalkService holds a wakelock while broadcasting the intent for data message received. + * It then automatically release the wakelock after a timeout. This setting controls what + * the timeout should be. + */ + public static final String GTALK_DATA_MESSAGE_WAKELOCK_MS = + "gtalk_data_message_wakelock_ms"; + + /** + * The socket read timeout used to control how long ssl handshake wait for reads before + * timing out. This is needed so the ssl handshake doesn't hang for a long time in some + * circumstances. + */ + public static final String GTALK_SSL_HANDSHAKE_TIMEOUT_MS = + "gtalk_ssl_handshake_timeout_ms"; + + /** + * How many bytes long a message has to be, in order to be gzipped. + */ + public static final String SYNC_MIN_GZIP_BYTES = + "sync_min_gzip_bytes"; + + /** + * The hash value of the current provisioning settings + */ + public static final String PROVISIONING_DIGEST = "digest"; + + /** + * Provisioning keys to block from server update + */ + public static final String PROVISIONING_OVERRIDE = "override"; + + /** + * "Generic" service name for authentication requests. + */ + public static final String GOOGLE_LOGIN_GENERIC_AUTH_SERVICE + = "google_login_generic_auth_service"; + + /** + * Frequency in milliseconds at which we should sync the locally installed Vending Machine + * content with the server. + */ + public static final String VENDING_SYNC_FREQUENCY_MS = "vending_sync_frequency_ms"; + + /** + * Support URL that is opened in a browser when user clicks on 'Help and Info' in Vending + * Machine. + */ + public static final String VENDING_SUPPORT_URL = "vending_support_url"; + + /** + * Indicates if Vending Machine requires a SIM to be in the phone to allow a purchase. + * + * true = SIM is required + * false = SIM is not required + */ + public static final String VENDING_REQUIRE_SIM_FOR_PURCHASE = + "vending_require_sim_for_purchase"; + + /** + * The current version id of the Vending Machine terms of service. + */ + public static final String VENDING_TOS_VERSION = "vending_tos_version"; + + /** + * URL that points to the terms of service for Vending Machine. + */ + public static final String VENDING_TOS_URL = "vending_tos_url"; + + /** + * Whether to use sierraqa instead of sierra tokens for the purchase flow in + * Vending Machine. + * + * true = use sierraqa + * false = use sierra (default) + */ + public static final String VENDING_USE_CHECKOUT_QA_SERVICE = + "vending_use_checkout_qa_service"; + + /** + * URL that points to the legal terms of service to display in Settings. + * <p> + * This should be a https URL. For a pretty user-friendly URL, use + * {@link #SETTINGS_TOS_PRETTY_URL}. + */ + public static final String SETTINGS_TOS_URL = "settings_tos_url"; + + /** + * URL that points to the legal terms of service to display in Settings. + * <p> + * This should be a pretty http URL. For the URL the device will access + * via Settings, use {@link #SETTINGS_TOS_URL}. + */ + public static final String SETTINGS_TOS_PRETTY_URL = "settings_tos_pretty_url"; + + /** + * URL that points to the contributors to display in Settings. + * <p> + * This should be a https URL. For a pretty user-friendly URL, use + * {@link #SETTINGS_CONTRIBUTORS_PRETTY_URL}. + */ + public static final String SETTINGS_CONTRIBUTORS_URL = "settings_contributors_url"; + + /** + * URL that points to the contributors to display in Settings. + * <p> + * This should be a pretty http URL. For the URL the device will access + * via Settings, use {@link #SETTINGS_CONTRIBUTORS_URL}. + */ + public static final String SETTINGS_CONTRIBUTORS_PRETTY_URL = + "settings_contributors_pretty_url"; + + /** + * Request an MSISDN token for various Google services. + */ + public static final String USE_MSISDN_TOKEN = "use_msisdn_token"; + + /** + * RSA public key used to encrypt passwords stored in the database. + */ + public static final String GLS_PUBLIC_KEY = "google_login_public_key"; + + /** + * Only check parental control status if this is set to "true". + */ + public static final String PARENTAL_CONTROL_CHECK_ENABLED = + "parental_control_check_enabled"; + + + /** + * Duration in which parental control status is valid. + */ + public static final String PARENTAL_CONTROL_TIMEOUT_IN_MS = + "parental_control_timeout_in_ms"; + + /** + * When parental control is off, we expect to get this string from the + * litmus url. + */ + public static final String PARENTAL_CONTROL_EXPECTED_RESPONSE = + "parental_control_expected_response"; + + /** + * When the litmus url returns a 302, declare parental control to be on + * only if the redirect url matches this regular expression. + */ + public static final String PARENTAL_CONTROL_REDIRECT_REGEX = + "parental_control_redirect_regex"; + + /** + * Threshold for the amount of change in disk free space required to report the amount of + * free space. Used to prevent spamming the logs when the disk free space isn't changing + * frequently. + */ + public static final String DISK_FREE_CHANGE_REPORTING_THRESHOLD = + "disk_free_change_reporting_threshold"; + + /** + * Prefix for new Google services published by the checkin + * server. + */ + public static final String GOOGLE_SERVICES_PREFIX + = "google_services:"; + + /** + * The maximum reconnect delay for short network outages or when the network is suspended + * due to phone use. + */ + public static final String SYNC_MAX_RETRY_DELAY_IN_SECONDS = + "sync_max_retry_delay_in_seconds"; + + /** + * Minimum percentage of free storage on the device that is used to determine if + * the device is running low on storage. + * Say this value is set to 10, the device is considered running low on storage + * if 90% or more of the device storage is filled up. + */ + public static final String SYS_STORAGE_THRESHOLD_PERCENTAGE = + "sys_storage_threshold_percentage"; + + /** + * The interval in minutes after which the amount of free storage left on the + * device is logged to the event log + */ + public static final String SYS_FREE_STORAGE_LOG_INTERVAL = + "sys_free_storage_log_interval"; + + /** + * The interval in milliseconds at which to check packet counts on the + * mobile data interface when screen is on, to detect possible data + * connection problems. + */ + public static final String PDP_WATCHDOG_POLL_INTERVAL_MS = + "pdp_watchdog_poll_interval_ms"; + + /** + * The interval in milliseconds at which to check packet counts on the + * mobile data interface when screen is off, to detect possible data + * connection problems. + */ + public static final String PDP_WATCHDOG_LONG_POLL_INTERVAL_MS = + "pdp_watchdog_long_poll_interval_ms"; + + /** + * The interval in milliseconds at which to check packet counts on the + * mobile data interface after {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} + * outgoing packets has been reached without incoming packets. + */ + public static final String PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS = + "pdp_watchdog_error_poll_interval_ms"; + + /** + * The number of outgoing packets sent without seeing an incoming packet + * that triggers a countdown (of {@link #PDP_WATCHDOG_ERROR_POLL_COUNT} + * device is logged to the event log + */ + public static final String PDP_WATCHDOG_TRIGGER_PACKET_COUNT = + "pdp_watchdog_trigger_packet_count"; + + /** + * The number of polls to perform (at {@link #PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS}) + * after hitting {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} before + * attempting data connection recovery. + */ + public static final String PDP_WATCHDOG_ERROR_POLL_COUNT = + "pdp_watchdog_error_poll_count"; + + /** + * The number of failed PDP reset attempts before moving to something more + * drastic: re-registering to the network. + */ + public static final String PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT = + "pdp_watchdog_max_pdp_reset_fail_count"; + + /** + * Address to ping as a last sanity check before attempting any recovery. + * Unset or set to "0.0.0.0" to skip this check. + */ + public static final String PDP_WATCHDOG_PING_ADDRESS = + "pdp_watchdog_ping_address"; + + /** + * The "-w deadline" parameter for the ping, ie, the max time in + * seconds to spend pinging. + */ + public static final String PDP_WATCHDOG_PING_DEADLINE = + "pdp_watchdog_ping_deadline"; + + /** + * The interval in milliseconds after which Wi-Fi is considered idle. + * When idle, it is possible for the device to be switched from Wi-Fi to + * the mobile data network. + */ + public static final String WIFI_IDLE_MS = "wifi_idle_ms"; + + /** + * The interval in milliseconds at which we forcefully release the + * transition-to-mobile-data wake lock. + */ + public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = + "wifi_mobile_data_transition_wakelock_timeout_ms"; + + /** + * The maximum number of times we will retry a connection to an access + * point for which we have failed in acquiring an IP address from DHCP. + * A value of N means that we will make N+1 connection attempts in all. + */ + public static final String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; + + /** + * @deprecated + * @hide + */ + @Deprecated // Obviated by NameValueCache: just fetch the value directly. + public static class QueryMap extends ContentQueryMap { + + public QueryMap(ContentResolver contentResolver, Cursor cursor, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(cursor, NAME, keepUpdated, handlerForUpdateNotifications); + } + + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + this(contentResolver, + contentResolver.query(CONTENT_URI, null, null, null, null), + keepUpdated, handlerForUpdateNotifications); + } + + public String getString(String name) { + ContentValues cv = getValues(name); + if (cv == null) return null; + return cv.getAsString(VALUE); + } + } + + } + + /** + * User-defined bookmarks and shortcuts. The target of each bookmark is an + * Intent URL, allowing it to be either a web page or a particular + * application activity. + * + * @hide + */ + public static final class Bookmarks implements BaseColumns + { + private static final String TAG = "Bookmarks"; + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/bookmarks"); + + /** + * The row ID. + * <p>Type: INTEGER</p> + */ + public static final String ID = "_id"; + + /** + * Descriptive name of the bookmark that can be displayed to the user. + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * Arbitrary string (displayed to the user) that allows bookmarks to be + * organized into categories. There are some special names for + * standard folders, which all start with '@'. The label displayed for + * the folder changes with the locale (via {@link #labelForFolder}) but + * the folder name does not change so you can consistently query for + * the folder regardless of the current locale. + * + * <P>Type: TEXT</P> + * + */ + public static final String FOLDER = "folder"; + + /** + * The Intent URL of the bookmark, describing what it points to. This + * value is given to {@link android.content.Intent#getIntent} to create + * an Intent that can be launched. + * <P>Type: TEXT</P> + */ + public static final String INTENT = "intent"; + + /** + * Optional shortcut character associated with this bookmark. + * <P>Type: INTEGER</P> + */ + public static final String SHORTCUT = "shortcut"; + + /** + * The order in which the bookmark should be displayed + * <P>Type: INTEGER</P> + */ + public static final String ORDERING = "ordering"; + + private static final String[] sIntentProjection = { INTENT }; + private static final String[] sShortcutProjection = { ID, SHORTCUT }; + private static final String sShortcutSelection = SHORTCUT + "=?"; + + /** + * Convenience function to retrieve the bookmarked Intent for a + * particular shortcut key. + * + * @param cr The ContentResolver to query. + * @param shortcut The shortcut key. + * + * @return Intent The bookmarked URL, or null if there is no bookmark + * matching the given shortcut. + */ + public static Intent getIntentForShortcut(ContentResolver cr, char shortcut) + { + Intent intent = null; + + Cursor c = cr.query(CONTENT_URI, + sIntentProjection, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }, ORDERING); + // Keep trying until we find a valid shortcut + try { + while (intent == null && c.moveToNext()) { + try { + String intentURI = c.getString(c.getColumnIndexOrThrow(INTENT)); + intent = Intent.getIntent(intentURI); + } catch (java.net.URISyntaxException e) { + // The stored URL is bad... ignore it. + } catch (IllegalArgumentException e) { + // Column not found + Log.e(TAG, "Intent column not found", e); + } + } + } finally { + if (c != null) c.close(); + } + + return intent; + } + + /** + * Add a new bookmark to the system. + * + * @param cr The ContentResolver to query. + * @param intent The desired target of the bookmark. + * @param title Bookmark title that is shown to the user; null if none. + * @param folder Folder in which to place the bookmark; null if none. + * @param shortcut Shortcut that will invoke the bookmark; 0 if none. + * If this is non-zero and there is an existing + * bookmark entry with this same shortcut, then that + * existing shortcut is cleared (the bookmark is not + * removed). + * + * @return The unique content URL for the new bookmark entry. + */ + public static Uri add(ContentResolver cr, + Intent intent, + String title, + String folder, + char shortcut, + int ordering) + { + // If a shortcut is supplied, and it is already defined for + // another bookmark, then remove the old definition. + if (shortcut != 0) { + Cursor c = cr.query(CONTENT_URI, + sShortcutProjection, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }, null); + try { + if (c.moveToFirst()) { + while (c.getCount() > 0) { + if (!c.deleteRow()) { + Log.w(TAG, "Could not delete existing shortcut row"); + } + } + } + } finally { + if (c != null) c.close(); + } + } + + ContentValues values = new ContentValues(); + if (title != null) values.put(TITLE, title); + if (folder != null) values.put(FOLDER, folder); + values.put(INTENT, intent.toURI()); + if (shortcut != 0) values.put(SHORTCUT, (int) shortcut); + values.put(ORDERING, ordering); + return cr.insert(CONTENT_URI, values); + } + + /** + * Return the folder name as it should be displayed to the user. This + * takes care of localizing special folders. + * + * @param r Resources object for current locale; only need access to + * system resources. + * @param folder The value found in the {@link #FOLDER} column. + * + * @return CharSequence The label for this folder that should be shown + * to the user. + */ + public static CharSequence labelForFolder(Resources r, String folder) { + return folder; + } + } + + /** + * Returns the GTalk JID resource associated with this device. + * + * @return String the JID resource of the device. It uses the device IMEI in the computation + * of the JID resource. If IMEI is not ready (i.e. telephony module not ready), we'll return + * an empty string. + * @hide + */ + // TODO: we shouldn't not have a permenant Jid resource, as that's an easy target for + // spams. We should change it once a while, like when we resubscribe to the subscription feeds + // server. + // (also, should this live in GTalkService?) + public static synchronized String getJidResource() { + if (sJidResource != null) { + return sJidResource; + } + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("this should never happen"); + } + + String imei = TelephonyManager.getDefault().getDeviceId(); + if (TextUtils.isEmpty(imei)) { + return ""; + } + + byte[] hashedImei = digest.digest(imei.getBytes()); + String id = new String(Base64.encodeBase64(hashedImei), 0, 12); + id = id.replaceAll("/", "_"); + sJidResource = JID_RESOURCE_PREFIX + id; + return sJidResource; + } + + /** + * Returns the device ID that we should use when connecting to the mobile gtalk server. + * This is a string like "android-0x1242", where the hex string is the Android ID obtained + * from the GoogleLoginService. + * + * @param androidId The Android ID for this device. + * @return The device ID that should be used when connecting to the mobile gtalk server. + * @hide + */ + public static String getGTalkDeviceId(long androidId) { + return "android-" + Long.toHexString(androidId); + } +} diff --git a/core/java/android/provider/SubscribedFeeds.java b/core/java/android/provider/SubscribedFeeds.java new file mode 100644 index 0000000..4d430d5 --- /dev/null +++ b/core/java/android/provider/SubscribedFeeds.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/** + * The SubscribedFeeds provider stores all information about subscribed feeds. + * + * @hide + */ +public class SubscribedFeeds { + private SubscribedFeeds() {} + + /** + * Columns from the Feed table that other tables join into themselves. + */ + public interface FeedColumns { + /** + * The feed url. + * <P>Type: TEXT</P> + */ + public static final String FEED = "feed"; + + /** + * The authority that cares about the feed. + * <P>Type: TEXT</P> + */ + public static final String AUTHORITY = "authority"; + + /** + * The gaia service this feed is for (used for authentication). + * <P>Type: TEXT</P> + */ + public static final String SERVICE = "service"; + } + + /** + * Provides constants to access the Feeds table and some utility methods + * to ease using the Feeds content provider. + */ + public static final class Feeds implements BaseColumns, SyncConstValue, + FeedColumns { + private Feeds() {} + + public static Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static Cursor query(ContentResolver cr, String[] projection, + String where, String[] whereArgs, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + whereArgs, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://subscribedfeeds/feeds"); + + /** + * The content:// style URL for this table + */ + public static final Uri DELETED_CONTENT_URI = + Uri.parse("content://subscribedfeeds/deleted_feeds"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * subscribed feeds. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/subscribedfeeds"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * subscribed feed. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/subscribedfeed"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + } + + /** + * A convenience method to add a feed to the SubscribedFeeds + * content provider. The user specifies the values of the FEED, + * _SYNC_ACCOUNT, AUTHORITY. SERVICE, and ROUTING_INFO. + * @param resolver used to access the underlying content provider + * @param feed corresponds to the FEED column + * @param account corresponds to the _SYNC_ACCOUNT column + * @param authority corresponds to the AUTHORITY column + * @param service corresponds to the SERVICE column + * @return the Uri of the feed that was added + */ + public static Uri addFeed(ContentResolver resolver, + String feed, String account, + String authority, String service) { + ContentValues values = new ContentValues(); + values.put(SubscribedFeeds.Feeds.FEED, feed); + values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account); + values.put(SubscribedFeeds.Feeds.AUTHORITY, authority); + values.put(SubscribedFeeds.Feeds.SERVICE, service); + return resolver.insert(SubscribedFeeds.Feeds.CONTENT_URI, values); + } + + public static int deleteFeed(ContentResolver resolver, + String feed, String account, String authority) { + StringBuilder where = new StringBuilder(); + where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds.FEED + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); + return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, + where.toString(), new String[] {account, feed, authority}); + } + + public static int deleteFeeds(ContentResolver resolver, + String account, String authority) { + StringBuilder where = new StringBuilder(); + where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); + return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, + where.toString(), new String[] {account, authority}); + } + + public static String gtalkServiceRoutingInfoFromAccountAndResource( + String account, String res) { + return Uri.parse("gtalk://" + account + "/" + res).toString(); + } + + /** + * Columns from the Accounts table. + */ + public interface AccountColumns { + /** + * The account. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT = SyncConstValue._SYNC_ACCOUNT; + } + + /** + * Provides constants to access the Accounts table and some utility methods + * to ease using it. + */ + public static final class Accounts implements BaseColumns, AccountColumns { + private Accounts() {} + + public static Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://subscribedfeeds/accounts"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * accounts that have subscribed feeds. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/subscribedfeedaccounts"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * account in the subscribed feeds. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/subscribedfeedaccount"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + } +} diff --git a/core/java/android/provider/Sync.java b/core/java/android/provider/Sync.java new file mode 100644 index 0000000..b889293 --- /dev/null +++ b/core/java/android/provider/Sync.java @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2007 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 android.provider; + +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; + +import java.util.Map; + + +/** + * The Sync provider stores information used in managing the syncing of the device, + * including the history and pending syncs. + * + * @hide + */ +public final class Sync { + // utility class + private Sync() {} + + /** + * The content url for this provider. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync"); + + /** + * Columns from the stats table. + */ + public interface StatsColumns { + /** + * The sync account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT = "account"; + + /** + * The content authority (contacts, calendar, etc.). + * <P>Type: TEXT</P> + */ + public static final String AUTHORITY = "authority"; + } + + /** + * Provides constants and utility methods to access and use the stats table. + */ + public static final class Stats implements BaseColumns, StatsColumns { + + // utility class + private Stats() {} + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sync/stats"); + + /** Projection for the _id column in the stats table. */ + public static final String[] SYNC_STATS_PROJECTION = {_ID}; + } + + /** + * Columns from the history table. + */ + public interface HistoryColumns { + /** + * The ID of the stats row corresponding to this event. + * <P>Type: INTEGER</P> + */ + public static final String STATS_ID = "stats_id"; + + /** + * The source of the sync event (LOCAL, POLL, USER, SERVER). + * <P>Type: INTEGER</P> + */ + public static final String SOURCE = "source"; + + /** + * The type of sync event (START, STOP). + * <P>Type: INTEGER</P> + */ + public static final String EVENT = "event"; + + /** + * The time of the event. + * <P>Type: INTEGER</P> + */ + public static final String EVENT_TIME = "eventTime"; + + /** + * How long this event took. This is only valid if the EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String ELAPSED_TIME = "elapsedTime"; + + /** + * Any additional message associated with this event. + * <P>Type: TEXT</P> + */ + public static final String MESG = "mesg"; + + /** + * How much activity was performed sending data to the server. This is sync adapter + * specific, but usually is something like how many record update/insert/delete attempts + * were carried out. This is only valid if the EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String UPSTREAM_ACTIVITY = "upstreamActivity"; + + /** + * How much activity was performed while receiving data from the server. + * This is sync adapter specific, but usually is something like how many + * records were received from the server. This is only valid if the + * EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String DOWNSTREAM_ACTIVITY = "downstreamActivity"; + } + + /** + * Columns from the history table. + */ + public interface StatusColumns { + /** + * How many syncs were completed for this account and authority. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SYNCS = "numSyncs"; + + /** + * How long all the events for this account and authority took. + * <P>Type: INTEGER</P> + */ + public static final String TOTAL_ELAPSED_TIME = "totalElapsedTime"; + + /** + * The number of syncs with SOURCE_POLL. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_POLL = "numSourcePoll"; + + /** + * The number of syncs with SOURCE_SERVER. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_SERVER = "numSourceServer"; + + /** + * The number of syncs with SOURCE_LOCAL. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_LOCAL = "numSourceLocal"; + + /** + * The number of syncs with SOURCE_USER. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_USER = "numSourceUser"; + + /** + * The time in ms that the last successful sync ended. Will be null if + * there are no successful syncs. A successful sync is defined as one having + * MESG=MESG_SUCCESS. + * <P>Type: INTEGER</P> + */ + public static final String LAST_SUCCESS_TIME = "lastSuccessTime"; + + /** + * The SOURCE of the last successful sync. Will be null if + * there are no successful syncs. A successful sync is defined + * as one having MESG=MESG_SUCCESS. + * <P>Type: INTEGER</P> + */ + public static final String LAST_SUCCESS_SOURCE = "lastSuccessSource"; + + /** + * The end time in ms of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: INTEGER</P> + */ + public static final String LAST_FAILURE_TIME = "lastFailureTime"; + + /** + * The SOURCE of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: INTEGER</P> + */ + public static final String LAST_FAILURE_SOURCE = "lastFailureSource"; + + /** + * The MESG of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: STRING</P> + */ + public static final String LAST_FAILURE_MESG = "lastFailureMesg"; + + /** + * Is set to 1 if a sync is pending, 0 if not. + * <P>Type: INTEGER</P> + */ + public static final String PENDING = "pending"; + } + + /** + * Provides constants and utility methods to access and use the history + * table. + */ + public static class History implements BaseColumns, + StatsColumns, + HistoryColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sync/history"); + + /** Enum value for a sync start event. */ + public static final int EVENT_START = 0; + + /** Enum value for a sync stop event. */ + public static final int EVENT_STOP = 1; + + // TODO: i18n -- grab these out of resources. + /** String names for the sync event types. */ + public static final String[] EVENTS = { "START", "STOP" }; + + /** Enum value for a server-initiated sync. */ + public static final int SOURCE_SERVER = 0; + + /** Enum value for a local-initiated sync. */ + public static final int SOURCE_LOCAL = 1; + /** + * Enum value for a poll-based sync (e.g., upon connection to + * network) + */ + public static final int SOURCE_POLL = 2; + + /** Enum value for a user-initiated sync. */ + public static final int SOURCE_USER = 3; + + // TODO: i18n -- grab these out of resources. + /** String names for the sync source types. */ + public static final String[] SOURCES = { "SERVER", + "LOCAL", + "POLL", + "USER" }; + + // Error types + public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; + public static final int ERROR_AUTHENTICATION = 2; + public static final int ERROR_IO = 3; + public static final int ERROR_PARSE = 4; + public static final int ERROR_CONFLICT = 5; + public static final int ERROR_TOO_MANY_DELETIONS = 6; + public static final int ERROR_TOO_MANY_RETRIES = 7; + + // The MESG column will contain one of these or one of the Error types. + public static final String MESG_SUCCESS = "success"; + public static final String MESG_CANCELED = "canceled"; + + private static final String FINISHED_SINCE_WHERE_CLAUSE = EVENT + "=" + EVENT_STOP + + " AND " + EVENT_TIME + ">? AND " + ACCOUNT + "=? AND " + AUTHORITY + "=?"; + + public static String mesgToString(String mesg) { + if (MESG_SUCCESS.equals(mesg)) return mesg; + if (MESG_CANCELED.equals(mesg)) return mesg; + switch (Integer.parseInt(mesg)) { + case ERROR_SYNC_ALREADY_IN_PROGRESS: return "already in progress"; + case ERROR_AUTHENTICATION: return "bad authentication"; + case ERROR_IO: return "network error"; + case ERROR_PARSE: return "parse error"; + case ERROR_CONFLICT: return "conflict detected"; + case ERROR_TOO_MANY_DELETIONS: return "too many deletions"; + case ERROR_TOO_MANY_RETRIES: return "too many retries"; + default: return "unknown error"; + } + } + + // utility class + private History() {} + + /** + * returns a cursor that queries the sync history in descending event time order + * @param contentResolver the ContentResolver to use for the query + * @return the cursor on the History table + */ + public static Cursor query(ContentResolver contentResolver) { + return contentResolver.query(CONTENT_URI, null, null, null, EVENT_TIME + " desc"); + } + + public static boolean hasNewerSyncFinished(ContentResolver contentResolver, + String account, String authority, long when) { + Cursor c = contentResolver.query(CONTENT_URI, new String[]{_ID}, + FINISHED_SINCE_WHERE_CLAUSE, + new String[]{Long.toString(when), account, authority}, null); + try { + return c.getCount() > 0; + } finally { + c.close(); + } + } + } + + /** + * Provides constants and utility methods to access and use the authority history + * table, which contains information about syncs aggregated by account and authority. + * All the HistoryColumns except for EVENT are present, plus the AuthorityHistoryColumns. + */ + public static class Status extends History implements StatusColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/status"); + + // utility class + private Status() {} + + /** + * returns a cursor that queries the authority sync history in descending event order of + * ACCOUNT, AUTHORITY + * @param contentResolver the ContentResolver to use for the query + * @return the cursor on the AuthorityHistory table + */ + public static Cursor query(ContentResolver contentResolver) { + return contentResolver.query(CONTENT_URI, null, null, null, ACCOUNT + ", " + AUTHORITY); + } + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, + boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), + _ID, keepUpdated, handlerForUpdateNotifications); + } + + public ContentValues get(String account, String authority) { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + if (values.getAsString(ACCOUNT).equals(account) + && values.getAsString(AUTHORITY).equals(authority)) { + return values; + } + } + return null; + } + } + } + + /** + * Provides constants and utility methods to access and use the pending syncs table + */ + public static final class Pending implements BaseColumns, + StatsColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/pending"); + + // utility class + private Pending() {} + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, + handlerForUpdateNotifications); + } + + public boolean isPending(String account, String authority) { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + if (values.getAsString(ACCOUNT).equals(account) + && values.getAsString(AUTHORITY).equals(authority)) { + return true; + } + } + return false; + } + } + } + + /** + * Columns from the history table. + */ + public interface ActiveColumns { + /** + * The wallclock time of when the active sync started. + * <P>Type: INTEGER</P> + */ + public static final String START_TIME = "startTime"; + } + + /** + * Provides constants and utility methods to access and use the pending syncs table + */ + public static final class Active implements BaseColumns, + StatsColumns, + ActiveColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/active"); + + // utility class + private Active() {} + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, + handlerForUpdateNotifications); + } + + public ContentValues getActiveSyncInfo() { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + return values; + } + return null; + } + + public String getSyncingAccount() { + ContentValues values = getActiveSyncInfo(); + return (values == null) ? null : values.getAsString(ACCOUNT); + } + + public String getSyncingAuthority() { + ContentValues values = getActiveSyncInfo(); + return (values == null) ? null : values.getAsString(AUTHORITY); + } + + public long getSyncStartTime() { + ContentValues values = getActiveSyncInfo(); + return (values == null) ? -1 : values.getAsLong(START_TIME); + } + } + } + + /** + * Columns in the settings table, which holds key/value pairs of settings. + */ + public interface SettingsColumns { + /** + * The key of the setting + * <P>Type: TEXT</P> + */ + public static final String KEY = "name"; + + /** + * The value of the settings + * <P>Type: TEXT</P> + */ + public static final String VALUE = "value"; + } + + /** + * Provides constants and utility methods to access and use the settings + * table. + */ + public static final class Settings implements BaseColumns, SettingsColumns { + /** + * The Uri of the settings table. This table behaves a little differently than + * normal tables. Updates are not allowed, only inserts, and inserts cause a replace + * to be performed, which first deletes the row if it is already present. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/settings"); + + /** controls whether or not the devices listens for sync tickles */ + public static final String SETTING_LISTEN_FOR_TICKLES = "listen_for_tickles"; + + /** controls whether or not the individual provider is synced when tickles are received */ + public static final String SETTING_SYNC_PROVIDER_PREFIX = "sync_provider_"; + + /** + * Convenience function for updating a single settings value as a + * boolean. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param name The name of the setting to modify. + * @param val The new value for the setting. + */ + static private void putBoolean(ContentResolver contentResolver, String name, boolean val) { + ContentValues values = new ContentValues(); + values.put(KEY, name); + values.put(VALUE, Boolean.toString(val)); + // this insert is translated into an update by the underlying Sync provider + contentResolver.insert(CONTENT_URI, values); + } + + /** + * A convenience method to set whether or not the provider is synced when + * it receives a network tickle. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param providerName the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + static public void setSyncProviderAutomatically(ContentResolver contentResolver, + String providerName, boolean sync) { + putBoolean(contentResolver, SETTING_SYNC_PROVIDER_PREFIX + providerName, sync); + } + + /** + * A convenience method to set whether or not the tickle xmpp connection + * should be established. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param flag true if the tickle xmpp connection should be established + */ + static public void setListenForNetworkTickles(ContentResolver contentResolver, + boolean flag) { + putBoolean(contentResolver, SETTING_LISTEN_FOR_TICKLES, flag); + } + + public static class QueryMap extends ContentQueryMap { + private ContentResolver mContentResolver; + + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), KEY, keepUpdated, + handlerForUpdateNotifications); + mContentResolver = contentResolver; + } + + /** + * Check if the provider should be synced when a network tickle is received + * @param providerName the provider whose setting we are querying + * @return true of the provider should be synced when a network tickle is received + */ + public boolean getSyncProviderAutomatically(String providerName) { + return getBoolean(SETTING_SYNC_PROVIDER_PREFIX + providerName, true); + } + + /** + * Set whether or not the provider is synced when it receives a network tickle. + * + * @param providerName the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + public void setSyncProviderAutomatically(String providerName, boolean sync) { + Settings.setSyncProviderAutomatically(mContentResolver, providerName, sync); + } + + /** + * Set whether or not the tickle xmpp connection should be established. + * + * @param flag true if the tickle xmpp connection should be established + */ + public void setListenForNetworkTickles(boolean flag) { + Settings.setListenForNetworkTickles(mContentResolver, flag); + } + + /** + * Check if the tickle xmpp connection should be established + * @return true if it should be stablished + */ + public boolean getListenForNetworkTickles() { + return getBoolean(SETTING_LISTEN_FOR_TICKLES, true); + } + + /** + * Convenience function for retrieving a single settings value + * as a boolean. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined. + */ + private boolean getBoolean(String name, boolean def) { + ContentValues values = getValues(name); + return values != null ? values.getAsBoolean(VALUE) : def; + } + } + } +} diff --git a/core/java/android/provider/SyncConstValue.java b/core/java/android/provider/SyncConstValue.java new file mode 100644 index 0000000..6eb4398 --- /dev/null +++ b/core/java/android/provider/SyncConstValue.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2006 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 android.provider; + +/** + * Columns for tables that are synced to a server. + * @hide + */ +public interface SyncConstValue +{ + /** + * The account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT = "_sync_account"; + + /** + * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ID = "_sync_id"; + + /** + * The last time, from the sync source's point of view, that this row has been synchronized. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_TIME = "_sync_time"; + + /** + * The version of the row, as assigned by the server. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_VERSION = "_sync_version"; + + /** + * Used in temporary provider while syncing, always NULL for rows in persistent providers. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_LOCAL_ID = "_sync_local_id"; + + /** + * Used only in persistent providers, and only during merging. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_MARK = "_sync_mark"; + + /** + * Used to indicate that local, unsynced, changes are present. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_DIRTY = "_sync_dirty"; + + /** + * Used to indicate that this account is not synced + */ + public static final String NON_SYNCABLE_ACCOUNT = "non_syncable"; +} diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java new file mode 100644 index 0000000..776a266 --- /dev/null +++ b/core/java/android/provider/Telephony.java @@ -0,0 +1,1694 @@ +/* + * Copyright (C) 2006 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 android.provider; + +import com.android.internal.telephony.CallerInfo; +import com.google.android.mms.util.SqliteWrapper; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.telephony.gsm.SmsMessage; +import android.text.TextUtils; +import android.text.util.Regex; +import android.util.Config; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The Telephony provider contains data related to phone operation. + * + * @hide + */ +public final class Telephony { + private static final String TAG = "Telephony"; + private static final boolean DEBUG = false; + private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + + /** + * Base columns for tables that contain text based SMSs. + */ + public interface TextBasedSmsColumns { + /** + * The type of the message + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + public static final int MESSAGE_TYPE_ALL = 0; + public static final int MESSAGE_TYPE_INBOX = 1; + public static final int MESSAGE_TYPE_SENT = 2; + public static final int MESSAGE_TYPE_DRAFT = 3; + public static final int MESSAGE_TYPE_OUTBOX = 4; + public static final int MESSAGE_TYPE_FAILED = 5; // for failed outgoing messages + public static final int MESSAGE_TYPE_QUEUED = 6; // for messages to send later + + + /** + * The thread ID of the message + * <P>Type: INTEGER</P> + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The address of the other party + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + + /** + * The person ID of the sender + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + + /** + * The date the message was sent + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * Has the message been read + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * The TP-Status value for the message, or -1 if no status has + * been received + */ + public static final String STATUS = "status"; + + public static final int STATUS_NONE = -1; + public static final int STATUS_COMPLETE = 0; + public static final int STATUS_PENDING = 64; + public static final int STATUS_FAILED = 128; + + /** + * The subject of the message, if present + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "subject"; + + /** + * The body of the message + * <P>Type: TEXT</P> + */ + public static final String BODY = "body"; + + /** + * The id of the sender of the conversation, if present + * <P>Type: INTEGER (reference to item in content://contacts/people)</P> + */ + public static final String PERSON = "person"; + + /** + * The protocol identifier code + * <P>Type: INTEGER</P> + */ + public static final String PROTOCOL = "protocol"; + + /** + * Whether the <code>TP-Reply-Path</code> bit was set on this message + * <P>Type: BOOLEAN</P> + */ + public static final String REPLY_PATH_PRESENT = "reply_path_present"; + + /** + * The service center (SC) through which to send the message, if present + * <P>Type: TEXT</P> + */ + public static final String SERVICE_CENTER = "service_center"; + } + + /** + * Contains all text based SMS messages. + */ + public static final class Sms implements BaseColumns, TextBasedSmsColumns { + public static final Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the given URI. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @return the URI for the new message + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport) { + return addMessageToUri(resolver, uri, address, body, subject, + date, read, deliveryReport, -1L); + } + + /** + * Add an SMS to the given URI with thread_id specified. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @param threadId the thread_id of the message + * @return the URI for the new message + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport, long threadId) { + ContentValues values = new ContentValues(7); + + values.put(ADDRESS, address); + if (date != null) { + values.put(DATE, date); + } + values.put(READ, read ? Integer.valueOf(1) : Integer.valueOf(0)); + values.put(SUBJECT, subject); + values.put(BODY, body); + if (deliveryReport) { + values.put(STATUS, STATUS_PENDING); + } + if (threadId != -1L) { + values.put(THREAD_ID, threadId); + } + return resolver.insert(uri, values); + } + + /** + * Move a message to the given folder. + * + * @param context the context to use + * @param uri the message to move + * @param folder the folder to move to + * @return true if the operation succeeded + */ + public static boolean moveMessageToFolder(Context context, + Uri uri, int folder) { + if ((uri == null) || ((folder != MESSAGE_TYPE_INBOX) + && (folder != MESSAGE_TYPE_OUTBOX) + && (folder != MESSAGE_TYPE_SENT) + && (folder != MESSAGE_TYPE_DRAFT) + && (folder != MESSAGE_TYPE_FAILED) + && (folder != MESSAGE_TYPE_QUEUED))) { + return false; + } + + ContentValues values = new ContentValues(1); + + values.put(TYPE, folder); + return 1 == SqliteWrapper.update(context, context.getContentResolver(), + uri, values, null, null); + } + + /** + * Returns true iff the folder (message type) identifies an + * outgoing message. + */ + public static boolean isOutgoingFolder(int messageType) { + return (messageType == MESSAGE_TYPE_FAILED) + || (messageType == MESSAGE_TYPE_OUTBOX) + || (messageType == MESSAGE_TYPE_SENT) + || (messageType == MESSAGE_TYPE_QUEUED); + } + + /** + * Returns true if the address is an email address + * + * @param address the input address to be tested + * @return true if address is an email address + */ + public static boolean isEmailAddress(String address) { + /* + * The '@' char isn't a valid char in phone numbers. However, in SMS + * messages sent by carrier, the originating-address can contain + * non-dialable alphanumeric chars. For the purpose of thread id + * grouping, we don't care about those. We only care about the + * legitmate/dialable phone numbers (which we use the special phone + * number comparison) and email addresses (which we do straight up + * string comparison). + */ + return (address != null) && (address.indexOf('@') != -1); + } + + /** + * Formats an address for displaying, doing a phone number lookup in the + * Address Book, etc. + * + * @param context the context to use + * @param address the address to format + * @return a nicely formatted version of the sender to display + */ + public static String getDisplayAddress(Context context, String address) { + String result; + int index; + if (isEmailAddress(address)) { + index = address.indexOf('@'); + if (index != -1) { + result = address.substring(0, index); + } else { + result = address; + } + } else { + result = CallerInfo.getCallerId(context, address); + } + return result; + } + + /** + * Contains all text based SMS messages in the SMS app's inbox. + */ + public static final class Inbox implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/inbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean read) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, read, false); + } + } + + /** + * Contains all sent text based SMS messages in the SMS app's. + */ + public static final class Sent implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/sent"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + } + + /** + * Contains all sent text based SMS messages in the SMS app's. + */ + public static final class Draft implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/draft"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + + /** + * Save over an existing draft message. + * + * @param resolver the content resolver to use + * @param uri of existing message + * @param body the new body for the draft message + * @return true is successful, false otherwise + */ + public static boolean saveMessage(ContentResolver resolver, + Uri uri, String body) { + ContentValues values = new ContentValues(2); + values.put(BODY, body); + values.put(DATE, System.currentTimeMillis()); + return resolver.update(uri, values, null, null) == 1; + } + } + + /** + * Contains all pending outgoing text based SMS messages. + */ + public static final class Outbox implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/outbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Out box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param deliveryReport whether a delivery report was requested for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean deliveryReport, long threadId) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, deliveryReport, threadId); + } + } + + /** + * Contains all sent text-based SMS messages in the SMS app's. + */ + public static final class Conversations + implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/conversations"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * The first 45 characters of the body of the message + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + + /** + * The number of messages in the conversation + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "msg_count"; + } + + /** + * Contains info about SMS related Intents that are broadcast. + */ + public static final class Intents { + /** + * Broadcast Action: A new text based SMS message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_RECEIVED_ACTION = + "android.provider.Telephony.SMS_RECEIVED"; + + /** + * Broadcast Action: A new data based SMS message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DATA_SMS_RECEIVED_ACTION = + "android.intent.action.DATA_SMS_RECEIVED"; + + /** + * Broadcast Action: A new WAP PUSH message has been received by the + * device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>transactionId (Integer)</em> - The WAP transaction + * ID</li> + * <li><em>pduType (Integer)</em> - The WAP PDU type</li> + * <li><em>data</em> - The data payload of the message</li> + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String WAP_PUSH_RECEIVED_ACTION = + "android.provider.Telephony.WAP_PUSH_RECEIVED"; + + /** + * Broadcast Action: The SIM storage for SMS messages is full. If + * space is not freed, messages targeted for the SIM (class 2) may + * not be saved. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SIM_FULL_ACTION = + "android.provider.Telephony.SIM_FULL"; + + /** + * Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a + * {@link #DATA_SMS_RECEIVED_ACTION} intent. + * + * @param intent the intent to read from + * @return an array of SmsMessages for the PDUs + */ + public static final SmsMessage[] getMessagesFromIntent( + Intent intent) { + Object[] messages = (Object[]) intent.getSerializableExtra("pdus"); + byte[][] pduObjs = new byte[messages.length][]; + + for (int i = 0; i < messages.length; i++) { + pduObjs[i] = (byte[]) messages[i]; + } + byte[][] pdus = new byte[pduObjs.length][]; + int pduCount = pdus.length; + SmsMessage[] msgs = new SmsMessage[pduCount]; + for (int i = 0; i < pduCount; i++) { + pdus[i] = pduObjs[i]; + msgs[i] = SmsMessage.createFromPdu(pdus[i]); + } + return msgs; + } + } + } + + /** + * Base columns for tables that contain MMSs. + */ + public interface BaseMmsColumns extends BaseColumns { + + public static final int MESSAGE_BOX_ALL = 0; + public static final int MESSAGE_BOX_INBOX = 1; + public static final int MESSAGE_BOX_SENT = 2; + public static final int MESSAGE_BOX_DRAFTS = 3; + public static final int MESSAGE_BOX_OUTBOX = 4; + + /** + * The date the message was sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The box which the message belong to, for example, MESSAGE_BOX_INBOX. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_BOX = "msg_box"; + + /** + * Has the message been read. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * The Message-ID of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_ID = "m_id"; + + /** + * The subject of the message, if present. + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "sub"; + + /** + * The character set of the subject, if present. + * <P>Type: INTEGER</P> + */ + public static final String SUBJECT_CHARSET = "sub_cs"; + + /** + * The Content-Type of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct_t"; + + /** + * The Content-Location of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_LOCATION = "ct_l"; + + /** + * The address of the sender. + * <P>Type: TEXT</P> + */ + public static final String FROM = "from"; + + /** + * The address of the recipients. + * <P>Type: TEXT</P> + */ + public static final String TO = "to"; + + /** + * The address of the cc. recipients. + * <P>Type: TEXT</P> + */ + public static final String CC = "cc"; + + /** + * The address of the bcc. recipients. + * <P>Type: TEXT</P> + */ + public static final String BCC = "bcc"; + + /** + * The expiry time of the message. + * <P>Type: INTEGER</P> + */ + public static final String EXPIRY = "exp"; + + /** + * The class of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_CLASS = "m_cls"; + + /** + * The type of the message defined by MMS spec. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_TYPE = "m_type"; + + /** + * The version of specification that this message conform. + * <P>Type: INTEGER</P> + */ + public static final String MMS_VERSION = "v"; + + /** + * The size of the message. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_SIZE = "m_size"; + + /** + * The priority of the message. + * <P>Type: TEXT</P> + */ + public static final String PRIORITY = "pri"; + + /** + * The read-report of the message. + * <P>Type: TEXT</P> + */ + public static final String READ_REPORT = "rr"; + + /** + * Whether the report is allowed. + * <P>Type: TEXT</P> + */ + public static final String REPORT_ALLOWED = "rpt_a"; + + /** + * The response-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String RESPONSE_STATUS = "resp_st"; + + /** + * The status of the message. + * <P>Type: INTEGER</P> + */ + public static final String STATUS = "st"; + + /** + * The transaction-id of the message. + * <P>Type: TEXT</P> + */ + public static final String TRANSACTION_ID = "tr_id"; + + /** + * The retrieve-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRIEVE_STATUS = "retr_st"; + + /** + * The retrieve-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RETRIEVE_TEXT = "retr_txt"; + + /** + * The character set of the retrieve-text. + * <P>Type: TEXT</P> + */ + public static final String RETRIEVE_TEXT_CHARSET = "retr_txt_cs"; + + /** + * The read-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String READ_STATUS = "read_status"; + + /** + * The content-class of the message. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_CLASS = "ct_cls"; + + /** + * The delivery-report of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_REPORT = "d_rpt"; + + /** + * The delivery-time-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_TIME_TOKEN = "d_tm_tok"; + + /** + * The delivery-time of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_TIME = "d_tm"; + + /** + * The response-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RESPONSE_TEXT = "resp_txt"; + + /** + * The sender-visibility of the message. + * <P>Type: TEXT</P> + */ + public static final String SENDER_VISIBILITY = "s_vis"; + + /** + * The reply-charging of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING = "r_chg"; + + /** + * The reply-charging-deadline-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_DEADLINE_TOKEN = "r_chg_dl_tok"; + + /** + * The reply-charging-deadline of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_DEADLINE = "r_chg_dl"; + + /** + * The reply-charging-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLY_CHARGING_ID = "r_chg_id"; + + /** + * The reply-charging-size of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_SIZE = "r_chg_sz"; + + /** + * The previously-sent-by of the message. + * <P>Type: TEXT</P> + */ + public static final String PREVIOUSLY_SENT_BY = "p_s_by"; + + /** + * The previously-sent-date of the message. + * <P>Type: INTEGER</P> + */ + public static final String PREVIOUSLY_SENT_DATE = "p_s_d"; + + /** + * The store of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE = "store"; + + /** + * The mm-state of the message. + * <P>Type: INTEGER</P> + */ + public static final String MM_STATE = "mm_st"; + + /** + * The mm-flags-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MM_FLAGS_TOKEN = "mm_flg_tok"; + + /** + * The mm-flags of the message. + * <P>Type: TEXT</P> + */ + public static final String MM_FLAGS = "mm_flg"; + + /** + * The store-status of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE_STATUS = "store_st"; + + /** + * The store-status-text of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE_STATUS_TEXT = "store_st_txt"; + + /** + * The stored of the message. + * <P>Type: TEXT</P> + */ + public static final String STORED = "stored"; + + /** + * The totals of the message. + * <P>Type: TEXT</P> + */ + public static final String TOTALS = "totals"; + + /** + * The mbox-totals of the message. + * <P>Type: TEXT</P> + */ + public static final String MBOX_TOTALS = "mb_t"; + + /** + * The mbox-totals-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MBOX_TOTALS_TOKEN = "mb_t_tok"; + + /** + * The quotas of the message. + * <P>Type: TEXT</P> + */ + public static final String QUOTAS = "qt"; + + /** + * The mbox-quotas of the message. + * <P>Type: TEXT</P> + */ + public static final String MBOX_QUOTAS = "mb_qt"; + + /** + * The mbox-quotas-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MBOX_QUOTAS_TOKEN = "mb_qt_tok"; + + /** + * The message-count of the message. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "m_cnt"; + + /** + * The start of the message. + * <P>Type: INTEGER</P> + */ + public static final String START = "start"; + + /** + * The distribution-indicator of the message. + * <P>Type: TEXT</P> + */ + public static final String DISTRIBUTION_INDICATOR = "d_ind"; + + /** + * The element-descriptor of the message. + * <P>Type: TEXT</P> + */ + public static final String ELEMENT_DESCRIPTOR = "e_des"; + + /** + * The limit of the message. + * <P>Type: INTEGER</P> + */ + public static final String LIMIT = "limit"; + + /** + * The recommended-retrieval-mode of the message. + * <P>Type: INTEGER</P> + */ + public static final String RECOMMENDED_RETRIEVAL_MODE = "r_r_mod"; + + /** + * The recommended-retrieval-mode-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RECOMMENDED_RETRIEVAL_MODE_TEXT = "r_r_mod_txt"; + + /** + * The status-text of the message. + * <P>Type: TEXT</P> + */ + public static final String STATUS_TEXT = "st_txt"; + + /** + * The applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String APPLIC_ID = "apl_id"; + + /** + * The reply-applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLY_APPLIC_ID = "r_apl_id"; + + /** + * The aux-applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String AUX_APPLIC_ID = "aux_apl_id"; + + /** + * The drm-content of the message. + * <P>Type: TEXT</P> + */ + public static final String DRM_CONTENT = "drm_c"; + + /** + * The adaptation-allowed of the message. + * <P>Type: TEXT</P> + */ + public static final String ADAPTATION_ALLOWED = "adp_a"; + + /** + * The replace-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLACE_ID = "repl_id"; + + /** + * The cancel-id of the message. + * <P>Type: TEXT</P> + */ + public static final String CANCEL_ID = "cl_id"; + + /** + * The cancel-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String CANCEL_STATUS = "cl_st"; + + /** + * The thread ID of the message + * <P>Type: INTEGER</P> + */ + public static final String THREAD_ID = "thread_id"; + } + + /** + * Columns for the "canonical_addresses" table used by MMS and + * SMS." + */ + public interface CanonicalAddressesColumns extends BaseColumns { + /** + * An address used in MMS or SMS. Email addresses are + * converted to lower case and are compared by string + * equality. Other addresses are compared using + * PHONE_NUMBERS_EQUAL. + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + } + + /** + * Columns for the "threads" table used by MMS and SMS. + */ + public interface ThreadsColumns extends BaseColumns { + /** + * The date at which the thread was created. + * + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * A string encoding of the recipient IDs of the recipients of + * the message, in numerical order and separated by spaces. + * <P>Type: TEXT</P> + */ + public static final String RECIPIENT_IDS = "recipient_ids"; + + /** + * The message count of the thread. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "message_count"; + /** + * Indicates whether all messages of the thread have been read. + * <P>Type: INTEGER</P> + */ + public static final String READ = "read"; + /** + * The snippet of the latest message in the thread. + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + /** + * The charset of the snippet. + * <P>Type: INTEGER</P> + */ + public static final String SNIPPET_CHARSET = "snippet_cs"; + /** + * Type of the thread, either Threads.COMMON_THREAD or + * Threads.BROADCAST_THREAD. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + /** + * Indicates whether there is a transmission error in the thread. + * <P>Type: INTEGER</P> + */ + public static final String ERROR = "error"; + } + + /** + * Helper functions for the "threads" table used by MMS and SMS. + */ + public static final class Threads implements ThreadsColumns { + private static final String[] ID_PROJECTION = { BaseColumns._ID }; + private static final String STANDARD_ENCODING = "UTF-8"; + private static final Uri THREAD_ID_CONTENT_URI = Uri.parse( + "content://mms-sms/threadID"); + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "conversations"); + public static final Uri OBSOLETE_THREADS_URI = Uri.withAppendedPath( + CONTENT_URI, "obsolete"); + + public static final int COMMON_THREAD = 0; + public static final int BROADCAST_THREAD = 1; + + // No one should construct an instance of this class. + private Threads() { + } + + /** + * This is a single-recipient version of + * getOrCreateThreadId. It's convenient for use with SMS + * messages. + */ + public static long getOrCreateThreadId(Context context, String recipient) { + Set<String> recipients = new HashSet<String>(); + + recipients.add(recipient); + return getOrCreateThreadId(context, recipients); + } + + /** + * Given the recipients list and subject of an unsaved message, + * return its thread ID. If the message starts a new thread, + * allocate a new thread ID. Otherwise, use the appropriate + * existing thread ID. + * + * Find the thread ID of the same set of recipients (in + * any order, without any additions). If one + * is found, return it. Otherwise, return a unique thread ID. + */ + public static long getOrCreateThreadId( + Context context, Set<String> recipients) { + Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon(); + + for (String recipient : recipients) { + if (Mms.isEmailAddress(recipient)) { + recipient = Mms.extractAddrSpec(recipient); + } + + uriBuilder.appendQueryParameter("recipient", recipient); + } + + Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), + uriBuilder.build(), ID_PROJECTION, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } + } finally { + cursor.close(); + } + } + + throw new IllegalArgumentException("Unable to find or allocate a thread ID."); + } + } + + /** + * Contains all MMS messages. + */ + public static final class Mms implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://mms"); + + public static final Uri REPORT_REQUEST_URI = Uri.withAppendedPath( + CONTENT_URI, "report-request"); + + public static final Uri REPORT_STATUS_URI = Uri.withAppendedPath( + CONTENT_URI, "report-status"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * mailbox = name-addr + * name-addr = [display-name] angle-addr + * angle-addr = [CFWS] "<" addr-spec ">" [CFWS] + */ + private static final Pattern NAME_ADDR_EMAIL_PATTERN = + Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); + + /** + * quoted-string = [CFWS] + * DQUOTE *([FWS] qcontent) [FWS] DQUOTE + * [CFWS] + */ + private static final Pattern QUOTED_STRING_PATTERN = + Pattern.compile("\\s*\"([^\"]*)\"\\s*"); + + public static final Cursor query( + ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query( + ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, + where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + public static final String getMessageBoxName(int msgBox) { + switch (msgBox) { + case MESSAGE_BOX_ALL: + return "all"; + case MESSAGE_BOX_INBOX: + return "inbox"; + case MESSAGE_BOX_SENT: + return "sent"; + case MESSAGE_BOX_DRAFTS: + return "drafts"; + case MESSAGE_BOX_OUTBOX: + return "outbox"; + default: + throw new IllegalArgumentException("Invalid message box: " + msgBox); + } + } + + public static String extractAddrSpec(String address) { + Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address); + + if (match.matches()) { + return match.group(2); + } + return address; + } + + /** + * Returns true if the address is an email address + * + * @param address the input address to be tested + * @return true if address is an email address + */ + public static boolean isEmailAddress(String address) { + if (TextUtils.isEmpty(address)) { + return false; + } + + String s = extractAddrSpec(address); + Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(s); + return match.matches(); + } + + /** + * Formats an address for displaying, doing a phone number lookup in the + * Address Book, etc. + * + * @param context the context to use + * @param address the address to format + * @return a nicely formatted version of the sender to display + */ + public static String getDisplayAddress(Context context, String address) { + if (address == null) { + return ""; + } + + String localNumber = TelephonyManager.getDefault().getLine1Number(); + String[] values = address.split(";"); + String result = ""; + for (int i = 0; i < values.length; i++) { + if (values[i].length() > 0) { + if (PhoneNumberUtils.compare(values[i], localNumber)) { + result = result + ";" + + context.getString(com.android.internal.R.string.me); + } else if (isEmailAddress(values[i])) { + result = result + ";" + getDisplayName(context, values[i]); + } else { + result = result + ";" + CallerInfo.getCallerId(context, values[i]); + } + } + } + + if (result.length() > 0) { + // Skip the first ';' + return result.substring(1); + } + return result; + } + + private static String getEmailDisplayName(String displayString) { + Matcher match = QUOTED_STRING_PATTERN.matcher(displayString); + if (match.matches()) { + return match.group(1); + } + + return displayString; + } + + private static String getDisplayName(Context context, String email) { + Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(email); + if (match.matches()) { + // email has display name + return getEmailDisplayName(match.group(1)); + } + + Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), + Contacts.ContactMethods.CONTENT_EMAIL_URI, + new String[] { Contacts.ContactMethods.NAME }, + Contacts.ContactMethods.DATA + " = \'" + email + "\'", + null, null); + + if (cursor != null) { + try { + int columnIndex = cursor.getColumnIndexOrThrow( + Contacts.ContactMethods.NAME); + while (cursor.moveToNext()) { + String name = cursor.getString(columnIndex); + if (!TextUtils.isEmpty(name)) { + return name; + } + } + } finally { + cursor.close(); + } + } + return email; + } + + /** + * Contains all MMS messages in the MMS app's inbox. + */ + public static final class Inbox implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/inbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's sent box. + */ + public static final class Sent implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/sent"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's drafts box. + */ + public static final class Draft implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/drafts"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's outbox. + */ + public static final class Outbox implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/outbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + public static final class Addr implements BaseColumns { + /** + * The ID of MM which this address entry belongs to. + */ + public static final String MSG_ID = "msg_id"; + + /** + * The ID of contact entry in Phone Book. + */ + public static final String CONTACT_ID = "contact_id"; + + /** + * The address text. + */ + public static final String ADDRESS = "address"; + + /** + * Type of address, must be one of PduHeaders.BCC, + * PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO. + */ + public static final String TYPE = "type"; + + /** + * Character set of this entry. + */ + public static final String CHARSET = "charset"; + } + + public static final class Part implements BaseColumns { + /** + * The identifier of the message which this part belongs to. + * <P>Type: INTEGER</P> + */ + public static final String MSG_ID = "mid"; + + /** + * The order of the part. + * <P>Type: INTEGER</P> + */ + public static final String SEQ = "seq"; + + /** + * The content type of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct"; + + /** + * The name of the part. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The charset of the part. + * <P>Type: TEXT</P> + */ + public static final String CHARSET = "chset"; + + /** + * The file name of the part. + * <P>Type: TEXT</P> + */ + public static final String FILENAME = "fn"; + + /** + * The content disposition of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_DISPOSITION = "cd"; + + /** + * The content ID of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_ID = "cid"; + + /** + * The content location of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_LOCATION = "cl"; + + /** + * The start of content-type of the message. + * <P>Type: INTEGER</P> + */ + public static final String CT_START = "ctt_s"; + + /** + * The type of content-type of the message. + * <P>Type: TEXT</P> + */ + public static final String CT_TYPE = "ctt_t"; + + /** + * The location(on filesystem) of the binary data of the part. + * <P>Type: INTEGER</P> + */ + public static final String _DATA = "_data"; + + } + + public static final class Rate { + public static final Uri CONTENT_URI = Uri.withAppendedPath( + Mms.CONTENT_URI, "rate"); + /** + * When a message was successfully sent. + * <P>Type: INTEGER</P> + */ + public static final String SENT_TIME = "sent_time"; + } + + public static final class Intents { + private Intents() { + // Non-instantiatable. + } + + /** + * The extra field to store the contents of the Intent, + * which should be an array of Uri. + */ + public static final String EXTRA_CONTENTS = "contents"; + /** + * The extra field to store the type of the contents, + * which should be an array of String. + */ + public static final String EXTRA_TYPES = "types"; + /** + * The extra field to store the 'Cc' addresses. + */ + public static final String EXTRA_CC = "cc"; + /** + * The extra field to store the 'Bcc' addresses; + */ + public static final String EXTRA_BCC = "bcc"; + /** + * The extra field to store the 'Subject'. + */ + public static final String EXTRA_SUBJECT = "subject"; + /** + * Indicates that the contents of specified URIs were changed. + * The application which is showing or caching these contents + * should be updated. + */ + public static final String + CONTENT_CHANGED_ACTION = "android.intent.action.CONTENT_CHANGED"; + /** + * An extra field which stores the URI of deleted contents. + */ + public static final String DELETED_CONTENTS = "deleted_contents"; + } + } + + /** + * Contains all MMS and SMS messages. + */ + public static final class MmsSms implements BaseColumns { + /** + * The column to distinguish SMS & MMS messages in query results. + */ + public static final String TYPE_DISCRIMINATOR_COLUMN = + "transport_type"; + + public static final Uri CONTENT_URI = Uri.parse("content://mms-sms/"); + + public static final Uri CONTENT_CONVERSATIONS_URI = Uri.parse( + "content://mms-sms/conversations"); + + public static final Uri CONTENT_FILTER_BYPHONE_URI = Uri.parse( + "content://mms-sms/messages/byphone"); + + public static final Uri CONTENT_UNDELIVERED_URI = Uri.parse( + "content://mms-sms/undelivered"); + + public static final Uri CONTENT_DRAFT_URI = Uri.parse( + "content://mms-sms/draft"); + + // Constants for message protocol types. + public static final int SMS_PROTO = 0; + public static final int MMS_PROTO = 1; + + // Constants for error types of pending messages. + public static final int NO_ERROR = 0; + public static final int ERR_TYPE_GENERIC = 1; + public static final int ERR_TYPE_SMS_PROTO_TRANSIENT = 2; + public static final int ERR_TYPE_MMS_PROTO_TRANSIENT = 3; + public static final int ERR_TYPE_TRANSPORT_FAILURE = 4; + public static final int ERR_TYPE_GENERIC_PERMANENT = 10; + public static final int ERR_TYPE_SMS_PROTO_PERMANENT = 11; + public static final int ERR_TYPE_MMS_PROTO_PERMANENT = 12; + + public static final class PendingMessages implements BaseColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "pending"); + /** + * The type of transport protocol(MMS or SMS). + * <P>Type: INTEGER</P> + */ + public static final String PROTO_TYPE = "proto_type"; + /** + * The ID of the message to be sent or downloaded. + * <P>Type: INTEGER</P> + */ + public static final String MSG_ID = "msg_id"; + /** + * The type of the message to be sent or downloaded. + * This field is only valid for MM. For SM, its value is always + * set to 0. + */ + public static final String MSG_TYPE = "msg_type"; + /** + * The type of the error code. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_TYPE = "err_type"; + /** + * The error code of sending/retrieving process. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "err_code"; + /** + * How many times we tried to send or download the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRY_INDEX = "retry_index"; + /** + * The time to do next retry. + */ + public static final String DUE_TIME = "due_time"; + /** + * The time we last tried to send or download the message. + */ + public static final String LAST_TRY = "last_try"; + } + } + + public static final class Carriers implements BaseColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://telephony/carriers"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + public static final String NAME = "name"; + + public static final String APN = "apn"; + + public static final String PROXY = "proxy"; + + public static final String PORT = "port"; + + public static final String MMSPROXY = "mmsproxy"; + + public static final String MMSPORT = "mmsport"; + + public static final String SERVER = "server"; + + public static final String USER = "user"; + + public static final String PASSWORD = "password"; + + public static final String MMSC = "mmsc"; + + public static final String MCC = "mcc"; + + public static final String MNC = "mnc"; + + public static final String NUMERIC = "numeric"; + + public static final String TYPE = "type"; + + } + + public static final class Intents { + private Intents() { + // Not instantiable + } + + /** + * Broadcast Action: A "secret code" has been entered in the dialer. Secret codes are + * of the form *#*#<code>#*#*. The intent will have the data URI:</p> + * + * <p><code>android_secret_code://<code></code></p> + */ + public static final String SECRET_CODE_ACTION = + "android.provider.Telephony.SECRET_CODE"; + + /** + * Broadcast Action: The Service Provider string(s) have been updated. Activities or + * services that use these strings should update their display. + * The intent will have the following extra values:</p> + * <ul> + * <li><em>showPlmn</em> - Boolean that indicates whether the PLMN should be shown.</li> + * <li><em>plmn</em> - The operator name of the registered network, as a string.</li> + * <li><em>showSpn</em> - Boolean that indicates whether the SPN should be shown.</li> + * <li><em>spn</em> - The service provider name, as a string.</li> + * </ul> + * Note that <em>showPlmn</em> may indicate that <em>plmn</em> should be displayed, even + * though the value for <em>plmn</em> is null. This can happen, for example, if the phone + * has not registered to a network yet. In this case the receiver may substitute an + * appropriate placeholder string (eg, "No service"). + * + * It is recommended to display <em>plmn</em> before / above <em>spn</em> if + * both are displayed. + */ + public static final String SPN_STRINGS_UPDATED_ACTION = + "android.provider.Telephony.SPN_STRINGS_UPDATED"; + + public static final String EXTRA_SHOW_PLMN = "showPlmn"; + public static final String EXTRA_PLMN = "plmn"; + public static final String EXTRA_SHOW_SPN = "showSpn"; + public static final String EXTRA_SPN = "spn"; + } +} + + diff --git a/core/java/android/provider/package.html b/core/java/android/provider/package.html new file mode 100644 index 0000000..a553592 --- /dev/null +++ b/core/java/android/provider/package.html @@ -0,0 +1,11 @@ +<HTML> +<BODY> +Provides convenience classes to access the content providers supplied by +Android. +<p>Android ships with a number of content providers that store common data such +as contact informations, calendar information, and media files. These classes +provide simplified methods of adding or retrieving data from these content +providers. For information about how to use a content provider, see <a +href="{@docRoot}devel/data.html">Reading and Writing Persistent Data</a>. +</BODY> +</HTML> |