Initial draft of new SyncAdapter sample
Implements one-way sync with Android Developer Blog via
Atom feed.
UI will need to be improved, once final look and feel for
samples has been finalized.
Change-Id: I2c792860dfde40ac32d0836793ec15649f4e6267
diff --git a/networking/sync/BasicSyncAdapter/AndroidManifest.xml b/networking/sync/BasicSyncAdapter/AndroidManifest.xml
new file mode 100644
index 0000000..f8434bb
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/AndroidManifest.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.network.sync.basicsyncadapter"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <!-- SyncAdapters are available in API 5 and above. We use API 7 as a baseline for samples. -->
+ <uses-sdk
+ android:minSdkVersion="7"
+ android:targetSdkVersion="17" />
+
+ <!-- Required for fetching feed data. -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <!-- Required to register a SyncStatusObserver to display a "syncing..." progress indicator. -->
+ <uses-permission android:name="android.permission.READ_SYNC_STATS"/>
+ <!-- Required to enable our SyncAdapter after it's created. -->
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+ <!-- Required because we're manually creating a new account. -->
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+
+ <!-- Main activity, responsible for showing a list of feed entries. -->
+ <activity
+ android:name="com.example.android.network.sync.basicsyncadapter.EntryListActivity"
+ android:label="@string/app_name" >
+ <!-- This intent filter places this activity in the system's app launcher. -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <!-- ContentProvider to store feed data.
+
+ The "authorities" here are defined as part of a ContentProvider interface. It's used here
+ as an attachment point for the SyncAdapter. See res/xml/syncadapter.xml and
+ SyncService.java.
+
+ Since this ContentProvider is not exported, it will not be accessible outside of this app's
+ package. -->
+ <provider
+ android:name=".provider.FeedProvider"
+ android:authorities="com.example.android.network.sync.basicsyncadapter"
+ android:exported="false" />
+
+ <!-- This service implements our SyncAdapter. It needs to be exported, so that the system
+ sync framework can access it. -->
+ <service android:name=".SyncService"
+ android:exported="true">
+ <!-- This intent filter is required. It allows the system to launch our sync service
+ as needed. -->
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <!-- This points to a required XML file which describes our SyncAdapter. -->
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter" />
+ </service>
+
+ <!-- This implements the account we'll use as an attachment point for our SyncAdapter. Since
+ our SyncAdapter doesn't need to authenticate the current user (it just fetches a public RSS
+ feed), this account's implementation is largely empty.
+
+ It's also possible to attach a SyncAdapter to an existing account provided by another
+ package. In that case, this element could be omitted here. -->
+ <service android:name=".accounts.GenericAccountService">
+ <!-- Required filter used by the system to launch our account service. -->
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <!-- This points to an XMLf ile which describes our account service. -->
+ <meta-data android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator" />
+ </service>
+
+</application>
+
+</manifest>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/AndroidManifest.xml b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/AndroidManifest.xml
new file mode 100644
index 0000000..91c9861
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.network.sync.basicsyncadapter.tests"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <uses-sdk
+ android:minSdkVersion="11"
+ android:targetSdkVersion="17" />
+
+ <!-- We add an application tag here just so that we can indicate that
+ this package needs to link against the android.test library,
+ which is needed when building test cases. -->
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+ <!--
+ This declares that this application uses the instrumentation test runner targeting
+ the package of com.android.example.FeedSyncSampleTo run the tests use the command:
+ "adb shell am instrument -w com.android.example.FeedSyncSamplests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.example.android.network.sync.basicsyncadapter"
+ android:label="Tests for com.example.android.network.sync.BasicSyncAdapter"/>
+</manifest>
diff --git a/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/proguard-project.txt b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/SyncAdapterTest.java b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/SyncAdapterTest.java
new file mode 100644
index 0000000..820882d
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/SyncAdapterTest.java
@@ -0,0 +1,73 @@
+package com.example.android.network.sync.basicsyncadapter;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.test.ServiceTestCase;
+
+import com.example.android.network.sync.basicsyncadapter.provider.FeedContract;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+
+public class SyncAdapterTest extends ServiceTestCase<SyncService> {
+ public SyncAdapterTest() {
+ super(SyncService.class);
+ }
+
+ public void testIncomingFeedParsed()
+ throws IOException, XmlPullParserException, RemoteException,
+ OperationApplicationException, ParseException {
+ String sampleFeed = "<?xml version=\"1.0\"?>\n" +
+ "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +
+ " \n" +
+ " <title>Sample Blog</title>\n" +
+ " <link href=\"http://example.com/\"/>\n" +
+ " <link type=\"application/atom+xml\" rel=\"self\" href=\"http://example.xom/feed.xml\"/>\n" +
+ " <updated>2013-05-16T16:53:23-07:00</updated>\n" +
+ " <id>http://example.com/</id>\n" +
+ " <author>\n" +
+ " <name>Rick Deckard</name>\n" +
+ " <email>deckard@example.com</email>\n" +
+ " </author>\n" +
+ "\n" +
+ " <entry>\n" +
+ " <id>http://example.com/2012/10/20/test-post</id>\n" +
+ " <link type=\"text/html\" rel=\"alternate\" href=\"http://example.com/2012/10/20/test-post.html\"/>\n" +
+ " <title>Test Post #1</title>\n" +
+ " <published>2012-10-20T00:00:00-07:00</published>\n" +
+ " <updated>2012-10-20T00:00:00-07:00</updated>\n" +
+ " <author>\n" +
+ " <name>Rick Deckard</name>\n" +
+ " <uri>http://example.com/</uri>\n" +
+ " </author>\n" +
+ " <summary>This is a sample summary.</summary>\n" +
+ " <content type=\"html\">Here's some <em>sample</em> content.</content>\n" +
+ " </entry>\n" +
+ "</feed>\n";
+ InputStream stream = new ByteArrayInputStream(sampleFeed.getBytes());
+ SyncAdapter adapter = new SyncAdapter(getContext(), false);
+ adapter.updateLocalFeedData(stream, new SyncResult());
+
+ Context ctx = getContext();
+ assert ctx != null;
+ ContentResolver cr = ctx.getContentResolver();
+ final String[] projection = {FeedContract.Entry.COLUMN_NAME_ENTRY_ID,
+ FeedContract.Entry.COLUMN_NAME_TITLE,
+ FeedContract.Entry.COLUMN_NAME_LINK};
+ Cursor c = cr.query(FeedContract.Entry.CONTENT_URI, projection, null, null, null);
+ assert c != null;
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertEquals("http://example.com/2012/10/20/test-post", c.getString(0));
+ assertEquals("Test Post #1", c.getString(1));
+ assertEquals("http://example.com/2012/10/20/test-post.html", c.getString(2));
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/net/FeedParserTest.java b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/net/FeedParserTest.java
new file mode 100644
index 0000000..0c66871
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/net/FeedParserTest.java
@@ -0,0 +1,21 @@
+package com.example.android.network.sync.basicsyncadapter.net;
+
+import junit.framework.TestCase;
+
+public class FeedParserTest extends TestCase {
+ public FeedParserTest() {
+ super();
+ }
+
+// public void testEntriesEqualById() {
+// FeedParser.Entry e1 = new FeedParser.Entry("alpha", "Aardvark", "Bear", "Cat");
+// FeedParser.Entry e2 = new FeedParser.Entry("alpha", "Dog", "Elephant", "Faun");
+// assertEquals(e1, e2);
+// }
+//
+// public void testEntriesHashById() {
+// FeedParser.Entry e1 = new FeedParser.Entry("alpha", "Aardvark", "Bear", "Cat");
+// FeedParser.Entry e2 = new FeedParser.Entry("alpha", "Dog", "Elephant", "Faun");
+// assertEquals(e1.hashCode(), e2.hashCode());
+// }
+}
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProviderTest.java b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProviderTest.java
new file mode 100644
index 0000000..a80b5ca
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/BasicSyncAdapterTests/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProviderTest.java
@@ -0,0 +1,119 @@
+package com.example.android.network.sync.basicsyncadapter.provider;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.ProviderTestCase2;
+
+public class FeedProviderTest extends ProviderTestCase2<FeedProvider> {
+ public FeedProviderTest() {
+ super(FeedProvider.class, FeedContract.CONTENT_AUTHORITY);
+ }
+
+ public void testEntryContentUriIsSane() {
+ assertEquals(Uri.parse("content://com.example.android.network.sync.basicsyncadapter/entries"),
+ FeedContract.Entry.CONTENT_URI);
+ }
+
+ public void testCreateAndRetrieve() {
+ // Create
+ ContentValues newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_TITLE, "MyTitle");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://example.com");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, "MyEntryID");
+ Uri newUri = getMockContentResolver().insert(
+ FeedContract.Entry.CONTENT_URI,
+ newValues);
+
+ // Retrieve
+ String[] projection = {
+ FeedContract.Entry.COLUMN_NAME_TITLE, // 0
+ FeedContract.Entry.COLUMN_NAME_LINK, // 1
+ FeedContract.Entry.COLUMN_NAME_ENTRY_ID}; // 2
+ Cursor c = getMockContentResolver().query(newUri, projection, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertEquals("MyTitle", c.getString(0));
+ assertEquals("http://example.com", c.getString(1));
+ assertEquals("MyEntryID", c.getString(2));
+ }
+
+ public void testCreateAndQuery() {
+ // Create
+ ContentValues newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_TITLE, "Alpha-MyTitle");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://alpha.example.com");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, "Alpha-MyEntryID");
+ getMockContentResolver().insert(
+ FeedContract.Entry.CONTENT_URI,
+ newValues);
+
+ newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_TITLE, "Beta-MyTitle");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://beta.example.com");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, "Beta-MyEntryID");
+ getMockContentResolver().insert(
+ FeedContract.Entry.CONTENT_URI,
+ newValues);
+
+ // Retrieve
+ String[] projection = {
+ FeedContract.Entry.COLUMN_NAME_TITLE, // 0
+ FeedContract.Entry.COLUMN_NAME_LINK, // 1
+ FeedContract.Entry.COLUMN_NAME_ENTRY_ID}; // 2
+ String where = FeedContract.Entry.COLUMN_NAME_TITLE + " LIKE ?";
+ Cursor c = getMockContentResolver().query(FeedContract.Entry.CONTENT_URI, projection,
+ where, new String[] {"Alpha%"}, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertEquals("Alpha-MyTitle", c.getString(0));
+ assertEquals("http://alpha.example.com", c.getString(1));
+ assertEquals("Alpha-MyEntryID", c.getString(2));
+ }
+
+ public void testUpdate() {
+ // Create
+ ContentValues newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_TITLE, "Alpha-MyTitle");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://alpha.example.com");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, "Alpha-MyEntryID");
+ Uri alpha = getMockContentResolver().insert(
+ FeedContract.Entry.CONTENT_URI,
+ newValues);
+
+ newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_TITLE, "Beta-MyTitle");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://beta.example.com");
+ newValues.put(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, "Beta-MyEntryID");
+ Uri beta = getMockContentResolver().insert(
+ FeedContract.Entry.CONTENT_URI,
+ newValues);
+
+ // Update
+ newValues = new ContentValues();
+ newValues.put(FeedContract.Entry.COLUMN_NAME_LINK, "http://replaced.example.com");
+ getMockContentResolver().update(alpha, newValues, null, null);
+
+ // Retrieve
+ String[] projection = {
+ FeedContract.Entry.COLUMN_NAME_TITLE, // 0
+ FeedContract.Entry.COLUMN_NAME_LINK, // 1
+ FeedContract.Entry.COLUMN_NAME_ENTRY_ID}; // 2
+ // Check that alpha was updated
+ Cursor c = getMockContentResolver().query(alpha, projection, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertEquals("Alpha-MyTitle", c.getString(0));
+ assertEquals("http://replaced.example.com", c.getString(1));
+ assertEquals("Alpha-MyEntryID", c.getString(2));
+
+ // ...and that beta was not
+ c = getMockContentResolver().query(beta, projection, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertEquals("Beta-MyTitle", c.getString(0));
+ assertEquals("http://beta.example.com", c.getString(1));
+ assertEquals("Beta-MyEntryID", c.getString(2));
+ }
+
+}
diff --git a/networking/sync/BasicSyncAdapter/libs/android-support-v4.jar b/networking/sync/BasicSyncAdapter/libs/android-support-v4.jar
new file mode 100644
index 0000000..4846ef9
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/libs/android-support-v4.jar
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/libs/guava-14.0.1.jar b/networking/sync/BasicSyncAdapter/libs/guava-14.0.1.jar
new file mode 100644
index 0000000..3a3d925
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/libs/guava-14.0.1.jar
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/proguard-project.txt b/networking/sync/BasicSyncAdapter/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/networking/sync/BasicSyncAdapter/res/drawable-hdpi/ic_launcher.png b/networking/sync/BasicSyncAdapter/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a0f7005
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/res/drawable-mdpi/ic_launcher.png b/networking/sync/BasicSyncAdapter/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a085462
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_action_refresh.png b/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_action_refresh.png
new file mode 100644
index 0000000..4f5d255
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_action_refresh.png
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_launcher.png b/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4f78eb8
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/res/drawable-xxhdpi/ic_launcher.png b/networking/sync/BasicSyncAdapter/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b198ee3
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/networking/sync/BasicSyncAdapter/res/layout/actionbar_indeterminate_progress.xml b/networking/sync/BasicSyncAdapter/res/layout/actionbar_indeterminate_progress.xml
new file mode 100644
index 0000000..b254013
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/layout/actionbar_indeterminate_progress.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2012 Google Inc.
+
+ 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.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/action_button_min_width"
+ android:minWidth="@dimen/action_button_min_width">
+
+ <ProgressBar android:layout_width="@dimen/indeterminate_progress_size"
+ android:layout_height="@dimen/indeterminate_progress_size"
+ android:layout_gravity="center"
+ style="?indeterminateProgressStyle" />
+</FrameLayout>
diff --git a/networking/sync/BasicSyncAdapter/res/layout/activity_entry_list.xml b/networking/sync/BasicSyncAdapter/res/layout/activity_entry_list.xml
new file mode 100644
index 0000000..6e3e2fd
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/layout/activity_entry_list.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<fragment xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/entry_list"
+ android:name="com.example.android.network.sync.basicsyncadapter.EntryListFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ tools:context=".EntryListActivity"
+ tools:layout="@android:layout/list_content" />
diff --git a/networking/sync/BasicSyncAdapter/res/menu/main.xml b/networking/sync/BasicSyncAdapter/res/menu/main.xml
new file mode 100644
index 0000000..63ad3d1
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/menu/main.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/menu_refresh"
+ android:icon="@drawable/ic_action_refresh"
+ android:title="@string/description_refresh"
+ android:orderInCategory="1"
+ android:showAsAction="always" />
+</menu>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values-v11/styles.xml b/networking/sync/BasicSyncAdapter/res/values-v11/styles.xml
new file mode 100644
index 0000000..ff65301
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values-v11/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+
+ <!--
+ Base application theme for API 11+. This theme completely replaces
+ AppBaseTheme from res/values/styles.xml on API 11+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values-v14/styles.xml b/networking/sync/BasicSyncAdapter/res/values-v14/styles.xml
new file mode 100644
index 0000000..a4a443a
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values-v14/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+
+ <!--
+ Base application theme for API 14+. This theme completely replaces
+ AppBaseTheme from BOTH res/values/styles.xml and
+ res/values-v11/styles.xml on API 14+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values/attrs.xml b/networking/sync/BasicSyncAdapter/res/values/attrs.xml
new file mode 100644
index 0000000..6c15504
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+ <!-- Specifies a style resource to use for an indeterminate progress spinner. -->
+ <attr name="indeterminateProgressStyle" format="reference"/>
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values/dimen.xml b/networking/sync/BasicSyncAdapter/res/values/dimen.xml
new file mode 100644
index 0000000..d838c69
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values/dimen.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+ <dimen name="action_button_min_width">56dp</dimen>
+ <dimen name="indeterminate_progress_size">32dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values/strings.xml b/networking/sync/BasicSyncAdapter/res/values/strings.xml
new file mode 100644
index 0000000..0271850
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+ <string name="app_name">FeedSync Sample</string>
+ <string name="account_name">FeedSync Service</string>
+ <string name="title_entry_detail">Entry Detail</string>
+ <string name="loading">Waiting for sync...</string>
+ <string name="description_refresh">Refresh</string>
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/values/styles.xml b/networking/sync/BasicSyncAdapter/res/values/styles.xml
new file mode 100644
index 0000000..43a8f2b
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/values/styles.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/res/xml/authenticator.xml b/networking/sync/BasicSyncAdapter/res/xml/authenticator.xml
new file mode 100644
index 0000000..cb69a66
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/xml/authenticator.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="com.example.android.network.sync.basicsyncadapter"
+ android:icon="@drawable/ic_launcher"
+ android:smallIcon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ />
diff --git a/networking/sync/BasicSyncAdapter/res/xml/syncadapter.xml b/networking/sync/BasicSyncAdapter/res/xml/syncadapter.xml
new file mode 100644
index 0000000..0fcd6e3
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/res/xml/syncadapter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2013 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.
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.example.android.network.sync.basicsyncadapter"
+ android:accountType="com.example.android.network.sync.basicsyncadapter"
+ android:userVisible="false"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"
+ />
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListActivity.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListActivity.java
new file mode 100644
index 0000000..cff0702
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListActivity.java
@@ -0,0 +1,16 @@
+package com.example.android.network.sync.basicsyncadapter;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+/**
+ * Activity for holding EntryListFragment.
+ */
+public class EntryListActivity extends FragmentActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_entry_list);
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListFragment.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListFragment.java
new file mode 100644
index 0000000..b1f55b6
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/EntryListFragment.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SyncStatusObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.example.android.network.sync.basicsyncadapter.accounts.GenericAccountService;
+import com.example.android.network.sync.basicsyncadapter.provider.FeedContract;
+
+/**
+ * List fragment containing a list of Atom entry objects (articles) stored in the local database.
+ *
+ * <p>Database access is mediated by a content provider, specified in
+ * {@link com.example.android.network.sync.basicsyncadapter.provider.FeedProvider}. This content
+ * provider is
+ * automatically populated by {@link SyncService}.
+ *
+ * <p>Selecting an item from the displayed list displays the article in the default browser.
+ *
+ * <p>If the content provider doesn't return any data, then the first sync hasn't run yet. This sync
+ * adapter assumes data exists in the provider once a sync has run. If your app doesn't work like
+ * this, you should add a flag that notes if a sync has run, so you can differentiate between "no
+ * available data" and "no initial sync", and display this in the UI.
+ *
+ * <p>The ActionBar displays a "Refresh" button. When the user clicks "Refresh", the sync adapter
+ * runs immediately. An indeterminate ProgressBar element is displayed, showing that the sync is
+ * occurring.
+ */
+public class EntryListFragment extends ListFragment
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ private static final String TAG = "EntryListFragment";
+
+ /**
+ * Cursor adapter for controlling ListView results.
+ */
+ private SimpleCursorAdapter mAdapter;
+
+ /**
+ * Handle to a SyncObserver. The ProgressBar element is visible until the SyncObserver reports
+ * that the sync is complete.
+ *
+ * <p>This allows us to delete our SyncObserver once the application is no longer in the
+ * foreground.
+ */
+ private Object mSyncObserverHandle;
+
+ /**
+ * Options menu used to populate ActionBar.
+ */
+ private Menu mOptionsMenu;
+
+ /**
+ * Projection for querying the content provider.
+ */
+ private static final String[] PROJECTION = new String[]{
+ FeedContract.Entry._ID,
+ FeedContract.Entry.COLUMN_NAME_TITLE,
+ FeedContract.Entry.COLUMN_NAME_LINK,
+ FeedContract.Entry.COLUMN_NAME_PUBLISHED
+ };
+
+ // Column indexes. The index of a column in the Cursor is the same as its relative position in
+ // the projection.
+ /** Column index for _ID */
+ private static final int COLUMN_ID = 0;
+ /** Column index for title */
+ private static final int COLUMN_TITLE = 1;
+ /** Column index for link */
+ private static final int COLUMN_URL_STRING = 2;
+ /** Column index for published */
+ private static final int COLUMN_PUBLISHED = 3;
+
+ /**
+ * List of Cursor columns to read from when preparing an adapter to populate the ListView.
+ */
+ private static final String[] FROM_COLUMNS = new String[]{
+ FeedContract.Entry.COLUMN_NAME_TITLE,
+ FeedContract.Entry.COLUMN_NAME_PUBLISHED
+ };
+
+ /**
+ * List of Views which will be populated by Cursor data.
+ */
+ private static final int[] TO_FIELDS = new int[]{
+ android.R.id.text1,
+ android.R.id.text2};
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public EntryListFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * Create SyncAccount at launch, if needed.
+ *
+ * <p>This will create a new account with the system for our application, register our
+ * {@link SyncService} with it, and establish a sync schedule.
+ */
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ // Create account, if needed
+ SyncUtils.CreateSyncAccount(activity);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mAdapter = new SimpleCursorAdapter(
+ getActivity(), // Current context
+ android.R.layout.simple_list_item_activated_2, // Layout for individual rows
+ null, // Cursor
+ FROM_COLUMNS, // Cursor columns to use
+ TO_FIELDS, // Layout fields to use
+ 0 // No flags
+ );
+ mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
+ @Override
+ public boolean setViewValue(View view, Cursor cursor, int i) {
+ if (i == COLUMN_PUBLISHED) {
+ // Convert timestamp to human-readable date
+ Time t = new Time();
+ t.set(cursor.getLong(i));
+ ((TextView) view).setText(t.format("%Y-%m-%d %H:%M"));
+ return true;
+ } else {
+ // Let SimpleCursorAdapter handle other fields automatically
+ return false;
+ }
+ }
+ });
+ setListAdapter(mAdapter);
+ setEmptyText(getText(R.string.loading));
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mSyncStatusObserver.onStatusChanged(0);
+
+ // Watch for sync state changes
+ final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING |
+ ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
+ mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mSyncObserverHandle != null) {
+ ContentResolver.removeStatusChangeListener(mSyncObserverHandle);
+ mSyncObserverHandle = null;
+ }
+ }
+
+ /**
+ * Query the content provider for data.
+ *
+ * <p>Loaders do queries in a background thread. They also provide a ContentObserver that is
+ * triggered when data in the content provider changes. When the sync adapter updates the
+ * content provider, the ContentObserver responds by resetting the loader and then reloading
+ * it.
+ */
+ @Override
+ public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
+ // We only have one loader, so we can ignore the value of i.
+ // (It'll be '0', as set in onCreate().)
+ return new CursorLoader(getActivity(), // Context
+ FeedContract.Entry.CONTENT_URI, // URI
+ PROJECTION, // Projection
+ null, // Selection
+ null, // Selection args
+ FeedContract.Entry.COLUMN_NAME_PUBLISHED + " desc"); // Sort
+ }
+
+ /**
+ * Move the Cursor returned by the query into the ListView adapter. This refreshes the existing
+ * UI with the data in the Cursor.
+ */
+ @Override
+ public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
+ mAdapter.changeCursor(cursor);
+ }
+
+ /**
+ * Called when the ContentObserver defined for the content provider detects that data has
+ * changed. The ContentObserver resets the loader, and then re-runs the loader. In the adapter,
+ * set the Cursor value to null. This removes the reference to the Cursor, allowing it to be
+ * garbage-collected.
+ */
+ @Override
+ public void onLoaderReset(Loader<Cursor> cursorLoader) {
+ mAdapter.changeCursor(null);
+ }
+
+ /**
+ * Create the ActionBar.
+ */
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ mOptionsMenu = menu;
+ inflater.inflate(R.menu.main, menu);
+ }
+
+ /**
+ * Respond to user gestures on the ActionBar.
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ // If the user clicks the "Refresh" button.
+ case R.id.menu_refresh:
+ SyncUtils.TriggerRefresh();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Load an article in the default browser when selected by the user.
+ */
+ @Override
+ public void onListItemClick(ListView listView, View view, int position, long id) {
+ super.onListItemClick(listView, view, position, id);
+
+ // Get a URI for the selected item, then start an Activity that displays the URI. Any
+ // Activity that filters for ACTION_VIEW and a URI can accept this. In most cases, this will
+ // be a browser.
+
+ // Get the item at the selected position, in the form of a Cursor.
+ Cursor c = (Cursor) mAdapter.getItem(position);
+ // Get the link to the article represented by the item.
+ String articleUrlString = c.getString(COLUMN_URL_STRING);
+ if (articleUrlString == null) {
+ Log.e(TAG, "Attempt to launch entry with null link");
+ return;
+ }
+
+ Log.i(TAG, "Opening URL: " + articleUrlString);
+ // Get a Uri object for the URL string
+ Uri articleURL = Uri.parse(articleUrlString);
+ Intent i = new Intent(Intent.ACTION_VIEW, articleURL);
+ startActivity(i);
+ }
+
+ /**
+ * Set the state of the Refresh button. If a sync is active, turn on the ProgressBar widget.
+ * Otherwise, turn it off.
+ *
+ * @param refreshing True if an active sync is occuring, false otherwise
+ */
+ public void setRefreshActionButtonState(boolean refreshing) {
+ if (mOptionsMenu == null) {
+ return;
+ }
+
+ final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh);
+ if (refreshItem != null) {
+ if (refreshing) {
+ refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);
+ } else {
+ refreshItem.setActionView(null);
+ }
+ }
+ }
+
+ /**
+ * Crfate a new anonymous SyncStatusObserver. It's attached to the app's ContentResolver in
+ * onResume(), and removed in onPause(). If status changes, it sets the state of the Refresh
+ * button. If a sync is active or pending, the Refresh button is replaced by an indeterminate
+ * ProgressBar; otherwise, the button itself is displayed.
+ */
+ private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
+ /** Callback invoked with the sync adapter status changes. */
+ @Override
+ public void onStatusChanged(int which) {
+ getActivity().runOnUiThread(new Runnable() {
+ /**
+ * The SyncAdapter runs on a background thread. To update the UI, onStatusChanged()
+ * runs on the UI thread.
+ */
+ @Override
+ public void run() {
+ // Create a handle to the account that was created by
+ // SyncService.CreateSyncAccount(). This will be used to query the system to
+ // see how the sync status has changed.
+ Account account = GenericAccountService.GetAccount();
+ if (account == null) {
+ // GetAccount() returned an invalid value. This shouldn't happen, but
+ // we'll set the status to "not refreshing".
+ setRefreshActionButtonState(false);
+ return;
+ }
+
+ // Test the ContentResolver to see if the sync adapter is active or pending.
+ // Set the state of the refresh button accordingly.
+ boolean syncActive = ContentResolver.isSyncActive(
+ account, FeedContract.CONTENT_AUTHORITY);
+ boolean syncPending = ContentResolver.isSyncPending(
+ account, FeedContract.CONTENT_AUTHORITY);
+ setRefreshActionButtonState(syncActive || syncPending);
+ }
+ });
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncAdapter.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncAdapter.java
new file mode 100644
index 0000000..a759adb
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncAdapter.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.example.android.network.sync.basicsyncadapter.net.FeedParser;
+import com.example.android.network.sync.basicsyncadapter.provider.FeedContract;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Define a sync adapter for the app.
+ *
+ * <p>This class is instantiated in {@link SyncService}, which also binds SyncAdapter to the system.
+ * SyncAdapter should only be initialized in SyncService, never anywhere else.
+ *
+ * <p>The system calls onPerformSync() via an RPC call through the IBinder object supplied by
+ * SyncService.
+ */
+class SyncAdapter extends AbstractThreadedSyncAdapter {
+ public static final String TAG = "SyncAdapter";
+
+ /**
+ * URL to fetch content from during a sync.
+ *
+ * <p>This points to the Android Developers Blog. (Side note: We highly recommend reading the
+ * Android Developer Blog to stay up to date on the latest Android platform developments!)
+ */
+ private static final String FEED_URL = "http://android-developers.blogspot.com/atom.xml";
+
+ /**
+ * Network connection timeout, in milliseconds.
+ */
+ private static final int NET_CONNECT_TIMEOUT_MILLIS = 15000; // 15 seconds
+
+ /**
+ * Network read timeout, in milliseconds.
+ */
+ private static final int NET_READ_TIMEOUT_MILLIS = 10000; // 10 seconds
+
+ /**
+ * Content resolver, for performing database operations.
+ */
+ private final ContentResolver mContentResolver;
+
+ /**
+ * Project used when querying content provider. Returns all known fields.
+ */
+ private static final String[] PROJECTION = new String[] {
+ FeedContract.Entry._ID,
+ FeedContract.Entry.COLUMN_NAME_ENTRY_ID,
+ FeedContract.Entry.COLUMN_NAME_TITLE,
+ FeedContract.Entry.COLUMN_NAME_LINK,
+ FeedContract.Entry.COLUMN_NAME_PUBLISHED};
+
+ // Constants representing column positions from PROJECTION.
+ public static final int COLUMN_ID = 0;
+ public static final int COLUMN_ENTRY_ID = 1;
+ public static final int COLUMN_TITLE = 2;
+ public static final int COLUMN_LINK = 3;
+ public static final int COLUMN_PUBLISHED = 4;
+
+ /**
+ * Constructor. Obtains handle to content resolver for later use.
+ */
+ public SyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ mContentResolver = context.getContentResolver();
+ }
+
+ /**
+ * Constructor. Obtains handle to content resolver for later use.
+ */
+ public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ mContentResolver = context.getContentResolver();
+ }
+
+ /**
+ * Called by the Android system in response to a request to run the sync adapter. The work
+ * required to read data from the network, parse it, and store it in the content provider is
+ * done here. Extending AbstractThreadedSyncAdapter ensures that all methods within SyncAdapter
+ * run on a background thread. For this reason, blocking I/O and other long-running tasks can be
+ * run <em>in situ</em>, and you don't have to set up a separate thread for them.
+ .
+ *
+ * <p>This is where we actually perform any work required to perform a sync.
+ * {@link AbstractThreadedSyncAdapter} guarantees that this will be called on a non-UI thread,
+ * so it is safe to peform blocking I/O here.
+ *
+ * <p>The syncResult argument allows you to pass information back to the method that triggered
+ * the sync.
+ */
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority,
+ ContentProviderClient provider, SyncResult syncResult) {
+ Log.i(TAG, "Beginning network synchronization");
+ try {
+ final URL location = new URL(FEED_URL);
+ InputStream stream = null;
+
+ try {
+ Log.i(TAG, "Streaming data from network: " + location);
+ stream = downloadUrl(location);
+ updateLocalFeedData(stream, syncResult);
+ // Makes sure that the InputStream is closed after the app is
+ // finished using it.
+ } finally {
+ if (stream != null) {
+ stream.close();
+ }
+ }
+ } catch (MalformedURLException e) {
+ Log.wtf(TAG, "Feed URL is malformed", e);
+ syncResult.stats.numParseExceptions++;
+ return;
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading from network: " + e.toString());
+ syncResult.stats.numIoExceptions++;
+ return;
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Error parsing feed: " + e.toString());
+ syncResult.stats.numParseExceptions++;
+ return;
+ } catch (ParseException e) {
+ Log.e(TAG, "Error parsing feed: " + e.toString());
+ syncResult.stats.numParseExceptions++;
+ return;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating database: " + e.toString());
+ syncResult.databaseError = true;
+ return;
+ } catch (OperationApplicationException e) {
+ Log.e(TAG, "Error updating database: " + e.toString());
+ syncResult.databaseError = true;
+ return;
+ }
+ Log.i(TAG, "Network synchronization complete");
+ }
+
+ /**
+ * Read XML from an input stream, storing it into the content provider.
+ *
+ * <p>This is where incoming data is persisted, committing the results of a sync. In order to
+ * minimize (expensive) disk operations, we compare incoming data with what's already in our
+ * database, and compute a merge. Only changes (insert/update/delete) will result in a database
+ * write.
+ *
+ * <p>As an additional optimization, we use a batch operation to perform all database writes at
+ * once.
+ *
+ * <p>Merge strategy:
+ * 1. Get cursor to all items in feed<br/>
+ * 2. For each item, check if it's in the incoming data.<br/>
+ * a. YES: Remove from "incoming" list. Check if data has mutated, if so, perform
+ * database UPDATE.<br/>
+ * b. NO: Schedule DELETE from database.<br/>
+ * (At this point, incoming database only contains missing items.)<br/>
+ * 3. For any items remaining in incoming list, ADD to database.
+ */
+ public void updateLocalFeedData(final InputStream stream, final SyncResult syncResult)
+ throws IOException, XmlPullParserException, RemoteException,
+ OperationApplicationException, ParseException {
+ final FeedParser feedParser = new FeedParser();
+ final ContentResolver contentResolver = getContext().getContentResolver();
+
+ Log.i(TAG, "Parsing stream as Atom feed");
+ final List<FeedParser.Entry> entries = feedParser.parse(stream);
+ Log.i(TAG, "Parsing complete. Found " + entries.size() + " entries");
+
+
+ ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();
+
+ // Build hash table of incoming entries
+ HashMap<String, FeedParser.Entry> entryMap = new HashMap<String, FeedParser.Entry>();
+ for (FeedParser.Entry e : entries) {
+ entryMap.put(e.id, e);
+ }
+
+ // Get list of all items
+ Log.i(TAG, "Fetching local entries for merge");
+ Uri uri = FeedContract.Entry.CONTENT_URI; // Get all entries
+ Cursor c = contentResolver.query(uri, PROJECTION, null, null, null);
+ assert c != null;
+ Log.i(TAG, "Found " + c.getCount() + " local entries. Computing merge solution...");
+
+ // Find stale data
+ int id;
+ String entryId;
+ String title;
+ String link;
+ long published;
+ while (c.moveToNext()) {
+ syncResult.stats.numEntries++;
+ id = c.getInt(COLUMN_ID);
+ entryId = c.getString(COLUMN_ENTRY_ID);
+ title = c.getString(COLUMN_TITLE);
+ link = c.getString(COLUMN_LINK);
+ published = c.getLong(COLUMN_PUBLISHED);
+ FeedParser.Entry match = entryMap.get(entryId);
+ if (match != null) {
+ // Entry exists. Remove from entry map to prevent insert later.
+ entryMap.remove(entryId);
+ // Check to see if the entry needs to be updated
+ Uri existingUri = FeedContract.Entry.CONTENT_URI.buildUpon()
+ .appendPath(Integer.toString(id)).build();
+ if ((match.title != null && !match.title.equals(title)) ||
+ (match.link != null && !match.link.equals(link)) ||
+ (match.published != published)) {
+ // Update existing record
+ Log.i(TAG, "Scheduling update: " + existingUri);
+ batch.add(ContentProviderOperation.newUpdate(existingUri)
+ .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, title)
+ .withValue(FeedContract.Entry.COLUMN_NAME_LINK, link)
+ .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, published)
+ .build());
+ syncResult.stats.numUpdates++;
+ } else {
+ Log.i(TAG, "No action: " + existingUri);
+ }
+ } else {
+ // Entry doesn't exist. Remove it from the database.
+ Uri deleteUri = FeedContract.Entry.CONTENT_URI.buildUpon()
+ .appendPath(Integer.toString(id)).build();
+ Log.i(TAG, "Scheduling delete: " + deleteUri);
+ batch.add(ContentProviderOperation.newDelete(deleteUri).build());
+ syncResult.stats.numDeletes++;
+ }
+ }
+ c.close();
+
+ // Add new items
+ for (FeedParser.Entry e : entryMap.values()) {
+ Log.i(TAG, "Scheduling insert: entry_id=" + e.id);
+ batch.add(ContentProviderOperation.newInsert(FeedContract.Entry.CONTENT_URI)
+ .withValue(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, e.id)
+ .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, e.title)
+ .withValue(FeedContract.Entry.COLUMN_NAME_LINK, e.link)
+ .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, e.published)
+ .build());
+ syncResult.stats.numInserts++;
+ }
+ Log.i(TAG, "Merge solution ready. Applying batch update");
+ mContentResolver.applyBatch(FeedContract.CONTENT_AUTHORITY, batch);
+ mContentResolver.notifyChange(
+ FeedContract.Entry.CONTENT_URI, // URI where data was modified
+ null, // No local observer
+ false); // IMPORTANT: Do not sync to network
+ // This sample doesn't support uploads, but if *your* code does, make sure you set
+ // syncToNetwork=false in the line above to prevent duplicate syncs.
+ }
+
+ /**
+ * Given a string representation of a URL, sets up a connection and gets an input stream.
+ */
+ private InputStream downloadUrl(final URL url) throws IOException {
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setReadTimeout(NET_READ_TIMEOUT_MILLIS /* milliseconds */);
+ conn.setConnectTimeout(NET_CONNECT_TIMEOUT_MILLIS /* milliseconds */);
+ conn.setRequestMethod("GET");
+ conn.setDoInput(true);
+ // Starts the query
+ conn.connect();
+ return conn.getInputStream();
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncService.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncService.java
new file mode 100644
index 0000000..bd92f37
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.example.android.network.sync.basicsyncadapter;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+/** Service to handle sync requests.
+ *
+ * <p>This service is invoked in response to Intents with action android.content.SyncAdapter, and
+ * returns a Binder connection to SyncAdapter.
+ *
+ * <p>For performance, only one sync adapter will be initialized within this application's context.
+ *
+ * <p>Note: The SyncService itself is not notified when a new sync occurs. It's role is to
+ * manage the lifecycle of our {@link SyncAdapter} and provide a handle to said SyncAdapter to the
+ * OS on request.
+ */
+public class SyncService extends Service {
+ private static final String TAG = "SyncService";
+
+ private static final Object sSyncAdapterLock = new Object();
+ private static SyncAdapter sSyncAdapter = null;
+
+ /**
+ * Thread-safe constructor, creates static {@link SyncAdapter} instance.
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(TAG, "Service created");
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ /**
+ * Logging-only destructor.
+ */
+ public void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "Service destroyed");
+ }
+
+ /**
+ * Return Binder handle for IPC communication with {@link SyncAdapter}.
+ *
+ * <p>New sync requests will be sent directly to the SyncAdapter using this channel.
+ *
+ * @param intent Calling intent
+ * @return Binder handle for {@link SyncAdapter}
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncUtils.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncUtils.java
new file mode 100644
index 0000000..bf3e76c
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/SyncUtils.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.example.android.network.sync.basicsyncadapter;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+
+import com.example.android.network.sync.basicsyncadapter.accounts.GenericAccountService;
+import com.example.android.network.sync.basicsyncadapter.provider.FeedContract;
+
+/**
+ * Static helper methods for working with the sync framework.
+ */
+public class SyncUtils {
+ private static final long SYNC_FREQUENCY = 60 * 60; // 1 hour (in seconds)
+ private static final String CONTENT_AUTHORITY = FeedContract.CONTENT_AUTHORITY;
+ private static final String PREF_SETUP_COMPLETE = "setup_complete";
+
+ /**
+ * Create an entry for this application in the system account list, if it isn't already there.
+ *
+ * @param context Context
+ */
+ public static void CreateSyncAccount(Context context) {
+ boolean newAccount = false;
+ boolean setupComplete = PreferenceManager
+ .getDefaultSharedPreferences(context).getBoolean(PREF_SETUP_COMPLETE, false);
+
+ // Create account, if it's missing. (Either first run, or user has deleted account.)
+ Account account = GenericAccountService.GetAccount();
+ AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+ if (accountManager.addAccountExplicitly(account, null, null)) {
+ // Inform the system that this account supports sync
+ ContentResolver.setIsSyncable(account, CONTENT_AUTHORITY, 1);
+ // Inform the system that this account is eligible for auto sync when the network is up
+ ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, true);
+ // Recommend a schedule for automatic synchronization. The system may modify this based
+ // on other scheduled syncs and network utilization.
+ ContentResolver.addPeriodicSync(
+ account, CONTENT_AUTHORITY, new Bundle(),SYNC_FREQUENCY);
+ newAccount = true;
+ }
+
+ // Schedule an initial sync if we detect problems with either our account or our local
+ // data has been deleted. (Note that it's possible to clear app data WITHOUT affecting
+ // the account list, so wee need to check both.)
+ if (newAccount || !setupComplete) {
+ TriggerRefresh();
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putBoolean(PREF_SETUP_COMPLETE, true).commit();
+ }
+ }
+
+ /**
+ * Helper method to trigger an immediate sync ("refresh").
+ *
+ * <p>This should only be used when we need to preempt the normal sync schedule. Typically, this
+ * means the user has pressed the "refresh" button.
+ *
+ * Note that SYNC_EXTRAS_MANUAL will cause an immediate sync, without any optimization to
+ * preserve battery life. If you know new data is available (perhaps via a GCM notification),
+ * but the user is not actively waiting for that data, you should omit this flag; this will give
+ * the OS additional freedom in scheduling your sync request.
+ */
+ public static void TriggerRefresh() {
+ Bundle b = new Bundle();
+ // Disable sync backoff and ignore sync preferences. In other words...perform sync NOW!
+ b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+ b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+ ContentResolver.requestSync(
+ GenericAccountService.GetAccount(), // Sync account
+ FeedContract.CONTENT_AUTHORITY, // Content authority
+ b); // Extras
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/accounts/GenericAccountService.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/accounts/GenericAccountService.java
new file mode 100644
index 0000000..b5dc98e
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/accounts/GenericAccountService.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter.accounts;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+public class GenericAccountService extends Service {
+ private static final String TAG = "GenericAccountService";
+ private static final String ACCOUNT_TYPE = "com.example.android.network.sync.basicsyncadapter";
+ public static final String ACCOUNT_NAME = "sync";
+ private Authenticator mAuthenticator;
+
+ /**
+ * Obtain a handle to the {@link android.accounts.Account} used for sync in this application.
+ *
+ * @return Handle to application's account (not guaranteed to resolve unless CreateSyncAccount()
+ * has been called)
+ */
+ public static Account GetAccount() {
+ // Note: Normally the account name is set to the user's identity (username or email
+ // address). However, since we aren't actually using any user accounts, it makes more sense
+ // to use a generic string in this case.
+ //
+ // This string should *not* be localized. If the user switches locale, we would not be
+ // able to locate the old account, and may erroneously register multiple accounts.
+ final String accountName = ACCOUNT_NAME;
+ return new Account(accountName, ACCOUNT_TYPE);
+ }
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "Service created");
+ mAuthenticator = new Authenticator(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "Service destroyed");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mAuthenticator.getIBinder();
+ }
+
+ public class Authenticator extends AbstractAccountAuthenticator {
+ public Authenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ String s, String s2, String[] strings, Bundle bundle)
+ throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account, Bundle bundle)
+ throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account, String s, Bundle bundle)
+ throws NetworkErrorException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getAuthTokenLabel(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account, String s, Bundle bundle)
+ throws NetworkErrorException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account, String[] strings)
+ throws NetworkErrorException {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+}
+
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/net/FeedParser.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/net/FeedParser.java
new file mode 100644
index 0000000..2bcbc0f
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/net/FeedParser.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter.net;
+
+import android.text.format.Time;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class parses generic Atom feeds.
+ *
+ * <p>Given an InputStream representation of a feed, it returns a List of entries,
+ * where each list element represents a single entry (post) in the XML feed.
+ *
+ * <p>An example of an Atom feed can be found at:
+ * http://en.wikipedia.org/w/index.php?title=Atom_(standard)&oldid=560239173#Example_of_an_Atom_1.0_feed
+ */
+public class FeedParser {
+
+ // Constants indicting XML element names that we're interested in
+ private static final int TAG_ID = 1;
+ private static final int TAG_TITLE = 2;
+ private static final int TAG_PUBLISHED = 3;
+ private static final int TAG_LINK = 4;
+
+ // We don't use XML namespaces
+ private static final String ns = null;
+
+ /** Parse an Atom feed, returning a collection of Entry objects.
+ *
+ * @param in Atom feed, as a stream.
+ * @return List of {@link Entry} objects.
+ * @throws XmlPullParserException on error parsing feed.
+ * @throws IOException on I/O error.
+ */
+ public List<Entry> parse(InputStream in)
+ throws XmlPullParserException, IOException, ParseException {
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(in, null);
+ parser.nextTag();
+ return readFeed(parser);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Decode a feed attached to an XmlPullParser.
+ *
+ * @param parser Incoming XMl
+ * @return List of {@link Entry} objects.
+ * @throws XmlPullParserException on error parsing feed.
+ * @throws IOException on I/O error.
+ */
+ private List<Entry> readFeed(XmlPullParser parser)
+ throws XmlPullParserException, IOException, ParseException {
+ List<Entry> entries = new ArrayList<Entry>();
+
+ // Search for <feed> tags. These wrap the beginning/end of an Atom document.
+ //
+ // Example:
+ // <?xml version="1.0" encoding="utf-8"?>
+ // <feed xmlns="http://www.w3.org/2005/Atom">
+ // ...
+ // </feed>
+ parser.require(XmlPullParser.START_TAG, ns, "feed");
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ // Starts by looking for the <entry> tag. This tag repeates inside of <feed> for each
+ // article in the feed.
+ //
+ // Example:
+ // <entry>
+ // <title>Article title</title>
+ // <link rel="alternate" type="text/html" href="http://example.com/article/1234"/>
+ // <link rel="edit" href="http://example.com/admin/article/1234"/>
+ // <id>urn:uuid:218AC159-7F68-4CC6-873F-22AE6017390D</id>
+ // <published>2003-06-27T12:00:00Z</published>
+ // <updated>2003-06-28T12:00:00Z</updated>
+ // <summary>Article summary goes here.</summary>
+ // <author>
+ // <name>Rick Deckard</name>
+ // <email>deckard@example.com</email>
+ // </author>
+ // </entry>
+ if (name.equals("entry")) {
+ entries.add(readEntry(parser));
+ } else {
+ skip(parser);
+ }
+ }
+ return entries;
+ }
+
+ /**
+ * Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them
+ * off to their respective "read" methods for processing. Otherwise, skips the tag.
+ */
+ private Entry readEntry(XmlPullParser parser)
+ throws XmlPullParserException, IOException, ParseException {
+ parser.require(XmlPullParser.START_TAG, ns, "entry");
+ String id = null;
+ String title = null;
+ String link = null;
+ long publishedOn = 0;
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ if (name.equals("id")){
+ // Example: <id>urn:uuid:218AC159-7F68-4CC6-873F-22AE6017390D</id>
+ id = readTag(parser, TAG_ID);
+ } else if (name.equals("title")) {
+ // Example: <title>Article title</title>
+ title = readTag(parser, TAG_TITLE);
+ } else if (name.equals("link")) {
+ // Example: <link rel="alternate" type="text/html" href="http://example.com/article/1234"/>
+ //
+ // Multiple link types can be included. readAlternateLink() will only return
+ // non-null when reading an "alternate"-type link. Ignore other responses.
+ String tempLink = readTag(parser, TAG_LINK);
+ if (tempLink != null) {
+ link = tempLink;
+ }
+ } else if (name.equals("published")) {
+ // Example: <published>2003-06-27T12:00:00Z</published>
+ Time t = new Time();
+ t.parse3339(readTag(parser, TAG_PUBLISHED));
+ publishedOn = t.toMillis(false);
+ } else {
+ skip(parser);
+ }
+ }
+ return new Entry(id, title, link, publishedOn);
+ }
+
+ /**
+ * Process an incoming tag and read the selected value from it.
+ */
+ private String readTag(XmlPullParser parser, int tagType)
+ throws IOException, XmlPullParserException {
+ String tag = null;
+ String endTag = null;
+
+ switch (tagType) {
+ case TAG_ID:
+ return readBasicTag(parser, "id");
+ case TAG_TITLE:
+ return readBasicTag(parser, "title");
+ case TAG_PUBLISHED:
+ return readBasicTag(parser, "published");
+ case TAG_LINK:
+ return readAlternateLink(parser);
+ default:
+ throw new IllegalArgumentException("Unknown tag type: " + tagType);
+ }
+ }
+
+ /**
+ * Reads the body of a basic XML tag, which is guaranteed not to contain any nested elements.
+ *
+ * <p>You probably want to call readTag().
+ *
+ * @param parser Current parser object
+ * @param tag XML element tag name to parse
+ * @return Body of the specified tag
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private String readBasicTag(XmlPullParser parser, String tag)
+ throws IOException, XmlPullParserException {
+ parser.require(XmlPullParser.START_TAG, ns, tag);
+ String result = readText(parser);
+ parser.require(XmlPullParser.END_TAG, ns, tag);
+ return result;
+ }
+
+ /**
+ * Processes link tags in the feed.
+ */
+ private String readAlternateLink(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ String link = null;
+ parser.require(XmlPullParser.START_TAG, ns, "link");
+ String tag = parser.getName();
+ String relType = parser.getAttributeValue(null, "rel");
+ if (relType.equals("alternate")) {
+ link = parser.getAttributeValue(null, "href");
+ }
+ while (true) {
+ if (parser.nextTag() == XmlPullParser.END_TAG) break;
+ // Intentionally break; consumes any remaining sub-tags.
+ }
+ return link;
+ }
+
+ /**
+ * For the tags title and summary, extracts their text values.
+ */
+ private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String result = null;
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.getText();
+ parser.nextTag();
+ }
+ return result;
+ }
+
+ /**
+ * Skips tags the parser isn't interested in. Uses depth to handle nested tags. i.e.,
+ * if the next tag after a START_TAG isn't a matching END_TAG, it keeps going until it
+ * finds the matching END_TAG (as indicated by the value of "depth" being 0).
+ */
+ private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException();
+ }
+ int depth = 1;
+ while (depth != 0) {
+ switch (parser.next()) {
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ }
+ }
+ }
+
+ /**
+ * This class represents a single entry (post) in the XML feed.
+ *
+ * <p>It includes the data members "title," "link," and "summary."
+ */
+ public static class Entry {
+ public final String id;
+ public final String title;
+ public final String link;
+ public final long published;
+
+ Entry(String id, String title, String link, long published) {
+ this.id = id;
+ this.title = title;
+ this.link = link;
+ this.published = published;
+ }
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedContract.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedContract.java
new file mode 100644
index 0000000..7bfcf7f
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedContract.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter.provider;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * Field and table name constants for
+ * {@link com.example.android.network.sync.basicsyncadapter.provider.FeedProvider}.
+ */
+public class FeedContract {
+ private FeedContract() {
+ }
+
+ /**
+ * Content provider authority.
+ */
+ public static final String CONTENT_AUTHORITY = "com.example.android.network.sync.basicsyncadapter";
+
+ /**
+ * Base URI. (content://com.example.android.network.sync.basicsyncadapter)
+ */
+ public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
+
+ /**
+ * Path component for "entry"-type resources..
+ */
+ private static final String PATH_ENTRIES = "entries";
+
+ /**
+ * Columns supported by "entries" records.
+ */
+ public static class Entry implements BaseColumns {
+ /**
+ * MIME type for lists of entries.
+ */
+ public static final String CONTENT_TYPE =
+ ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.basicsyncadapter.entries";
+ /**
+ * MIME type for individual entries.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.basicsyncadapter.entry";
+
+ /**
+ * Fully qualified URI for "entry" resources.
+ */
+ public static final Uri CONTENT_URI =
+ BASE_CONTENT_URI.buildUpon().appendPath(PATH_ENTRIES).build();
+
+ /**
+ * Table name where records are stored for "entry" resources.
+ */
+ public static final String TABLE_NAME = "entry";
+ /**
+ * Atom ID. (Note: Not to be confused with the database primary key, which is _ID.
+ */
+ public static final String COLUMN_NAME_ENTRY_ID = "entry_id";
+ /**
+ * Article title
+ */
+ public static final String COLUMN_NAME_TITLE = "title";
+ /**
+ * Article hyperlink. Corresponds to the rel="alternate" link in the
+ * Atom spec.
+ */
+ public static final String COLUMN_NAME_LINK = "link";
+ /**
+ * Date article was published.
+ */
+ public static final String COLUMN_NAME_PUBLISHED = "published";
+ }
+}
\ No newline at end of file
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProvider.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProvider.java
new file mode 100644
index 0000000..88d8746
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/provider/FeedProvider.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2013 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.example.android.network.sync.basicsyncadapter.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+import com.example.android.network.sync.basicsyncadapter.util.SelectionBuilder;
+
+public class FeedProvider extends ContentProvider {
+ FeedDatabase mDatabaseHelper;
+
+ /**
+ * Content authority for this provider.
+ */
+ private static final String AUTHORITY = FeedContract.CONTENT_AUTHORITY;
+
+ // The constants below represent individual URI routes, as IDs. Every URI pattern recognized by
+ // this ContentProvider is defined using sUriMatcher.addURI(), and associated with one of these
+ // IDs.
+ //
+ // When a incoming URI is run through sUriMatcher, it will be tested against the defined
+ // URI patterns, and the corresponding route ID will be returned.
+ /**
+ * URI ID for route: /entries
+ */
+ public static final int ROUTE_ENTRIES = 1;
+
+ /**
+ * URI ID for route: /entries/{ID}
+ */
+ public static final int ROUTE_ENTRIES_ID = 2;
+
+ /**
+ * UriMatcher, used to decode incoming URIs.
+ */
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ sUriMatcher.addURI(AUTHORITY, "entries", ROUTE_ENTRIES);
+ sUriMatcher.addURI(AUTHORITY, "entries/*", ROUTE_ENTRIES_ID);
+ }
+
+ @Override
+ public boolean onCreate() {
+ mDatabaseHelper = new FeedDatabase(getContext());
+ return true;
+ }
+
+ /**
+ * Determine the mime type for entries returned by a given URI.
+ */
+ @Override
+ public String getType(Uri uri) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case ROUTE_ENTRIES:
+ return FeedContract.Entry.CONTENT_TYPE;
+ case ROUTE_ENTRIES_ID:
+ return FeedContract.Entry.CONTENT_ITEM_TYPE;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ /**
+ * Perform a database query by URI.
+ *
+ * <p>Currently supports returning all entries (/entries) and individual entries by ID
+ * (/entries/{ID}).
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
+ SelectionBuilder builder = new SelectionBuilder();
+ int uriMatch = sUriMatcher.match(uri);
+ switch (uriMatch) {
+ case ROUTE_ENTRIES_ID:
+ // Return a single entry, by ID.
+ String id = uri.getLastPathSegment();
+ builder.where(FeedContract.Entry._ID + "=?", id);
+ case ROUTE_ENTRIES:
+ // Return all known entries.
+ builder.table(FeedContract.Entry.TABLE_NAME)
+ .where(selection, selectionArgs);
+ Cursor c = builder.query(db, projection, sortOrder);
+ // Note: Notification URI must be manually set here for loaders to correctly
+ // register ContentObservers.
+ Context ctx = getContext();
+ assert ctx != null;
+ c.setNotificationUri(ctx.getContentResolver(), uri);
+ return c;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ /**
+ * Insert a new entry into the database.
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ assert db != null;
+ final int match = sUriMatcher.match(uri);
+ Uri result;
+ switch (match) {
+ case ROUTE_ENTRIES:
+ long id = db.insertOrThrow(FeedContract.Entry.TABLE_NAME, null, values);
+ result = Uri.parse(FeedContract.Entry.CONTENT_URI + "/" + id);
+ break;
+ case ROUTE_ENTRIES_ID:
+ throw new UnsupportedOperationException("Insert not supported on URI: " + uri);
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ // Send broadcast to registered ContentObservers, to refresh UI.
+ Context ctx = getContext();
+ assert ctx != null;
+ ctx.getContentResolver().notifyChange(uri, null, false);
+ return result;
+ }
+
+ /**
+ * Delete an entry by database by URI.
+ */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SelectionBuilder builder = new SelectionBuilder();
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ int count;
+ switch (match) {
+ case ROUTE_ENTRIES:
+ count = builder.table(FeedContract.Entry.TABLE_NAME)
+ .where(selection, selectionArgs)
+ .delete(db);
+ break;
+ case ROUTE_ENTRIES_ID:
+ String id = uri.getLastPathSegment();
+ count = builder.table(FeedContract.Entry.TABLE_NAME)
+ .where(FeedContract.Entry._ID + "=?", id)
+ .where(selection, selectionArgs)
+ .delete(db);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ // Send broadcast to registered ContentObservers, to refresh UI.
+ Context ctx = getContext();
+ assert ctx != null;
+ ctx.getContentResolver().notifyChange(uri, null, false);
+ return count;
+ }
+
+ /**
+ * Update an etry in the database by URI.
+ */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ SelectionBuilder builder = new SelectionBuilder();
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ int count;
+ switch (match) {
+ case ROUTE_ENTRIES:
+ count = builder.table(FeedContract.Entry.TABLE_NAME)
+ .where(selection, selectionArgs)
+ .update(db, values);
+ break;
+ case ROUTE_ENTRIES_ID:
+ String id = uri.getLastPathSegment();
+ count = builder.table(FeedContract.Entry.TABLE_NAME)
+ .where(FeedContract.Entry._ID + "=?", id)
+ .where(selection, selectionArgs)
+ .update(db, values);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ Context ctx = getContext();
+ assert ctx != null;
+ ctx.getContentResolver().notifyChange(uri, null, false);
+ return count;
+ }
+
+ /**
+ * SQLite backend for @{link FeedProvider}.
+ *
+ * Provides access to an disk-backed, SQLite datastore which is utilized by FeedProvider. This
+ * database should never be accessed by other parts of the application directly.
+ */
+ static class FeedDatabase extends SQLiteOpenHelper {
+ /** Schema version. */
+ public static final int DATABASE_VERSION = 1;
+ /** Filename for SQLite file. */
+ public static final String DATABASE_NAME = "feed.db";
+
+ private static final String TYPE_TEXT = " TEXT";
+ private static final String TYPE_INTEGER = " INTEGER";
+ private static final String COMMA_SEP = ",";
+ /** SQL statement to create "entry" table. */
+ private static final String SQL_CREATE_ENTRIES =
+ "CREATE TABLE " + FeedContract.Entry.TABLE_NAME + " (" +
+ FeedContract.Entry._ID + " INTEGER PRIMARY KEY," +
+ FeedContract.Entry.COLUMN_NAME_ENTRY_ID + TYPE_TEXT + COMMA_SEP +
+ FeedContract.Entry.COLUMN_NAME_TITLE + TYPE_TEXT + COMMA_SEP +
+ FeedContract.Entry.COLUMN_NAME_LINK + TYPE_TEXT + COMMA_SEP +
+ FeedContract.Entry.COLUMN_NAME_PUBLISHED + TYPE_INTEGER + ")";
+
+ /** SQL statement to drop "entry" table. */
+ private static final String SQL_DELETE_ENTRIES =
+ "DROP TABLE IF EXISTS " + FeedContract.Entry.TABLE_NAME;
+
+ public FeedDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_ENTRIES);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // This database is only a cache for online data, so its upgrade policy is
+ // to simply to discard the data and start over
+ db.execSQL(SQL_DELETE_ENTRIES);
+ onCreate(db);
+ }
+ }
+}
diff --git a/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/util/SelectionBuilder.java b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/util/SelectionBuilder.java
new file mode 100644
index 0000000..556713a
--- /dev/null
+++ b/networking/sync/BasicSyncAdapter/src/com/example/android/network/sync/basicsyncadapter/util/SelectionBuilder.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2013 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.
+ */
+
+/*
+ * Modifications:
+ * -Imported from AOSP frameworks/base/core/java/com/android/internal/content
+ * -Changed package name
+ */
+
+package com.example.android.network.sync.basicsyncadapter.util;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Helper for building selection clauses for {@link SQLiteDatabase}. Each
+ * appended clause is combined using {@code AND}. This class is <em>not</em>
+ * thread safe.
+ */
+public class SelectionBuilder {
+ private static final String TAG = "basicsyncadapter";
+
+ private String mTable = null;
+ private Map<String, String> mProjectionMap = Maps.newHashMap();
+ private StringBuilder mSelection = new StringBuilder();
+ private ArrayList<String> mSelectionArgs = Lists.newArrayList();
+
+ /**
+ * Reset any internal state, allowing this builder to be recycled.
+ */
+ public SelectionBuilder reset() {
+ mTable = null;
+ mSelection.setLength(0);
+ mSelectionArgs.clear();
+ return this;
+ }
+
+ /**
+ * Append the given selection clause to the internal state. Each clause is
+ * surrounded with parenthesis and combined using {@code AND}.
+ */
+ public SelectionBuilder where(String selection, String... selectionArgs) {
+ if (TextUtils.isEmpty(selection)) {
+ if (selectionArgs != null && selectionArgs.length > 0) {
+ throw new IllegalArgumentException(
+ "Valid selection required when including arguments=");
+ }
+
+ // Shortcut when clause is empty
+ return this;
+ }
+
+ if (mSelection.length() > 0) {
+ mSelection.append(" AND ");
+ }
+
+ mSelection.append("(").append(selection).append(")");
+ if (selectionArgs != null) {
+ Collections.addAll(mSelectionArgs, selectionArgs);
+ }
+
+ return this;
+ }
+
+ public SelectionBuilder table(String table) {
+ mTable = table;
+ return this;
+ }
+
+ private void assertTable() {
+ if (mTable == null) {
+ throw new IllegalStateException("Table not specified");
+ }
+ }
+
+ public SelectionBuilder mapToTable(String column, String table) {
+ mProjectionMap.put(column, table + "." + column);
+ return this;
+ }
+
+ public SelectionBuilder map(String fromColumn, String toClause) {
+ mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn);
+ return this;
+ }
+
+ /**
+ * Return selection string for current internal state.
+ *
+ * @see #getSelectionArgs()
+ */
+ public String getSelection() {
+ return mSelection.toString();
+ }
+
+ /**
+ * Return selection arguments for current internal state.
+ *
+ * @see #getSelection()
+ */
+ public String[] getSelectionArgs() {
+ return mSelectionArgs.toArray(new String[mSelectionArgs.size()]);
+ }
+
+ private void mapColumns(String[] columns) {
+ for (int i = 0; i < columns.length; i++) {
+ final String target = mProjectionMap.get(columns[i]);
+ if (target != null) {
+ columns[i] = target;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection()
+ + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]";
+ }
+
+ /**
+ * Execute query using the current internal state as {@code WHERE} clause.
+ */
+ public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) {
+ return query(db, columns, null, null, orderBy, null);
+ }
+
+ /**
+ * Execute query using the current internal state as {@code WHERE} clause.
+ */
+ public Cursor query(SQLiteDatabase db, String[] columns, String groupBy,
+ String having, String orderBy, String limit) {
+ assertTable();
+ if (columns != null) mapColumns(columns);
+ Log.v(TAG, "query(columns=" + Arrays.toString(columns) + ") " + this);
+ return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having,
+ orderBy, limit);
+ }
+
+ /**
+ * Execute update using the current internal state as {@code WHERE} clause.
+ */
+ public int update(SQLiteDatabase db, ContentValues values) {
+ assertTable();
+ Log.v(TAG, "update() " + this);
+ return db.update(mTable, values, getSelection(), getSelectionArgs());
+ }
+
+ /**
+ * Execute delete using the current internal state as {@code WHERE} clause.
+ */
+ public int delete(SQLiteDatabase db) {
+ assertTable();
+ Log.v(TAG, "delete() " + this);
+ return db.delete(mTable, getSelection(), getSelectionArgs());
+ }
+}