summaryrefslogtreecommitdiffstats
path: root/core/java/android/provider
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch)
tree35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/provider
downloadframeworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2
Initial Contribution
Diffstat (limited to 'core/java/android/provider')
-rw-r--r--core/java/android/provider/BaseColumns.java32
-rw-r--r--core/java/android/provider/Browser.java450
-rw-r--r--core/java/android/provider/Calendar.java1115
-rw-r--r--core/java/android/provider/CallLog.java190
-rw-r--r--core/java/android/provider/Checkin.java290
-rw-r--r--core/java/android/provider/Contacts.java1606
-rw-r--r--core/java/android/provider/Downloads.java604
-rw-r--r--core/java/android/provider/DrmStore.java185
-rw-r--r--core/java/android/provider/Gmail.java2355
-rw-r--r--core/java/android/provider/Im.java1937
-rw-r--r--core/java/android/provider/MediaStore.java1224
-rw-r--r--core/java/android/provider/OpenableColumns.java37
-rw-r--r--core/java/android/provider/SearchRecentSuggestions.java224
-rw-r--r--core/java/android/provider/Settings.java2073
-rw-r--r--core/java/android/provider/SubscribedFeeds.java204
-rw-r--r--core/java/android/provider/Sync.java603
-rw-r--r--core/java/android/provider/SyncConstValue.java71
-rw-r--r--core/java/android/provider/Telephony.java1694
-rw-r--r--core/java/android/provider/package.html11
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 &quot;bobby&quot; &lt;bob@example.com&gt;
+ * @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 &quot;bobby&quot; &lt;bob@example.com&gt;
+ * @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 &lt;= position &lt;= 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 &amp; 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://&lt;code&gt;</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>