Add Picasa module to new3d. It's self-contained and will be needed to implement PicasaMediaSet later.
Change-Id: I41b3839215366dee4997b9743e4ca62cf50df885
diff --git a/new3d/src/com/android/gallery3d/picasa/AlbumEntry.java b/new3d/src/com/android/gallery3d/picasa/AlbumEntry.java
new file mode 100644
index 0000000..24d6a40
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/AlbumEntry.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import org.xml.sax.Attributes;
+
+/**
+ * This class models the album entry kind in the Picasa GData API.
+ */
+@Entry.Table("albums")
+public final class AlbumEntry extends Entry {
+ public static final EntrySchema SCHEMA = new EntrySchema(AlbumEntry.class);
+
+ /**
+ * The user account that is the sync source for this entry. Must be set
+ * before insert/update.
+ */
+ @Column(Columns.SYNC_ACCOUNT)
+ public String syncAccount;
+
+ /**
+ * The ETag for the album/photos GData feed.
+ */
+ @Column(Columns.PHOTOS_ETAG)
+ public String photosEtag = null;
+
+ /**
+ * True if the contents of the album need to be synchronized. Must be set
+ * before insert/update.
+ */
+ @Column(Columns.PHOTOS_DIRTY)
+ public boolean photosDirty;
+
+ /**
+ * The "edit" URI of the album.
+ */
+ @Column(Columns.EDIT_URI)
+ public String editUri;
+
+ /**
+ * The album owner.
+ */
+ @Column(Columns.USER)
+ public String user;
+
+ /**
+ * The title of the album.
+ */
+ @Column(value = Columns.TITLE)
+ public String title;
+
+ /**
+ * A short summary of the contents of the album.
+ */
+ @Column(value = Columns.SUMMARY)
+ public String summary;
+
+ /**
+ * The date the album was created.
+ */
+ @Column(Columns.DATE_PUBLISHED)
+ public long datePublished;
+
+ /**
+ * The date the album was last updated.
+ */
+ @Column(Columns.DATE_UPDATED)
+ public long dateUpdated;
+
+ /**
+ * The date the album entry was last edited. May be more recent than
+ * dateUpdated.
+ */
+ @Column(Columns.DATE_EDITED)
+ public long dateEdited;
+
+ /**
+ * The number of photos in the album.
+ */
+ @Column(Columns.NUM_PHOTOS)
+ public int numPhotos;
+
+ /**
+ * The number of bytes of storage that this album uses.
+ */
+ @Column(Columns.BYTES_USED)
+ public long bytesUsed;
+
+ /**
+ * The user-specified location associated with the album.
+ */
+ @Column(Columns.LOCATION_STRING)
+ public String locationString;
+
+ /**
+ * The thumbnail URL associated with the album.
+ */
+ @Column(Columns.THUMBNAIL_URL)
+ public String thumbnailUrl;
+
+ /**
+ * A link to the HTML page associated with the album.
+ */
+ @Column(Columns.HTML_PAGE_URL)
+ public String htmlPageUrl;
+
+ /**
+ * Column names specific to album entries.
+ */
+ public static final class Columns extends PicasaApi.Columns {
+ public static final String PHOTOS_ETAG = "photos_etag";
+ public static final String USER = "user";
+ public static final String BYTES_USED = "bytes_used";
+ public static final String NUM_PHOTOS = "num_photos";
+ public static final String LOCATION_STRING = "location_string";
+ public static final String PHOTOS_DIRTY = "photos_dirty";
+ }
+
+ /**
+ * Resets values to defaults for object reuse.
+ */
+ @Override
+ public void clear() {
+ super.clear();
+ syncAccount = null;
+ photosDirty = false;
+ editUri = null;
+ user = null;
+ title = null;
+ summary = null;
+ datePublished = 0;
+ dateUpdated = 0;
+ dateEdited = 0;
+ numPhotos = 0;
+ bytesUsed = 0;
+ locationString = null;
+ thumbnailUrl = null;
+ htmlPageUrl = null;
+ }
+
+ /**
+ * Sets the property value corresponding to the given XML element, if
+ * applicable.
+ */
+ @Override
+ public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+ char localNameChar = localName.charAt(0);
+ if (uri.equals(GDataParser.GPHOTO_NAMESPACE)) {
+ switch (localNameChar) {
+ case 'i':
+ if (localName.equals("id")) {
+ id = Long.parseLong(content);
+ }
+ break;
+ case 'u':
+ if (localName.equals("user")) {
+ user = content;
+ }
+ break;
+ case 'n':
+ if (localName.equals("numphotos")) {
+ numPhotos = Integer.parseInt(content);
+ }
+ break;
+ case 'b':
+ if (localName.equals("bytesUsed")) {
+ bytesUsed = Long.parseLong(content);
+ }
+ break;
+ }
+ } else if (uri.equals(GDataParser.ATOM_NAMESPACE)) {
+ switch (localNameChar) {
+ case 't':
+ if (localName.equals("title")) {
+ title = content;
+ }
+ break;
+ case 's':
+ if (localName.equals("summary")) {
+ summary = content;
+ }
+ break;
+ case 'p':
+ if (localName.equals("published")) {
+ datePublished = GDataParser.parseAtomTimestamp(content);
+ }
+ break;
+ case 'u':
+ if (localName.equals("updated")) {
+ dateUpdated = GDataParser.parseAtomTimestamp(content);
+ }
+ break;
+ case 'l':
+ if (localName.equals("link")) {
+ String rel = attrs.getValue("", "rel");
+ String href = attrs.getValue("", "href");
+ if (rel.equals("alternate") && attrs.getValue("", "type").equals("text/html")) {
+ htmlPageUrl = href;
+ } else if (rel.equals("edit")) {
+ editUri = href;
+ }
+ }
+ break;
+ }
+ } else if (uri.equals(GDataParser.APP_NAMESPACE)) {
+ if (localName.equals("edited")) {
+ dateEdited = GDataParser.parseAtomTimestamp(content);
+ }
+ } else if (uri.equals(GDataParser.MEDIA_RSS_NAMESPACE)) {
+ if (localName == "thumbnail") {
+ thumbnailUrl = attrs.getValue("", "url");
+ }
+ }
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/Entry.java b/new3d/src/com/android/gallery3d/picasa/Entry.java
new file mode 100644
index 0000000..a20bad0
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/Entry.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import org.xml.sax.Attributes;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public abstract class Entry {
+ public static final String[] ID_PROJECTION = { "_id" };
+
+ // The primary key of the entry.
+ @Column("_id")
+ public long id = 0;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface Table {
+ String value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Column {
+ String value();
+
+ boolean indexed() default false;
+
+ boolean fullText() default false;
+ }
+
+ public void clear() {
+ id = 0;
+ }
+
+ abstract public void setPropertyFromXml(
+ String uri, String localName, Attributes attrs, String content);
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/EntrySchema.java b/new3d/src/com/android/gallery3d/picasa/EntrySchema.java
new file mode 100644
index 0000000..b4993ee
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/EntrySchema.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.gallery3d.ui.Util;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+public final class EntrySchema {
+ public static final int TYPE_STRING = 0;
+ public static final int TYPE_BOOLEAN = 1;
+ public static final int TYPE_SHORT = 2;
+ public static final int TYPE_INT = 3;
+ public static final int TYPE_LONG = 4;
+ public static final int TYPE_FLOAT = 5;
+ public static final int TYPE_DOUBLE = 6;
+ public static final int TYPE_BLOB = 7;
+ public static final String SQLITE_TYPES[] = {
+ "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };
+
+ private static final String TAG = "SchemaInfo";
+ private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext";
+
+ private final String mTableName;
+ private final ColumnInfo[] mColumnInfo;
+ private final String[] mProjection;
+ private final boolean mHasFullTextIndex;
+
+ public EntrySchema(Class<? extends Entry> clazz) {
+ // Get table and column metadata from reflection.
+ ColumnInfo[] columns = parseColumnInfo(clazz);
+ mTableName = parseTableName(clazz);
+ mColumnInfo = columns;
+
+ // Cache the list of projection columns and check for full-text columns.
+ String[] projection = {};
+ boolean hasFullTextIndex = false;
+ if (columns != null) {
+ projection = new String[columns.length];
+ for (int i = 0; i != columns.length; ++i) {
+ ColumnInfo column = columns[i];
+ projection[i] = column.name;
+ if (column.fullText) {
+ hasFullTextIndex = true;
+ }
+ }
+ }
+ mProjection = projection;
+ mHasFullTextIndex = hasFullTextIndex;
+ }
+
+ public String getTableName() {
+ return mTableName;
+ }
+
+ public ColumnInfo[] getColumnInfo() {
+ return mColumnInfo;
+ }
+
+ public String[] getProjection() {
+ return mProjection;
+ }
+
+ private void logExecSql(SQLiteDatabase db, String sql) {
+ // Log.i(TAG, sql);
+ db.execSQL(sql);
+ }
+
+ public void cursorToObject(Cursor cursor, Entry object) {
+ try {
+ for (ColumnInfo column : mColumnInfo) {
+ int columnIndex = column.projectionIndex;
+ Field field = column.field;
+ switch (column.type) {
+ case TYPE_STRING:
+ field.set(object, cursor.getString(columnIndex));
+ break;
+ case TYPE_BOOLEAN:
+ field.setBoolean(object, cursor.getShort(columnIndex) == 1);
+ break;
+ case TYPE_SHORT:
+ field.setShort(object, cursor.getShort(columnIndex));
+ break;
+ case TYPE_INT:
+ field.setInt(object, cursor.getInt(columnIndex));
+ break;
+ case TYPE_LONG:
+ field.setLong(object, cursor.getLong(columnIndex));
+ break;
+ case TYPE_FLOAT:
+ field.setFloat(object, cursor.getFloat(columnIndex));
+ break;
+ case TYPE_DOUBLE:
+ field.setDouble(object, cursor.getDouble(columnIndex));
+ break;
+ case TYPE_BLOB:
+ field.set(object, cursor.getBlob(columnIndex));
+ break;
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void objectToValues(Entry object, ContentValues values) {
+ try {
+ for (ColumnInfo column : mColumnInfo) {
+ String columnName = column.name;
+ Field field = column.field;
+ switch (column.type) {
+ case TYPE_STRING:
+ values.put(columnName, (String) field.get(object));
+ break;
+ case TYPE_BOOLEAN:
+ values.put(columnName, field.getBoolean(object));
+ break;
+ case TYPE_SHORT:
+ values.put(columnName, field.getShort(object));
+ break;
+ case TYPE_INT:
+ values.put(columnName, field.getInt(object));
+ break;
+ case TYPE_LONG:
+ values.put(columnName, field.getLong(object));
+ break;
+ case TYPE_FLOAT:
+ values.put(columnName, field.getFloat(object));
+ break;
+ case TYPE_DOUBLE:
+ values.put(columnName, field.getDouble(object));
+ break;
+ case TYPE_BLOB:
+ values.put(columnName, (byte[]) field.get(object));
+ break;
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Cursor queryAll(SQLiteDatabase db) {
+ return db.query(mTableName, mProjection, null, null, null, null, null);
+ }
+
+ public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) {
+ Cursor cursor = db.query(mTableName, mProjection, "_id=?",
+ new String[] {Long.toString(id)}, null, null, null);
+ boolean success = false;
+ if (cursor.moveToFirst()) {
+ cursorToObject(cursor, entry);
+ success = true;
+ }
+ cursor.close();
+ return success;
+ }
+
+ public long insertOrReplace(SQLiteDatabase db, Entry entry) {
+ ContentValues values = new ContentValues();
+ objectToValues(entry, values);
+ if (entry.id == 0) {
+ Log.i(TAG, "removing id before insert");
+ values.remove("_id");
+ }
+ long id = db.replace(mTableName, "_id", values);
+ entry.id = id;
+ return id;
+ }
+
+ public boolean deleteWithId(SQLiteDatabase db, long id) {
+ return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1;
+ }
+
+ public void createTables(SQLiteDatabase db) {
+ // Wrapped class must have a @Table.Definition.
+ String tableName = mTableName;
+ Util.Assert(tableName != null);
+
+ // Add the CREATE TABLE statement for the main table.
+ StringBuilder sql = new StringBuilder("CREATE TABLE ");
+ sql.append(tableName);
+ sql.append(" (_id INTEGER PRIMARY KEY");
+ for (ColumnInfo column : mColumnInfo) {
+ if (!column.isId()) {
+ sql.append(',');
+ sql.append(column.name);
+ sql.append(' ');
+ sql.append(SQLITE_TYPES[column.type]);
+ if (column.extraSql != null) {
+ sql.append(' ');
+ sql.append(column.extraSql);
+ }
+ }
+ }
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Create indexes for all indexed columns.
+ for (ColumnInfo column : mColumnInfo) {
+ // Create an index on the indexed columns.
+ if (column.indexed) {
+ sql.append("CREATE INDEX ");
+ sql.append(tableName);
+ sql.append("_index_");
+ sql.append(column.name);
+ sql.append(" ON ");
+ sql.append(tableName);
+ sql.append(" (");
+ sql.append(column.name);
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+ }
+ }
+
+ if (mHasFullTextIndex) {
+ // Add an FTS virtual table if using full-text search.
+ String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX;
+ sql.append("CREATE VIRTUAL TABLE ");
+ sql.append(ftsTableName);
+ sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ // Add the column to the FTS table.
+ String columnName = column.name;
+ sql.append(',');
+ sql.append(columnName);
+ sql.append(" TEXT");
+ }
+ }
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Build an insert statement that will automatically keep the FTS
+ // table in sync.
+ StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO ");
+ insertSql.append(ftsTableName);
+ insertSql.append(" (_id");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ insertSql.append(',');
+ insertSql.append(column.name);
+ }
+ }
+ insertSql.append(") VALUES (new._id");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ insertSql.append(",new.");
+ insertSql.append(column.name);
+ }
+ }
+ insertSql.append(");");
+ String insertSqlString = insertSql.toString();
+
+ // Add an insert trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_insert_trigger AFTER INSERT ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN ");
+ sql.append(insertSqlString);
+ sql.append("END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Add an update trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_update_trigger AFTER UPDATE ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN ");
+ sql.append(insertSqlString);
+ sql.append("END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Add a delete trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_delete_trigger AFTER DELETE ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN DELETE FROM ");
+ sql.append(ftsTableName);
+ sql.append(" WHERE _id = old._id; END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+ }
+ }
+
+ public void dropTables(SQLiteDatabase db) {
+ String tableName = mTableName;
+ StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
+ sql.append(tableName);
+ sql.append(';');
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ if (mHasFullTextIndex) {
+ sql.append("DROP TABLE IF EXISTS ");
+ sql.append(tableName);
+ sql.append(FULL_TEXT_INDEX_SUFFIX);
+ sql.append(';');
+ logExecSql(db, sql.toString());
+ }
+
+ }
+
+ public void deleteAll(SQLiteDatabase db) {
+ StringBuilder sql = new StringBuilder("DELETE FROM ");
+ sql.append(mTableName);
+ sql.append(";");
+ logExecSql(db, sql.toString());
+ }
+
+ private String parseTableName(Class<? extends Object> clazz) {
+ // Check for a table annotation.
+ Entry.Table table = clazz.getAnnotation(Entry.Table.class);
+ if (table == null) {
+ return null;
+ }
+
+ // Return the table name.
+ return table.value();
+ }
+
+ private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) {
+ // Gather metadata from each annotated field.
+ ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
+ Field[] fields = clazz.getFields();
+ for (int i = 0; i != fields.length; ++i) {
+ // Get column metadata from the annotation.
+ Field field = fields[i];
+ Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class);
+ if (info == null) {
+ continue;
+ }
+
+ // Determine the field type.
+ int type;
+ Class<?> fieldType = field.getType();
+ if (fieldType == String.class) {
+ type = TYPE_STRING;
+ } else if (fieldType == boolean.class) {
+ type = TYPE_BOOLEAN;
+ } else if (fieldType == short.class) {
+ type = TYPE_SHORT;
+ } else if (fieldType == int.class) {
+ type = TYPE_INT;
+ } else if (fieldType == long.class) {
+ type = TYPE_LONG;
+ } else if (fieldType == float.class) {
+ type = TYPE_FLOAT;
+ } else if (fieldType == double.class) {
+ type = TYPE_DOUBLE;
+ } else if (fieldType == byte[].class) {
+ type = TYPE_BLOB;
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported field type for column: " + fieldType.getName());
+ }
+
+ // Add the column to the array.
+ int index = columns.size();
+ columns.add(new ColumnInfo(info.value(),
+ type, info.indexed(), info.fullText(), field, index));
+ }
+
+ // Return a list.
+ ColumnInfo[] columnList = new ColumnInfo[columns.size()];
+ columns.toArray(columnList);
+ return columnList;
+ }
+
+ public static final class ColumnInfo {
+ private static final String ID_KEY = "_id";
+
+ public final String name;
+ public final int type;
+ public final boolean indexed;
+ public final boolean fullText;
+ public final String extraSql = "";
+ public final Field field;
+ public final int projectionIndex;
+
+ public ColumnInfo(String name, int type, boolean indexed,
+ boolean fullText, Field field, int projectionIndex) {
+ this.name = name.toLowerCase();
+ this.type = type;
+ this.indexed = indexed;
+ this.fullText = fullText;
+ this.field = field;
+ this.projectionIndex = projectionIndex;
+ }
+
+ public boolean isId() {
+ return ID_KEY.equals(name);
+ }
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/GDataClient.java b/new3d/src/com/android/gallery3d/picasa/GDataClient.java
new file mode 100644
index 0000000..9e8a47a
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/GDataClient.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+public final class GDataClient {
+ private static final String TAG = "GDataClient";
+ private static final String USER_AGENT = "GData/1.0; gzip";
+ private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
+ private static final String IF_MATCH = "If-Match";
+ private static final int CONNECTION_TIMEOUT = 20000; // ms.
+ private static final int MIN_GZIP_SIZE = 512;
+ public static final HttpParams HTTP_PARAMS;
+ public static final ThreadSafeClientConnManager HTTP_CONNECTION_MANAGER;
+
+ private final DefaultHttpClient mHttpClient =
+ new DefaultHttpClient(HTTP_CONNECTION_MANAGER, HTTP_PARAMS);
+ private String mAuthToken;
+
+ static {
+ // Prepare HTTP parameters.
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+ HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(params, CONNECTION_TIMEOUT);
+ HttpClientParams.setRedirecting(params, true);
+ HttpProtocolParams.setUserAgent(params, USER_AGENT);
+ HTTP_PARAMS = params;
+
+ // Register HTTP protocol.
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+
+ // Create the connection manager.
+ HTTP_CONNECTION_MANAGER = new ThreadSafeClientConnManager(params, schemeRegistry);
+ }
+
+ public static final class Operation {
+ public String inOutEtag;
+ public int outStatus;
+ public InputStream outBody;
+ }
+
+ public void setAuthToken(String authToken) {
+ mAuthToken = authToken;
+ }
+
+ public void get(String feedUrl, Operation operation) throws IOException {
+ callMethod(new HttpGet(feedUrl), operation);
+ }
+
+ public void post(String feedUrl, byte[] data, String contentType, Operation operation)
+ throws IOException {
+ ByteArrayEntity entity = getCompressedEntity(data);
+ entity.setContentType(contentType);
+ HttpPost post = new HttpPost(feedUrl);
+ post.setEntity(entity);
+ callMethod(post, operation);
+ }
+
+ public void put(String feedUrl, byte[] data, String contentType, Operation operation)
+ throws IOException {
+ ByteArrayEntity entity = getCompressedEntity(data);
+ entity.setContentType(contentType);
+ HttpPost post = new HttpPost(feedUrl);
+ post.setHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
+ post.setEntity(entity);
+ callMethod(post, operation);
+ }
+
+ public void putStream(String feedUrl, InputStream stream, String contentType,
+ Operation operation) throws IOException {
+ InputStreamEntity entity = new InputStreamEntity(stream, -1);
+ entity.setContentType(contentType);
+ HttpPost post = new HttpPost(feedUrl);
+ post.setHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
+ post.setEntity(entity);
+ callMethod(post, operation);
+ }
+
+ public void delete(String feedUrl, Operation operation) throws IOException {
+ HttpPost post = new HttpPost(feedUrl);
+ String etag = operation.inOutEtag;
+ post.setHeader(X_HTTP_METHOD_OVERRIDE, "DELETE");
+ post.setHeader(IF_MATCH, etag != null ? etag : "*");
+ callMethod(post, operation);
+ }
+
+ private void callMethod(HttpUriRequest request, Operation operation) throws IOException {
+ // Specify GData protocol version 2.0.
+ request.addHeader("GData-Version", "2");
+
+ // Indicate support for gzip-compressed responses.
+ request.addHeader("Accept-Encoding", "gzip");
+
+ // Specify authorization token if provided.
+ String authToken = mAuthToken;
+ if (!TextUtils.isEmpty(authToken)) {
+ request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
+ }
+
+ // Specify the ETag of a prior response, if available.
+ String etag = operation.inOutEtag;
+ if (etag != null) {
+ request.addHeader("If-None-Match", etag);
+ }
+
+ // Execute the HTTP request.
+ HttpResponse httpResponse = null;
+ try {
+ httpResponse = mHttpClient.execute(request);
+ } catch (IOException e) {
+ Log.w(TAG, "Request failed: " + request.getURI());
+ throw e;
+ }
+
+ // Get the status code and response body.
+ int status = httpResponse.getStatusLine().getStatusCode();
+ InputStream stream = null;
+ HttpEntity entity = httpResponse.getEntity();
+ if (entity != null) {
+ // Wrap the entity input stream in a GZIP decoder if necessary.
+ stream = entity.getContent();
+ if (stream != null) {
+ Header header = entity.getContentEncoding();
+ if (header != null) {
+ if (header.getValue().contains("gzip")) {
+ stream = new GZIPInputStream(stream);
+ }
+ }
+ }
+ }
+
+ // Return the stream if successful.
+ Header etagHeader = httpResponse.getFirstHeader("ETag");
+ operation.outStatus = status;
+ operation.inOutEtag = etagHeader != null ? etagHeader.getValue() : null;
+ operation.outBody = stream;
+ }
+
+ private ByteArrayEntity getCompressedEntity(byte[] data) throws IOException {
+ ByteArrayEntity entity;
+ if (data.length >= MIN_GZIP_SIZE) {
+ ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(data.length / 2);
+ GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput);
+ gzipOutput.write(data);
+ gzipOutput.close();
+ entity = new ByteArrayEntity(byteOutput.toByteArray());
+ } else {
+ entity = new ByteArrayEntity(data);
+ }
+ return entity;
+ }
+
+ public static String inputStreamToString(InputStream stream) throws IOException {
+ InputStreamReader reader = new InputStreamReader(stream);
+ StringBuilder builder = new StringBuilder();
+ char buffer[] = new char[4096];
+ int charsRead = reader.read(buffer);
+ while (charsRead > 0) {
+ builder.append(buffer, 0, charsRead);
+ }
+ return builder.toString();
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/GDataParser.java b/new3d/src/com/android/gallery3d/picasa/GDataParser.java
new file mode 100644
index 0000000..175789c
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/GDataParser.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.text.format.Time;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+public final class GDataParser implements ContentHandler {
+ public static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
+ public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
+ public static final String GD_NAMESPACE = "http://schemas.google.com/g/2005";
+ public static final String GPHOTO_NAMESPACE = "http://schemas.google.com/photos/2007";
+ public static final String MEDIA_RSS_NAMESPACE = "http://search.yahoo.com/mrss/";
+ public static final String GML_NAMESPACE = "http://www.opengis.net/gml";
+ private static final String FEED_ELEMENT = "feed";
+ private static final String ENTRY_ELEMENT = "entry";
+
+ private static final int STATE_DOCUMENT = 0;
+ private static final int STATE_FEED = 1;
+ private static final int STATE_ENTRY = 2;
+
+ private static final int NUM_LEVELS = 5;
+
+ private Entry mEntry = null;
+ private EntryHandler mHandler = null;
+ private int mState = STATE_DOCUMENT;
+ private int mLevel = 0;
+ private final String[] mUri = new String[NUM_LEVELS];
+ private final String[] mName = new String[NUM_LEVELS];
+ private final AttributesImpl[] mAttributes = new AttributesImpl[NUM_LEVELS];
+ private final StringBuilder mValue = new StringBuilder(128);
+
+ public interface EntryHandler {
+ void handleEntry(Entry entry);
+ }
+
+ public GDataParser() {
+ AttributesImpl[] attributes = mAttributes;
+ for (int i = 0; i != NUM_LEVELS; ++i) {
+ attributes[i] = new AttributesImpl();
+ }
+ }
+
+ public void setEntry(Entry entry) {
+ mEntry = entry;
+ }
+
+ public void setHandler(EntryHandler handler) {
+ mHandler = handler;
+ }
+
+ public static long parseAtomTimestamp(String timestamp) {
+ Time time = new Time();
+ time.parse3339(timestamp);
+ return time.toMillis(true);
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attrs)
+ throws SAXException {
+ switch (mState) {
+ case STATE_DOCUMENT:
+ // Expect an atom:feed element.
+ if (uri.equals(ATOM_NAMESPACE) && localName.equals(FEED_ELEMENT)) {
+ mState = STATE_FEED;
+ } else {
+ throw new SAXException();
+ }
+ break;
+ case STATE_FEED:
+ // Expect a feed property element or an atom:entry element.
+ if (uri.equals(ATOM_NAMESPACE) && localName.equals(ENTRY_ELEMENT)) {
+ mState = STATE_ENTRY;
+ mEntry.clear();
+ } else {
+ startProperty(uri, localName, attrs);
+ }
+ break;
+ case STATE_ENTRY:
+ startProperty(uri, localName, attrs);
+ break;
+ }
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if (mLevel > 0) {
+ // Handle property exit.
+ endProperty();
+ } else {
+ // Handle state exit.
+ switch (mState) {
+ case STATE_DOCUMENT:
+ throw new SAXException();
+ case STATE_FEED:
+ mState = STATE_DOCUMENT;
+ break;
+ case STATE_ENTRY:
+ mState = STATE_FEED;
+ mHandler.handleEntry(mEntry);
+ break;
+ }
+ }
+ }
+
+ private void startProperty(String uri, String localName, Attributes attrs) {
+ // Push element information onto the property stack.
+ int level = ++mLevel;
+ mValue.setLength(0);
+ mUri[level] = uri;
+ mName[level] = localName;
+ mAttributes[level].setAttributes(attrs);
+ }
+
+ private void endProperty() {
+ // Apply property to the entry, then pop the stack.
+ int level = mLevel;
+ mEntry.setPropertyFromXml(mUri[level], mName[level], mAttributes[level], mValue.toString());
+ mLevel = level - 1;
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mValue.append(ch, start, length);
+ }
+
+ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
+ // Ignored.
+ }
+
+ public void processingInstruction(String target, String data) throws SAXException {
+ // Ignored.
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ // Ignored.
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ // Ignored.
+ }
+
+ public void startDocument() throws SAXException {
+ // Ignored.
+ }
+
+ public void endDocument() throws SAXException {
+ // Ignored.
+ }
+
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ // Ignored.
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ // Ignored.
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PhotoEntry.java b/new3d/src/com/android/gallery3d/picasa/PhotoEntry.java
new file mode 100644
index 0000000..e300a22
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PhotoEntry.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import org.xml.sax.Attributes;
+
+/**
+ * This class models the photo entry kind in the Picasa GData API.
+ */
+@Entry.Table("photos")
+public final class PhotoEntry extends Entry {
+ public static final EntrySchema SCHEMA = new EntrySchema(PhotoEntry.class);
+
+ /**
+ * The user account that is the sync source for this entry. Must be set
+ * before insert/update.
+ */
+ @Column("sync_account")
+ public String syncAccount;
+
+ /**
+ * The "edit" URI of the photo.
+ */
+ @Column("edit_uri")
+ public String editUri;
+
+ /**
+ * The containing album ID.
+ */
+ @Column(value = "album_id", indexed = true)
+ public long albumId;
+
+ /**
+ * The display index of the photo within the album. Must be set before
+ * insert/update.
+ */
+ @Column(value = "display_index", indexed = true)
+ public int displayIndex;
+
+ /**
+ * The title of the photo.
+ */
+ @Column("title")
+ public String title;
+
+ /**
+ * A short summary of the photo.
+ */
+ @Column("summary")
+ public String summary;
+
+ /**
+ * The date the photo was added.
+ */
+ @Column("date_published")
+ public long datePublished;
+
+ /**
+ * The date the photo was last updated.
+ */
+ @Column("date_updated")
+ public long dateUpdated;
+
+ /**
+ * The date the photo entry was last edited. May be more recent than
+ * dateUpdated.
+ */
+ @Column("date_edited")
+ public long dateEdited;
+
+ /**
+ * The date the photo was captured as specified in the EXIF data.
+ */
+ @Column("date_taken")
+ public long dateTaken;
+
+ /**
+ * The number of comments associated with the photo.
+ */
+ @Column("comment_count")
+ public int commentCount;
+
+ /**
+ * The width of the photo in pixels.
+ */
+ @Column("width")
+ public int width;
+
+ /**
+ * The height of the photo in pixels.
+ */
+ @Column("height")
+ public int height;
+
+ /**
+ * The rotation of the photo in degrees, if rotation has not already been
+ * applied.
+ */
+ @Column("rotation")
+ public int rotation;
+
+ /**
+ * The size of the photo is bytes.
+ */
+ @Column("size")
+ public int size;
+
+ /**
+ * The latitude associated with the photo.
+ */
+ @Column("latitude")
+ public double latitude;
+
+ /**
+ * The longitude associated with the photo.
+ */
+ @Column("longitude")
+ public double longitude;
+
+ /**
+ * The "mini-thumbnail" URL for the photo (currently 144px-cropped).
+ */
+ @Column("thumbnail_url")
+ public String thumbnailUrl;
+
+ /**
+ * The "screennail" URL for the photo (currently 800px).
+ */
+ @Column("screennail_url")
+ public String screennailUrl;
+
+ /**
+ * The "content" URL for the photo (currently 1280px, or a video). The
+ * original image URL is not fetched since "imgmax" accepts one size, used
+ * to get this resource.
+ */
+ @Column("content_url")
+ public String contentUrl;
+
+ /**
+ * The MIME type of the content URL.
+ */
+ @Column("content_type")
+ public String contentType;
+
+ /**
+ * A link to the HTML page associated with the album.
+ */
+ @Column("html_page_url")
+ public String htmlPageUrl;
+
+ /**
+ * Resets values to defaults for object reuse.
+ */
+ @Override
+ public void clear() {
+ super.clear();
+ syncAccount = null;
+ editUri = null;
+ albumId = 0;
+ displayIndex = 0;
+ title = null;
+ summary = null;
+ datePublished = 0;
+ dateUpdated = 0;
+ dateEdited = 0;
+ dateTaken = 0;
+ commentCount = 0;
+ width = 0;
+ height = 0;
+ rotation = 0;
+ size = 0;
+ latitude = 0;
+ longitude = 0;
+ thumbnailUrl = null;
+ screennailUrl = null;
+ contentUrl = null;
+ contentType = null;
+ htmlPageUrl = null;
+ }
+
+ /**
+ * Sets the property value corresponding to the given XML element, if
+ * applicable.
+ */
+ @Override
+ public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+ try {
+ char localNameChar = localName.charAt(0);
+ if (uri.equals(GDataParser.GPHOTO_NAMESPACE)) {
+ switch (localNameChar) {
+ case 'i':
+ if (localName.equals("id")) {
+ id = Long.parseLong(content);
+ }
+ break;
+ case 'a':
+ if (localName.equals("albumid")) {
+ albumId = Long.parseLong(content);
+ }
+ break;
+ case 't':
+ if (localName.equals("timestamp")) {
+ dateTaken = Long.parseLong(content);
+ }
+ break;
+ case 'c':
+ if (localName.equals("commentCount")) {
+ commentCount = Integer.parseInt(content);
+ }
+ break;
+ case 'w':
+ if (localName.equals("width")) {
+ width = Integer.parseInt(content);
+ }
+ break;
+ case 'h':
+ if (localName.equals("height")) {
+ height = Integer.parseInt(content);
+ }
+ break;
+ case 'r':
+ if (localName.equals("rotation")) {
+ rotation = Integer.parseInt(content);
+ }
+ break;
+ case 's':
+ if (localName.equals("size")) {
+ size = Integer.parseInt(content);
+ }
+ break;
+ case 'l':
+ if (localName.equals("latitude")) {
+ latitude = Double.parseDouble(content);
+ } else if (localName.equals("longitude")) {
+ longitude = Double.parseDouble(content);
+ }
+ break;
+ }
+ } else if (uri.equals(GDataParser.ATOM_NAMESPACE)) {
+ switch (localNameChar) {
+ case 't':
+ if (localName.equals("title")) {
+ title = content;
+ }
+ break;
+ case 's':
+ if (localName.equals("summary")) {
+ summary = content;
+ }
+ break;
+ case 'p':
+ if (localName.equals("published")) {
+ datePublished = GDataParser.parseAtomTimestamp(content);
+ }
+ break;
+ case 'u':
+ if (localName.equals("updated")) {
+ dateUpdated = GDataParser.parseAtomTimestamp(content);
+ }
+ break;
+ case 'l':
+ if (localName.equals("link")) {
+ String rel = attrs.getValue("", "rel");
+ String href = attrs.getValue("", "href");
+ if (rel.equals("alternate")
+ && attrs.getValue("", "type").equals("text/html")) {
+ htmlPageUrl = href;
+ } else if (rel.equals("edit")) {
+ editUri = href;
+ }
+ }
+ break;
+ }
+ } else if (uri.equals(GDataParser.APP_NAMESPACE)) {
+ if (localName.equals("edited")) {
+ dateEdited = GDataParser.parseAtomTimestamp(content);
+ }
+ } else if (uri.equals(GDataParser.MEDIA_RSS_NAMESPACE)) {
+ if (localName.equals("thumbnail")) {
+ int width = Integer.parseInt(attrs.getValue("", "width"));
+ int height = Integer.parseInt(attrs.getValue("", "height"));
+ int dimension = Math.max(width, height);
+ String url = attrs.getValue("", "url");
+ if (dimension <= 300) {
+ thumbnailUrl = url;
+ } else {
+ screennailUrl = url;
+ }
+ } else if (localName.equals("content")) {
+ // Only replace an existing URL if the MIME type is video.
+ String type = attrs.getValue("", "type");
+ if (contentUrl == null || type.startsWith("video/")) {
+ contentUrl = attrs.getValue("", "url");
+ contentType = type;
+ }
+ }
+ } else if (uri.equals(GDataParser.GML_NAMESPACE)) {
+ if (localName.equals("pos")) {
+ int spaceIndex = content.indexOf(' ');
+ if (spaceIndex != -1) {
+ latitude = Double.parseDouble(content.substring(0, spaceIndex));
+ longitude = Double.parseDouble(content.substring(spaceIndex + 1));
+ }
+ }
+ }
+ } catch (Exception e) {
+ return;
+ }
+ }
+
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PicasaApi.java b/new3d/src/com/android/gallery3d/picasa/PicasaApi.java
new file mode 100644
index 0000000..011774e
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PicasaApi.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SyncResult;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Xml;
+
+import org.apache.http.HttpStatus;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.net.SocketException;
+import java.util.ArrayList;
+
+public final class PicasaApi {
+ public static final int RESULT_OK = 0;
+ public static final int RESULT_NOT_MODIFIED = 1;
+ public static final int RESULT_ERROR = 2;
+
+ private static final String TAG = "PicasaAPI";
+ private static final String BASE_URL = "http://picasaweb.google.com/data/feed/api/";
+ private static final String BASE_QUERY_STRING;
+
+ static {
+ // Build the base query string using screen dimensions.
+ final StringBuilder query = new StringBuilder("?imgmax=1024&max-results=1000&thumbsize=");
+ final String thumbnailSize = "144u,";
+ final String screennailSize = "1024u";
+ query.append(thumbnailSize);
+ query.append(screennailSize);
+ query.append("&visibility=visible");
+ BASE_QUERY_STRING = query.toString();
+ }
+
+ private final GDataClient mClient;
+ private final GDataClient.Operation mOperation = new GDataClient.Operation();
+ private final GDataParser mParser = new GDataParser();
+ private final AlbumEntry mAlbumInstance = new AlbumEntry();
+ private final PhotoEntry mPhotoInstance = new PhotoEntry();
+ private AuthAccount mAuth;
+
+ public static final class AuthAccount {
+ public final String user;
+ public final String authToken;
+ public final Account account;
+
+ public AuthAccount(String user, String authToken, Account account) {
+ this.user = user;
+ this.authToken = authToken;
+ this.account = account;
+ }
+ }
+
+ public static Account[] getAccounts(Context context) {
+ // Return the list of accounts supporting the Picasa GData service.
+ AccountManager accountManager = AccountManager.get(context);
+ Account[] accounts = {};
+ try {
+ accounts = accountManager.getAccountsByTypeAndFeatures(PicasaService.ACCOUNT_TYPE,
+ new String[] { PicasaService.FEATURE_SERVICE_NAME }, null, null).getResult();
+ } catch (Exception e) {
+ Log.e(TAG, "cannot get accounts", e);
+ }
+ return accounts;
+ }
+
+ public static AuthAccount[] getAuthenticatedAccounts(Context context) {
+ AccountManager accountManager = AccountManager.get(context);
+ Account[] accounts = getAccounts(context);
+ if (accounts == null) return new AuthAccount[0];
+
+ int numAccounts = accounts.length;
+ ArrayList<AuthAccount> authAccounts = new ArrayList<AuthAccount>(numAccounts);
+ for (int i = 0; i != numAccounts; ++i) {
+ Account account = accounts[i];
+ String authToken;
+ try {
+ // Get the token without user interaction.
+ authToken = accountManager
+ .blockingGetAuthToken(account, PicasaService.SERVICE_NAME, true);
+
+ // TODO: Remove this once the build is signed by Google, since
+ // we will always have permission.
+ // This code requests permission from the user explicitly.
+ if (context instanceof Activity) {
+ Bundle bundle = accountManager.getAuthToken(
+ account, PicasaService.SERVICE_NAME, null, (Activity) context,
+ null, null).getResult();
+ authToken = bundle.getString("authtoken");
+ PicasaService.requestSync(context, PicasaService.TYPE_USERS_ALBUMS, -1);
+ }
+
+ // Add the account information to the list of accounts.
+ if (authToken != null) {
+ String username = canonicalizeUsername(account.name);
+ authAccounts.add(new AuthAccount(username, authToken, account));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "fail to get authenticated accounts", e);
+ }
+ }
+ return authAccounts.toArray(new AuthAccount[authAccounts.size()]);
+ }
+
+ /**
+ * Returns a canonical username for a Gmail account. Lowercases the username and
+ * strips off a "gmail.com" or "googlemail.com" domain, but leaves other domains alone.
+ *
+ * e.g., Passing in "User@gmail.com: will return "user".
+ *
+ * @param username The username to be canonicalized.
+ * @return The username, lowercased and possibly stripped of its domain if a "gmail.com" or
+ * "googlemail.com" domain.
+ */
+ public static String canonicalizeUsername(String username) {
+ username = username.toLowerCase();
+ if (username.contains("@gmail.") || username.contains("@googlemail.")) {
+ // Strip the domain from GMail accounts for
+ // canonicalization. TODO: is there an official way?
+ username = username.substring(0, username.indexOf('@'));
+ }
+ return username;
+ }
+
+ public PicasaApi() {
+ mClient = new GDataClient();
+ }
+
+ public void setAuth(AuthAccount auth) {
+ mAuth = auth;
+ synchronized (mClient) {
+ mClient.setAuthToken(auth.authToken);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ public int getAlbums(AccountManager accountManager,
+ SyncResult syncResult, UserEntry user, GDataParser.EntryHandler handler) {
+ // Construct the query URL for user albums.
+ StringBuilder builder = new StringBuilder(BASE_URL);
+ builder.append("user/");
+ builder.append(Uri.encode(mAuth.user));
+ builder.append(BASE_QUERY_STRING);
+ builder.append("&kind=album");
+ try {
+ // Send the request.
+ synchronized (mOperation) {
+ GDataClient.Operation operation = mOperation;
+ operation.inOutEtag = user.albumsEtag;
+ boolean retry = false;
+ int numRetries = 1;
+ do {
+ retry = false;
+ synchronized (mClient) {
+ mClient.get(builder.toString(), operation);
+ }
+ switch (operation.outStatus) {
+ case HttpStatus.SC_OK:
+ break;
+ case HttpStatus.SC_NOT_MODIFIED:
+ return RESULT_NOT_MODIFIED;
+ case HttpStatus.SC_FORBIDDEN:
+ case HttpStatus.SC_UNAUTHORIZED:
+ if (!retry) {
+ accountManager.invalidateAuthToken(
+ PicasaService.ACCOUNT_TYPE, mAuth.authToken);
+ retry = true;
+ }
+ if (numRetries == 0) {
+ ++syncResult.stats.numAuthExceptions;
+ }
+ // fall-through
+ default:
+ Log.i(TAG, "getAlbums uri " + builder.toString());
+ Log.e(TAG, "getAlbums: unexpected status code "
+ + operation.outStatus + " data: " + operation.outBody.toString());
+ ++syncResult.stats.numIoExceptions;
+ return RESULT_ERROR;
+ }
+ --numRetries;
+ } while (retry && numRetries >= 0);
+
+ // Store the new ETag for the user/albums feed.
+ user.albumsEtag = operation.inOutEtag;
+
+ // Parse the response.
+ synchronized (mParser) {
+ GDataParser parser = mParser;
+ parser.setEntry(mAlbumInstance);
+ parser.setHandler(handler);
+ try {
+ Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
+ } catch (SocketException e) {
+ Log.e(TAG, "getAlbumPhotos: " + e);
+ ++syncResult.stats.numIoExceptions;
+ e.printStackTrace();
+ return RESULT_ERROR;
+ }
+ }
+ }
+ return RESULT_OK;
+ } catch (IOException e) {
+ Log.e(TAG, "getAlbums: " + e);
+ ++syncResult.stats.numIoExceptions;
+ } catch (SAXException e) {
+ Log.e(TAG, "getAlbums: " + e);
+ ++syncResult.stats.numParseExceptions;
+ }
+ return RESULT_ERROR;
+ }
+
+ public int getAlbumPhotos(AccountManager accountManager, SyncResult syncResult,
+ AlbumEntry album, GDataParser.EntryHandler handler) {
+ // Construct the query URL for user albums.
+ StringBuilder builder = new StringBuilder(BASE_URL);
+ builder.append("user/");
+ builder.append(Uri.encode(mAuth.user));
+ builder.append("/albumid/");
+ builder.append(album.id);
+ builder.append(BASE_QUERY_STRING);
+ builder.append("&kind=photo");
+ try {
+ // Send the request.
+ synchronized (mOperation) {
+ GDataClient.Operation operation = mOperation;
+ operation.inOutEtag = album.photosEtag;
+ boolean retry = false;
+ int numRetries = 1;
+ do {
+ retry = false;
+ synchronized (mClient) {
+ mClient.get(builder.toString(), operation);
+ }
+ switch (operation.outStatus) {
+ case HttpStatus.SC_OK:
+ break;
+ case HttpStatus.SC_NOT_MODIFIED:
+ return RESULT_NOT_MODIFIED;
+ case HttpStatus.SC_FORBIDDEN:
+ case HttpStatus.SC_UNAUTHORIZED:
+ // We need to reset the authtoken and retry only once.
+ if (!retry) {
+ retry = true;
+ accountManager.invalidateAuthToken(PicasaService.SERVICE_NAME, mAuth.authToken);
+ }
+ if (numRetries == 0) {
+ ++syncResult.stats.numAuthExceptions;
+ }
+ break;
+ default:
+ Log.e(TAG, "getAlbumPhotos: " + builder.toString()
+ + ", unexpected status code " + operation.outStatus);
+ ++syncResult.stats.numIoExceptions;
+ return RESULT_ERROR;
+ }
+ --numRetries;
+ } while (retry && numRetries >= 0);
+
+ // Store the new ETag for the album/photos feed.
+ album.photosEtag = operation.inOutEtag;
+
+ // Parse the response.
+ synchronized (mParser) {
+ GDataParser parser = mParser;
+ parser.setEntry(mPhotoInstance);
+ parser.setHandler(handler);
+ try {
+ Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
+ } catch (SocketException e) {
+ Log.e(TAG, "getAlbumPhotos: " + e);
+ ++syncResult.stats.numIoExceptions;
+ e.printStackTrace();
+ return RESULT_ERROR;
+ }
+ }
+ }
+ return RESULT_OK;
+ } catch (IOException e) {
+ Log.e(TAG, "getAlbumPhotos", e);
+ ++syncResult.stats.numIoExceptions;
+ } catch (SAXException e) {
+ Log.e(TAG, "getAlbumPhotos", e);
+ ++syncResult.stats.numParseExceptions;
+ }
+ return RESULT_ERROR;
+ }
+
+ public int deleteEntry(String editUri) {
+ try {
+ synchronized (mOperation) {
+ GDataClient.Operation operation = mOperation;
+ operation.inOutEtag = null;
+ synchronized (mClient) {
+ mClient.delete(editUri, operation);
+ }
+ if (operation.outStatus == 200) {
+ return RESULT_OK;
+ } else {
+ Log.e(TAG, "deleteEntry: failed with status code " + operation.outStatus);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "deleteEntry", e);
+ }
+ return RESULT_ERROR;
+ }
+
+ /**
+ * Column names shared by multiple entry kinds.
+ */
+ public static class Columns {
+ public static final String _ID = "_id";
+ public static final String SYNC_ACCOUNT = "sync_account";
+ public static final String EDIT_URI = "edit_uri";
+ public static final String TITLE = "title";
+ public static final String SUMMARY = "summary";
+ public static final String DATE_PUBLISHED = "date_published";
+ public static final String DATE_UPDATED = "date_updated";
+ public static final String DATE_EDITED = "date_edited";
+ public static final String THUMBNAIL_URL = "thumbnail_url";
+ public static final String HTML_PAGE_URL = "html_page_url";
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PicasaContentProvider.java b/new3d/src/com/android/gallery3d/picasa/PicasaContentProvider.java
new file mode 100644
index 0000000..57b8295
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PicasaContentProvider.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncResult;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class PicasaContentProvider extends TableContentProvider {
+ public static final String AUTHORITY = "com.android.gallery3d.picasa.contentprovider";
+ public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+ public static final Uri PHOTOS_URI = Uri.withAppendedPath(BASE_URI, "photos");
+ public static final Uri ALBUMS_URI = Uri.withAppendedPath(BASE_URI, "albums");
+
+ private static final String TAG = "PicasaContentProvider";
+ private static final String[] ID_EDITED_PROJECTION = {"_id", "date_edited"};
+ private static final String[] ID_EDITED_INDEX_PROJECTION =
+ {"_id", "date_edited", "display_index"};
+ private static final String WHERE_ACCOUNT = "sync_account=?";
+ private static final String WHERE_ALBUM_ID = "album_id=?";
+
+ private final PhotoEntry mPhotoInstance = new PhotoEntry();
+ private final AlbumEntry mAlbumInstance = new AlbumEntry();
+ private SyncContext mSyncContext = null;
+ private Account mActiveAccount;
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ // Initialize the provider and set the database.
+ super.attachInfo(context, info);
+ setDatabase(new Database(context, Database.DATABASE_NAME));
+
+ // Add mappings for each of the exposed tables.
+ addMapping(AUTHORITY, "photos", "vnd.android.gallery3d.picasa.photo", PhotoEntry.SCHEMA);
+ addMapping(AUTHORITY, "albums", "vnd.android.gallery3d.picasa.album", AlbumEntry.SCHEMA);
+
+ // Create the sync context.
+ try {
+ mSyncContext = new SyncContext();
+ } catch (Exception e) {
+ Log.e(TAG, "cannot get sync context", e);
+ // The database wasn't created successfully, we create a memory backed database.
+ setDatabase(new Database(context, null));
+ }
+ }
+
+ public static final class Database extends SQLiteOpenHelper {
+ public static final String DATABASE_NAME = "picasa.db";
+ public static final int DATABASE_VERSION = 83;
+
+ public Database(Context context, String name) {
+ super(context, name, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ PhotoEntry.SCHEMA.createTables(db);
+ AlbumEntry.SCHEMA.createTables(db);
+ UserEntry.SCHEMA.createTables(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // No new versions yet, if we are asked to upgrade we just reset
+ // everything.
+ PhotoEntry.SCHEMA.dropTables(db);
+ AlbumEntry.SCHEMA.dropTables(db);
+ UserEntry.SCHEMA.dropTables(db);
+ onCreate(db);
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ // Ensure that the URI is well-formed. We currently do not allow WHERE
+ // clauses.
+ List<String> path = uri.getPathSegments();
+ if (path.size() != 2 || !uri.getAuthority().equals(AUTHORITY) || selection != null) {
+ return 0;
+ }
+
+ // Get the sync context.
+ SyncContext context = mSyncContext;
+
+ // Determine if the URI refers to an album or photo.
+ String type = path.get(0);
+ long id = Long.parseLong(path.get(1));
+ SQLiteDatabase db = context.db;
+ if (type.equals("photos")) {
+ // Retrieve the photo from the database to get the edit URI.
+ PhotoEntry photo = mPhotoInstance;
+ if (PhotoEntry.SCHEMA.queryWithId(db, id, photo)) {
+ // Send a DELETE request to the API.
+ if (context.login(photo.syncAccount)) {
+ if (context.api.deleteEntry(photo.editUri) == PicasaApi.RESULT_OK) {
+ deletePhoto(db, id);
+ context.photosChanged = true;
+ return 1;
+ }
+ }
+ }
+ } else if (type.equals("albums")) {
+ // Retrieve the album from the database to get the edit URI.
+ AlbumEntry album = mAlbumInstance;
+ if (AlbumEntry.SCHEMA.queryWithId(db, id, album)) {
+ // Send a DELETE request to the API.
+ if (context.login(album.syncAccount)) {
+ if (context.api.deleteEntry(album.editUri) == PicasaApi.RESULT_OK) {
+ deleteAlbum(db, id);
+ context.albumsChanged = true;
+ return 1;
+ }
+ }
+ }
+ }
+ context.finish();
+ return 0;
+ }
+
+ public void reloadAccounts() {
+ mSyncContext.reloadAccounts();
+ }
+
+ public void setActiveSyncAccount(Account account) {
+ mActiveAccount = account;
+ }
+
+ public void syncUsers(SyncResult syncResult) {
+ syncUsers(mSyncContext, syncResult);
+ }
+
+ public void syncUsersAndAlbums(final boolean syncAlbumPhotos, SyncResult syncResult) {
+ SyncContext context = mSyncContext;
+
+ // Synchronize users authenticated on the device.
+ UserEntry[] users = syncUsers(context, syncResult);
+
+ // Synchronize albums for each user.
+ String activeUsername = null;
+ if (mActiveAccount != null) {
+ activeUsername = PicasaApi.canonicalizeUsername(mActiveAccount.name);
+ }
+ boolean didSyncActiveUserName = false;
+ for (int i = 0, numUsers = users.length; i != numUsers; ++i) {
+ if (activeUsername != null && !context.accounts[i].user.equals(activeUsername))
+ continue;
+ if (!ContentResolver.getSyncAutomatically(context.accounts[i].account, AUTHORITY))
+ continue;
+ didSyncActiveUserName = true;
+ context.api.setAuth(context.accounts[i]);
+ syncUserAlbums(context, users[i], syncResult);
+ if (syncAlbumPhotos) {
+ syncUserPhotos(context, users[i].account, syncResult);
+ } else {
+ // // Always sync added albums.
+ // for (Long albumId : context.albumsAdded) {
+ // syncAlbumPhotos(albumId, false);
+ // }
+ }
+ }
+ if (!didSyncActiveUserName) {
+ ++syncResult.stats.numAuthExceptions;
+ }
+ context.finish();
+ }
+
+ public void syncAlbumPhotos(
+ final long albumId, final boolean forceRefresh, SyncResult syncResult) {
+ SyncContext context = mSyncContext;
+ AlbumEntry album = new AlbumEntry();
+ if (AlbumEntry.SCHEMA.queryWithId(context.db, albumId, album)) {
+ if ((album.photosDirty || forceRefresh) && context.login(album.syncAccount)) {
+ if (isSyncEnabled(album.syncAccount, context)) {
+ syncAlbumPhotos(context, album.syncAccount, album, syncResult);
+ }
+ }
+ }
+ context.finish();
+ }
+
+ public static boolean isSyncEnabled(String accountName, SyncContext context) {
+ if (context.accounts == null) {
+ context.reloadAccounts();
+ }
+ PicasaApi.AuthAccount[] accounts = context.accounts;
+ int numAccounts = accounts.length;
+ for (int i = 0; i < numAccounts; ++i) {
+ PicasaApi.AuthAccount account = accounts[i];
+ if (account.user.equals(accountName)) {
+ return ContentResolver.getSyncAutomatically(account.account, AUTHORITY);
+ }
+ }
+ return true;
+ }
+
+ private UserEntry[] syncUsers(SyncContext context, SyncResult syncResult) {
+ // Get authorized accounts.
+ context.reloadAccounts();
+ PicasaApi.AuthAccount[] accounts = context.accounts;
+ int numUsers = accounts.length;
+ UserEntry[] users = new UserEntry[numUsers];
+
+ // Scan existing accounts.
+ EntrySchema schema = UserEntry.SCHEMA;
+ SQLiteDatabase db = context.db;
+ Cursor cursor = schema.queryAll(db);
+ if (cursor.moveToFirst()) {
+ do {
+ // Read the current account.
+ UserEntry entry = new UserEntry();
+ schema.cursorToObject(cursor, entry);
+
+ // Find the corresponding account, or delete the row if it does
+ // not exist.
+ int i;
+ for (i = 0; i != numUsers; ++i) {
+ if (accounts[i].user.equals(entry.account)) {
+ users[i] = entry;
+ break;
+ }
+ }
+ if (i == numUsers) {
+ Log.e(TAG, "Deleting user " + entry.account);
+ entry.albumsEtag = null;
+ deleteUser(db, entry.account);
+ }
+ } while (cursor.moveToNext());
+ } else {
+ // Log.i(TAG, "No users in database yet");
+ }
+ cursor.close();
+
+ // Add new accounts and synchronize user albums if recursive.
+ for (int i = 0; i != numUsers; ++i) {
+ UserEntry entry = users[i];
+ PicasaApi.AuthAccount account = accounts[i];
+ if (entry == null) {
+ entry = new UserEntry();
+ entry.account = account.user;
+ users[i] = entry;
+ Log.e(TAG, "Inserting user " + entry.account);
+ }
+ }
+ return users;
+ }
+
+ private void syncUserAlbums(
+ final SyncContext context, final UserEntry user, final SyncResult syncResult) {
+ // Query existing album entry (id, dateEdited) sorted by ID.
+ final SQLiteDatabase db = context.db;
+ Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(),
+ ID_EDITED_PROJECTION, WHERE_ACCOUNT, new String[] { user.account },
+ null, null, AlbumEntry.Columns.DATE_EDITED);
+ int localCount = cursor.getCount();
+
+ // Build a sorted index with existing entry timestamps.
+ final EntryMetadata local[] = new EntryMetadata[localCount];
+ for (int i = 0; i != localCount; ++i) {
+ cursor.moveToPosition(i); // TODO: throw exception here if returns
+ // false?
+ local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), 0);
+ }
+ cursor.close();
+ Arrays.sort(local);
+
+ // Merge the truth from the API into the local database.
+ final EntrySchema albumSchema = AlbumEntry.SCHEMA;
+ final EntryMetadata key = new EntryMetadata();
+ final AccountManager accountManager = AccountManager.get(getContext());
+ int result = context.api.getAlbums(
+ accountManager, syncResult, user, new GDataParser.EntryHandler() {
+ public void handleEntry(Entry entry) {
+ AlbumEntry album = (AlbumEntry) entry;
+ long albumId = album.id;
+ key.id = albumId;
+ int index = Arrays.binarySearch(local, key);
+ EntryMetadata metadata = index >= 0 ? local[index] : null;
+ if (metadata == null || metadata.dateEdited < album.dateEdited) {
+ // Insert / update.
+ Log.i(TAG, "insert / update album " + album.title);
+ album.syncAccount = user.account;
+ album.photosDirty = true;
+ albumSchema.insertOrReplace(db, album);
+ if (metadata == null) {
+ context.albumsAdded.add(albumId);
+ }
+ ++syncResult.stats.numUpdates;
+ } else {
+ // Up-to-date.
+ // Log.i(TAG, "up-to-date album " + album.title);
+ }
+
+ // Mark item as surviving so it is not deleted.
+ if (metadata != null) {
+ metadata.survived = true;
+ }
+ }
+ });
+
+ // Return if not modified or on error.
+ switch (result) {
+ case PicasaApi.RESULT_ERROR:
+ ++syncResult.stats.numParseExceptions;
+ case PicasaApi.RESULT_NOT_MODIFIED:
+ return;
+ }
+
+ // Update the user entry with the new ETag.
+ UserEntry.SCHEMA.insertOrReplace(db, user);
+
+ // Delete all entries not present in the API response.
+ for (int i = 0; i != localCount; ++i) {
+ EntryMetadata metadata = local[i];
+ if (!metadata.survived) {
+ deleteAlbum(db, metadata.id);
+ ++syncResult.stats.numDeletes;
+ Log.i(TAG, "delete album " + metadata.id);
+ }
+ }
+
+ // Note that albums changed.
+ context.albumsChanged = true;
+ }
+
+ private void syncUserPhotos(SyncContext context, String account, SyncResult syncResult) {
+ // Synchronize albums with out-of-date photos.
+ SQLiteDatabase db = context.db;
+ Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(),
+ Entry.ID_PROJECTION, "sync_account=? AND photos_dirty=1",
+ new String[] { account }, null, null, null);
+ AlbumEntry album = new AlbumEntry();
+ for (int i = 0, count = cursor.getCount(); i != count; ++i) {
+ cursor.moveToPosition(i);
+ if (AlbumEntry.SCHEMA.queryWithId(db, cursor.getLong(0), album)) {
+ syncAlbumPhotos(context, account, album, syncResult);
+ }
+
+ // Abort if interrupted.
+ if (Thread.interrupted()) {
+ ++syncResult.stats.numIoExceptions;
+ Log.e(TAG, "syncUserPhotos interrupted");
+ }
+ }
+ cursor.close();
+ }
+
+ private void syncAlbumPhotos(SyncContext context, final String account,
+ AlbumEntry album, final SyncResult syncResult) {
+ Log.i(TAG, "Syncing Picasa album: " + album.title);
+ // Query existing album entry (id, dateEdited) sorted by ID.
+ final SQLiteDatabase db = context.db;
+ long albumId = album.id;
+ String[] albumIdArgs = { Long.toString(albumId) };
+ Cursor cursor = db.query(PhotoEntry.SCHEMA.getTableName(), ID_EDITED_INDEX_PROJECTION, WHERE_ALBUM_ID, albumIdArgs, null,
+ null, "date_edited");
+ int localCount = cursor.getCount();
+
+ // Build a sorted index with existing entry timestamps and display
+ // indexes.
+ final EntryMetadata local[] = new EntryMetadata[localCount];
+ final EntryMetadata key = new EntryMetadata();
+ for (int i = 0; i != localCount; ++i) {
+ cursor.moveToPosition(i); // TODO: throw exception here if returns
+ // false?
+ local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), cursor.getInt(2));
+ }
+ cursor.close();
+ Arrays.sort(local);
+
+ // Merge the truth from the API into the local database.
+ final EntrySchema photoSchema = PhotoEntry.SCHEMA;
+ final int[] displayIndex = { 0 };
+ final AccountManager accountManager = AccountManager.get(getContext());
+ int result = context.api.getAlbumPhotos(
+ accountManager, syncResult, album, new GDataParser.EntryHandler() {
+ public void handleEntry(Entry entry) {
+ PhotoEntry photo = (PhotoEntry) entry;
+ long photoId = photo.id;
+ int newDisplayIndex = displayIndex[0];
+ key.id = photoId;
+ int index = Arrays.binarySearch(local, key);
+ EntryMetadata metadata = index >= 0 ? local[index] : null;
+ if (metadata == null || metadata.dateEdited < photo.dateEdited
+ || metadata.displayIndex != newDisplayIndex) {
+
+ // Insert / update.
+ // Log.i(TAG, "insert / update photo " + photo.title);
+ photo.syncAccount = account;
+ photo.displayIndex = newDisplayIndex;
+ photoSchema.insertOrReplace(db, photo);
+ ++syncResult.stats.numUpdates;
+ } else {
+ // Up-to-date.
+ // Log.i(TAG, "up-to-date photo " + photo.title);
+ }
+
+ // Mark item as surviving so it is not deleted.
+ if (metadata != null) {
+ metadata.survived = true;
+ }
+
+ // Increment the display index.
+ displayIndex[0] = newDisplayIndex + 1;
+ }
+ });
+
+ // Return if not modified or on error.
+ switch (result) {
+ case PicasaApi.RESULT_ERROR:
+ ++syncResult.stats.numParseExceptions;
+ Log.e(TAG, "syncAlbumPhotos error");
+ case PicasaApi.RESULT_NOT_MODIFIED:
+ // Log.e(TAG, "result not modified");
+ return;
+ }
+
+ // Delete all entries not present in the API response.
+ for (int i = 0; i != localCount; ++i) {
+ EntryMetadata metadata = local[i];
+ if (!metadata.survived) {
+ deletePhoto(db, metadata.id);
+ ++syncResult.stats.numDeletes;
+ // Log.i(TAG, "delete photo " + metadata.id);
+ }
+ }
+
+ // Mark album as no longer dirty and store the new ETag.
+ album.photosDirty = false;
+ AlbumEntry.SCHEMA.insertOrReplace(db, album);
+ // Log.i(TAG, "Clearing dirty bit on album " + albumId);
+
+ // Mark that photos changed.
+ // context.photosChanged = true;
+ getContext().getContentResolver().notifyChange(ALBUMS_URI, null, false);
+ getContext().getContentResolver().notifyChange(PHOTOS_URI, null, false);
+ }
+
+ private void deleteUser(SQLiteDatabase db, String account) {
+ Log.w(TAG, "deleteUser(" + account + ")");
+
+ // Select albums owned by the user.
+ String albumTableName = AlbumEntry.SCHEMA.getTableName();
+ String[] whereArgs = { account };
+ Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(),
+ Entry.ID_PROJECTION, WHERE_ACCOUNT, whereArgs, null, null, null);
+
+ // Delete contained photos for each album.
+ if (cursor.moveToFirst()) {
+ do {
+ deleteAlbumPhotos(db, cursor.getLong(0));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+
+ // Delete all albums.
+ db.delete(albumTableName, WHERE_ACCOUNT, whereArgs);
+
+ // Delete the user entry.
+ db.delete(UserEntry.SCHEMA.getTableName(), "account=?", whereArgs);
+ }
+
+ private void deleteAlbum(SQLiteDatabase db, long albumId) {
+ // Delete contained photos.
+ deleteAlbumPhotos(db, albumId);
+
+ // Delete the album.
+ AlbumEntry.SCHEMA.deleteWithId(db, albumId);
+ }
+
+ private void deleteAlbumPhotos(SQLiteDatabase db, long albumId) {
+ Log.v(TAG, "deleteAlbumPhotos(" + albumId + ")");
+ String photoTableName = PhotoEntry.SCHEMA.getTableName();
+ String[] whereArgs = { Long.toString(albumId) };
+ Cursor cursor = db.query(photoTableName,
+ Entry.ID_PROJECTION, WHERE_ALBUM_ID, whereArgs, null, null, null);
+
+ // Delete cache entry for each photo.
+ if (cursor.moveToFirst()) {
+ do {
+ deletePhotoCache(cursor.getLong(0));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+
+ // Delete all photos.
+ db.delete(photoTableName, WHERE_ALBUM_ID, whereArgs);
+ }
+
+ private void deletePhoto(SQLiteDatabase db, long photoId) {
+ PhotoEntry.SCHEMA.deleteWithId(db, photoId);
+ deletePhotoCache(photoId);
+ }
+
+ private void deletePhotoCache(long photoId) {
+ // TODO: implement it.
+ }
+
+ private final class SyncContext {
+ // List of all authenticated user accounts.
+ public PicasaApi.AuthAccount[] accounts;
+
+ // A connection to the Picasa API for a specific user account. Initially
+ // null.
+ public PicasaApi api = new PicasaApi();
+
+ // A handle to the Picasa databse.
+ public SQLiteDatabase db;
+
+ // List of album IDs that were added during the sync.
+ public final ArrayList<Long> albumsAdded = new ArrayList<Long>();
+
+ // Set to true if albums were changed.
+ public boolean albumsChanged = false;
+
+ // Set to true if photos were changed.
+ public boolean photosChanged = false;
+
+ public SyncContext() {
+ db = mDatabase.getWritableDatabase();
+ }
+
+ public void reloadAccounts() {
+ accounts = PicasaApi.getAuthenticatedAccounts(getContext());
+ }
+
+ public void finish() {
+ // Send notifications if needed and reset state.
+ ContentResolver cr = getContext().getContentResolver();
+ if (albumsChanged) {
+ cr.notifyChange(ALBUMS_URI, null, false);
+ }
+ if (photosChanged) {
+ cr.notifyChange(PHOTOS_URI, null, false);
+ }
+ albumsChanged = false;
+ photosChanged = false;
+ }
+
+ public boolean login(String user) {
+ if (accounts == null) {
+ reloadAccounts();
+ }
+ final PicasaApi.AuthAccount[] authAccounts = accounts;
+ for (PicasaApi.AuthAccount auth : authAccounts) {
+ if (auth.user.equals(user)) {
+ api.setAuth(auth);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Minimal metadata gathered during sync.
+ */
+ private static final class EntryMetadata implements Comparable<EntryMetadata> {
+ public long id;
+ public long dateEdited;
+ public int displayIndex;
+ public boolean survived = false;
+
+ public EntryMetadata() {
+ }
+
+ public EntryMetadata(long id, long dateEdited, int displayIndex) {
+ this.id = id;
+ this.dateEdited = dateEdited;
+ this.displayIndex = displayIndex;
+ }
+
+ public int compareTo(EntryMetadata other) {
+ return Long.signum(id - other.id);
+ }
+
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PicasaReceiver.java b/new3d/src/com/android/gallery3d/picasa/PicasaReceiver.java
new file mode 100644
index 0000000..3e28820
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PicasaReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class PicasaReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "PicasaRecevier";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(TAG, "Accounts changed: " + intent);
+ }
+
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PicasaService.java b/new3d/src/com/android/gallery3d/picasa/PicasaService.java
new file mode 100644
index 0000000..bc2adc7
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PicasaService.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class PicasaService extends Service {
+ public static final String ACTION_SYNC = "com.android.gallery3d.picasa.action.SYNC";
+ public static final String ACTION_PERIODIC_SYNC =
+ "com.android.gallery3d.picasa.action.PERIODIC_SYNC";
+ public static final String ACCOUNT_TYPE = "com.google";
+ public static final String SERVICE_NAME = "lh2";
+ public static final String FEATURE_SERVICE_NAME = "service_" + SERVICE_NAME;
+ public static final String KEY_TYPE = "com.android.gallery3d.SYNC_TYPE";
+ public static final String KEY_ID = "com.android.gallery3d.SYNC_ID";
+ public static final int TYPE_USERS = 0;
+ public static final int TYPE_USERS_ALBUMS = 1;
+ public static final int TYPE_ALBUM_PHOTOS = 2;
+
+ private final HandlerThread mSyncThread = new HandlerThread("PicasaSyncThread");
+ private final Handler mSyncHandler;
+ private static final AtomicBoolean sSyncPending = new AtomicBoolean(false);
+
+ public static void requestSync(Context context, int type, long id) {
+ Bundle extras = new Bundle();
+ extras.putInt(KEY_TYPE, type);
+ extras.putLong(KEY_ID, id);
+
+ Account[] accounts = PicasaApi.getAccounts(context);
+ for (Account account : accounts) {
+ ContentResolver.requestSync(account, PicasaContentProvider.AUTHORITY, extras);
+ }
+
+ // context.startService(new Intent(context,
+ // PicasaService.class).putExtras(extras));
+ }
+
+ public PicasaService() {
+ mSyncThread.start();
+ mSyncHandler = new Handler(mSyncThread.getLooper());
+ mSyncHandler.post(new Runnable() {
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ }
+ });
+ }
+
+ private static PicasaContentProvider getContentProvider(Context context) {
+ ContentResolver cr = context.getContentResolver();
+ ContentProviderClient client =
+ cr.acquireContentProviderClient(PicasaContentProvider.AUTHORITY);
+ return (PicasaContentProvider) client.getLocalContentProvider();
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, int flags, final int startId) {
+ mSyncHandler.post(new Runnable() {
+ public void run() {
+ performSync(PicasaService.this, null, intent.getExtras(), new SyncResult());
+ stopSelf(startId);
+ }
+ });
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new PicasaSyncAdapter(getApplicationContext()).getSyncAdapterBinder();
+ }
+
+ @Override
+ public void onDestroy() {
+ mSyncThread.quit();
+ }
+
+ public static boolean performSync(
+ Context context, Account account, Bundle extras, SyncResult syncResult) {
+ // Skip if another sync is pending.
+ if (!sSyncPending.compareAndSet(false, true)) {
+ return false;
+ }
+
+ // Perform the sync.
+ performSyncImpl(context, account, extras, syncResult);
+
+ // Mark sync as complete and notify all waiters.
+ sSyncPending.set(false);
+ synchronized (sSyncPending) {
+ sSyncPending.notifyAll();
+ }
+ return true;
+ }
+
+ public static void waitForPerformSync() {
+ synchronized (sSyncPending) {
+ while (sSyncPending.get()) {
+ try {
+ // Wait for the sync to complete.
+ sSyncPending.wait();
+ } catch (InterruptedException e) {
+ // Stop waiting if interrupted.
+ break;
+ }
+ }
+ }
+ }
+
+ private static void performSyncImpl(
+ Context context, Account account, Bundle extras, SyncResult syncResult) {
+ // Initialize newly added accounts to sync by default.
+ String authority = PicasaContentProvider.AUTHORITY;
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
+ if (account != null && ContentResolver.getIsSyncable(account, authority) < 0) {
+ try {
+ ContentResolver.setIsSyncable(
+ account, authority, getIsSyncable(context, account) ? 1 : 0);
+ } catch (OperationCanceledException e) {
+ } catch (IOException e) {
+ }
+ }
+ return;
+ }
+
+ // Do nothing if sync is disabled for this account. TODO: is this
+ // blocked in PicasaContentProvider too?
+ if (account != null && ContentResolver.getIsSyncable(account, authority) < 0) {
+ ++syncResult.stats.numSkippedEntries;
+ return;
+ }
+
+ // Get the type of sync operation and the entity ID, if applicable.
+ // Default to synchronize all.
+ int type = extras.getInt(PicasaService.KEY_TYPE, PicasaService.TYPE_USERS_ALBUMS);
+ long id = extras.getLong(PicasaService.KEY_ID, -1);
+
+ // Get the content provider instance and reload the list of user
+ // accounts.
+ PicasaContentProvider provider = getContentProvider(context);
+ provider.reloadAccounts();
+
+ // Restrict sync to either a specific account or all accounts.
+ provider.setActiveSyncAccount(account);
+
+ // Perform the desired sync operation.
+ switch (type) {
+ case PicasaService.TYPE_USERS:
+ provider.syncUsers(syncResult);
+ break;
+ case PicasaService.TYPE_USERS_ALBUMS:
+ provider.syncUsersAndAlbums(true, syncResult);
+ break;
+ case PicasaService.TYPE_ALBUM_PHOTOS:
+ provider.syncAlbumPhotos(id, true, syncResult);
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private static boolean getIsSyncable(Context context, Account account)
+ throws IOException, OperationCanceledException {
+ try {
+ Account[] picasaAccounts = AccountManager.get(context).getAccountsByTypeAndFeatures(
+ ACCOUNT_TYPE, new String[] { FEATURE_SERVICE_NAME },
+ null /* callback */, null /* handler */).getResult();
+ for (Account picasaAccount : picasaAccounts) {
+ if (account.equals(picasaAccount)) {
+ return true;
+ }
+ }
+ return false;
+ } catch (AuthenticatorException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/PicasaSyncAdapter.java b/new3d/src/com/android/gallery3d/picasa/PicasaSyncAdapter.java
new file mode 100644
index 0000000..5d5ae60
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/PicasaSyncAdapter.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.util.Log;
+
+public class PicasaSyncAdapter extends AbstractThreadedSyncAdapter {
+ private final Context mContext;
+ public final static String TAG = "PicasaSyncAdapter";
+
+ public PicasaSyncAdapter(Context applicationContext) {
+ super(applicationContext, false);
+ mContext = applicationContext;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority,
+ ContentProviderClient providerClient, SyncResult syncResult) {
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
+ try {
+ Account[] picasaAccounts = AccountManager.get(getContext())
+ .getAccountsByTypeAndFeatures(
+ PicasaService.ACCOUNT_TYPE,
+ new String[] { PicasaService.FEATURE_SERVICE_NAME },
+ null /* callback */, null /* handler */).getResult();
+ boolean isPicasaAccount = false;
+ for (Account picasaAccount : picasaAccounts) {
+ if (account.equals(picasaAccount)) {
+ isPicasaAccount = true;
+ break;
+ }
+ }
+ if (isPicasaAccount) {
+ ContentResolver.setIsSyncable(account, authority, 1);
+ ContentResolver.setSyncAutomatically(account, authority, true);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "cannot do sync", e);
+ }
+ return;
+ }
+ try {
+ PicasaService.performSync(mContext, account, extras, syncResult);
+ } catch (Exception e) {
+ // Report an error
+ ++syncResult.stats.numIoExceptions;
+ }
+ }
+
+ public static final class AccountChangeReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // TODO: Need to get account list change broadcast.
+ }
+
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/TableContentProvider.java b/new3d/src/com/android/gallery3d/picasa/TableContentProvider.java
new file mode 100644
index 0000000..8f8d9fd
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/TableContentProvider.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+public class TableContentProvider extends ContentProvider {
+ private static final String NULL_COLUMN_HACK = "_id";
+ protected SQLiteOpenHelper mDatabase = null;
+ private final UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ private final ArrayList<Mapping> mMappings = new ArrayList<Mapping>();
+
+ public void setDatabase(SQLiteOpenHelper database) {
+ mDatabase = database;
+ }
+
+ public void addMapping(String authority, String path, String mimeSubtype, EntrySchema table) {
+ // Add the table URI mapping.
+ ArrayList<Mapping> mappings = mMappings;
+ UriMatcher matcher = mUriMatcher;
+ matcher.addURI(authority, path, mappings.size());
+ mappings.add(new Mapping(table, mimeSubtype, false));
+
+ // Add the row URI mapping.
+ matcher.addURI(authority, path + "/#", mappings.size());
+ mappings.add(new Mapping(table, mimeSubtype, true));
+ }
+
+ @Override
+ public boolean onCreate() {
+ // The database may not be loaded yet since attachInfo() has not been
+ // called, so we cannot
+ // check that the database opened successfully. Returns true
+ // optimistically.
+ return true;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Combine the standard type with the user-provided subtype.
+ Mapping mapping = mMappings.get(match);
+ String prefix = mapping.hasId
+ ? ContentResolver.CURSOR_ITEM_BASE_TYPE
+ : ContentResolver.CURSOR_DIR_BASE_TYPE;
+ return prefix + "/" + mapping.mimeSubtype;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Add the ID predicate if needed.
+ Mapping mapping = mMappings.get(match);
+ if (mapping.hasId) {
+ selection = whereWithId(uri, selection);
+ }
+
+ // System.out.println("QUERY " + uri + " WHERE (" + selection + ")");
+
+ // Run the query.
+ String tableName = mapping.table.getTableName();
+ Cursor cursor = mDatabase.getReadableDatabase().query(
+ tableName, projection, selection, selectionArgs, null, null, sortOrder);
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+ return cursor;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ Mapping mapping = match != UriMatcher.NO_MATCH ? mMappings.get(match) : null;
+ if (mapping == null || mapping.hasId) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Insert into the database, notify observers, and return the qualified
+ // URI.
+ String tableName = mapping.table.getTableName();
+ long rowId = mDatabase.getWritableDatabase().insert(tableName, NULL_COLUMN_HACK, values);
+ if (rowId > 0) {
+ notifyChange(uri);
+ return Uri.withAppendedPath(uri, Long.toString(rowId));
+ } else {
+ throw new SQLException("Failed to insert row at: " + uri);
+ }
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ Mapping mapping = match != UriMatcher.NO_MATCH ? mMappings.get(match) : null;
+ if (mapping == null || mapping.hasId) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Insert all rows into the database and notify observers.
+ String tableName = mapping.table.getTableName();
+ SQLiteDatabase database = mDatabase.getWritableDatabase();
+ int numInserted = 0;
+ try {
+ int length = values.length;
+ database.beginTransaction();
+ for (int i = 0; i != length; ++i) {
+ database.insert(tableName, NULL_COLUMN_HACK, values[i]);
+ }
+ database.setTransactionSuccessful();
+ numInserted = length;
+ } finally {
+ database.endTransaction();
+ }
+ notifyChange(uri);
+ return numInserted;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Add the ID predicate if needed.
+ Mapping mapping = mMappings.get(match);
+ if (mapping.hasId) {
+ selection = whereWithId(uri, selection);
+ }
+
+ // Update the item(s) and broadcast a change notification.
+ SQLiteDatabase db = mDatabase.getWritableDatabase();
+ String tableName = mapping.table.getTableName();
+ int count = db.update(tableName, values, selection, selectionArgs);
+ notifyChange(uri);
+ return count;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ // Resolve the URI.
+ int match = mUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+
+ // Add the ID predicate if needed.
+ Mapping mapping = mMappings.get(match);
+ if (mapping.hasId) {
+ selection = whereWithId(uri, selection);
+ }
+
+ // Delete the item(s) and broadcast a change notification.
+ SQLiteDatabase db = mDatabase.getWritableDatabase();
+ String tableName = mapping.table.getTableName();
+ int count = db.delete(tableName, selection, selectionArgs);
+ notifyChange(uri);
+ return count;
+ }
+
+ private final String whereWithId(Uri uri, String selection) {
+ String id = uri.getPathSegments().get(1);
+ StringBuilder where = new StringBuilder("_id=");
+ where.append(id);
+ if (!TextUtils.isEmpty(selection)) {
+ where.append(" AND (");
+ where.append(selection);
+ where.append(')');
+ }
+ return where.toString();
+ }
+
+ private final void notifyChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+
+ private static final class Mapping {
+ public EntrySchema table;
+ public String mimeSubtype;
+ public boolean hasId;
+
+ public Mapping(EntrySchema table, String mimeSubtype, boolean hasId) {
+ this.table = table;
+ this.mimeSubtype = mimeSubtype;
+ this.hasId = hasId;
+ }
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/picasa/UserEntry.java b/new3d/src/com/android/gallery3d/picasa/UserEntry.java
new file mode 100644
index 0000000..a0c86d1
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/picasa/UserEntry.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.picasa;
+
+import org.xml.sax.Attributes;
+
+@Entry.Table("users")
+public final class UserEntry extends Entry {
+ public static final EntrySchema SCHEMA = new EntrySchema(UserEntry.class);
+
+ @Column("account")
+ public String account;
+
+ @Column("albums_etag")
+ public String albumsEtag;
+
+ @Override
+ public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+ }
+}