Android apps can send or receive broadcast messages from the Android system and other Android apps.
Apps can register to receive specific broadcasts. When a broadcast is sent, the system automatically routes broadcasts to apps that have subscribed to receive that particular type of broadcast. System broadcasts
The system automatically sends broadcasts when various system events occur, such as when the system switches in and out of airplane mode. System broadcasts are sent to all apps that are subscribed to receive the event.
Receiving broadcasts
Apps can receive broadcasts in two ways: through manifest-declared receivers and context-registered receivers.
Manifest-declared receivers
If you declare a broadcast receiver in your manifest, the system launches your app (if the app is not already running) when the broadcast is sent.
To declare a broadcast receiver in the manifest, perform the following steps:
Specify the <receiver> element in your app's manifest.
The system package manager registers the receiver when the app is installed. The receiver then becomes a separate entry point into your app which means that the system can start the app and deliver the broadcast if the app is not currently running.
The system creates a new BroadcastReceiver component object to handle each broadcast that it receives. This object is valid only for the duration of the call to onReceive(Context, Intent). Once your code returns from this method, the system considers the component no longer active.
Context-registered receivers
To register a receiver with a context, perform the following steps:
For most Android applications, maintaining persistent synchronization between local and cloud data source can be troubling, complex, and frustrating, yet it is essential. Luckily, Android provides the SyncAdapter framework to batch synchronization efficiently for us!
The SyncAdapter can be invoked manually or can be set to sync periodically at regular intervals. There are also a plethora of other awesome features that using the SyncAdapter framework allows, such as account settings and a nice logo in the 'Accounts' tab in Android settings... which implicates its reliability on the AccountManager framework as well.
To use the SyncAdapter framework, you must create a few components:
ContentProvider (used to update local content),
Authenticator (can be stubbed or used to retrieve tokens such as OAuth tokens),
AuthenticatorService (Used only by Android to use authenticator if needed),
SyncAdapter (Performs the actual synchronization),
SyncService (Used only by Android to run your SyncAdapter)
As a hypothetical use case, we'll pretend that we are creating a SyncAdapter to synchronize pretend RSS feed with our application herein.
Prelude
Before being able to jump into building our SyncAdapter, we must create a few components before we can start.
Create Model Classes
We need to create a simple class to model what our hypothetical article's data should look like.
We need to create a utility that will convert JSON data into our Article model.
/** * This is an example parser to 'parse' pretend json news feed.*/publicclassArticleParser {
publicstaticArticleparse(JSONObjectjsonArticle) {
Article article =newArticle();
article.setId(jsonArticle.optString("id"));
article.setTitle(jsonArticle.optString("title"));
article.setContent(jsonArticle.optString("content"));
article.setLink(jsonArticle.optString("link"));
return article;
}
}
Create Contract Classes
Just like recommended in other Wiki pages on here, it's a good idea to define your database and provider structure concretely. (Refer to this great article for a more detailed explanation)
/** * Define all local entities in this class.*/publicfinalclassArticleContract {
// ContentProvider informationpublicstaticfinalStringCONTENT_AUTHORITY="com.example.sync";
staticfinalUriBASE_CONTENT_URI=Uri.parse("content://"+CONTENT_AUTHORITY);
staticfinalStringPATH_ARTICLES="articles";
// Database informationstaticfinalStringDB_NAME="articles_db";
staticfinalintDB_VERSION=1;
/** * This represents our SQLite table for our articles.*/publicstaticabstractclassArticles {
publicstaticfinalStringNAME="articles";
publicstaticfinalStringCOL_ID="articleId";
publicstaticfinalStringCOL_TITLE="articleTitle";
publicstaticfinalStringCOL_CONTENT="articleContent";
publicstaticfinalStringCOL_LINK="articleLink";
// ContentProvider information for articlespublicstaticfinalUriCONTENT_URI=BASE_CONTENT_URI.buildUpon().appendPath(PATH_ARTICLES).build();
publicstaticfinalStringCONTENT_TYPE="vnd.android.cursor.dir/"+CONTENT_URI+"/"+PATH_ARTICLES;
publicstaticfinalStringCONTENT_ITEM_TYPE="vnd.android.cursor.item/"+CONTENT_URI+"/"+PATH_ARTICLES;
}
}
Create SQLite Database
After defining our database structure, we will need to create, manage, and access it accordingly using the SQLiteOpenHelper class. For detailed information on using this, refer to this article.
There are several ways to build this class, but I'm using the Singleton pattern to provide access to the SQLiteDatabase class.
/** * Notice how we are inheriting {@link SQLiteOpenHelper}, this requires we do: * (1) Call parent constructor (specify database info) * (2) Implement {@link #onCreate(SQLiteDatabase)} (create our tables here) * (3) Implement {@link #onUpgrade(SQLiteDatabase, int, int)} (update our tables here) * * {@link #db} stores a reference to our database we want to use.*/publicfinalclassDatabaseClientextendsSQLiteOpenHelper {
privatestaticvolatileDatabaseClient instance;
privatefinalSQLiteDatabase db;
privateDatabaseClient(Contextc) {
super(c, DB_NAME, null, DB_VERSION);
this.db = getWritableDatabase();
}
/** * We use a Singleton to prevent leaking the SQLiteDatabase or Context. * @return {@link DatabaseClient}*/publicstaticDatabaseClientgetInstance(Contextc) {
if (instance ==null) {
synchronized (DatabaseClient.class) {
if (instance ==null) {
instance =newDatabaseClient(c);
}
}
}
return instance;
}
@OverridepublicvoidonCreate(SQLiteDatabasedb) {
// Create any SQLite tables here
createArticlesTable(db);
}
@OverridepublicvoidonUpgrade(SQLiteDatabasedb, intoldVersion, intnewVersion) {
// Update any SQLite tables here
db.execSQL("DROP TABLE IF EXISTS ["+Articles.NAME+"];");
onCreate(db);
}
/** * Provide access to our database.*/publicSQLiteDatabasegetDb() {
return db;
}
/** * Creates our 'articles' SQLite database table. * @param db {@link SQLiteDatabase}*/privatevoidcreateArticlesTable(SQLiteDatabasedb) {
db.execSQL("CREATE TABLE ["+Articles.NAME+"] (["+Articles.COL_ID+"] TEXT UNIQUE PRIMARY KEY,["+Articles.COL_TITLE+"] TEXT NOT NULL,["+Articles.COL_CONTENT+"] TEXT,["+Articles.COL_LINK+"] TEXT);");
}
}
Step 1: The ContentProvider
Now that we've created the necessary components to implement a ContentProvider, we can start. We need to create a ContentProvider for our hypothetical example to provide access to our local data source.
This ContentProvider is absolutely imperative as we use it to synchronize local data changes and utilize its content observer to potentially update the UI after syncing.
For an in-depth tutorial on creating a ContentProvider, refer to this fantastic article.
/** * This is the ContentProvider that will be used by our SyncAdapter to sync local data.*/publicclassArticleProviderextendsContentProvider {
// Use ints to represent different queriesprivatestaticfinalintARTICLE=1;
privatestaticfinalintARTICLE_ID=2;
privatestaticfinalUriMatcher uriMatcher;
static {
// Add all our query types to our UriMatcher
uriMatcher =newUriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ArticleContract.CONTENT_AUTHORITY, ArticleContract.PATH_ARTICLES, ARTICLE);
uriMatcher.addURI(ArticleContract.CONTENT_AUTHORITY, ArticleContract.PATH_ARTICLES+"/#", ARTICLE_ID);
}
privateSQLiteDatabase db;
@OverridepublicbooleanonCreate() {
this.db =DatabaseClient.getInstance(getContext()).getDb();
returntrue;
}
@Nullable@OverridepublicStringgetType(@NonNullUriuri) {
// Find the MIME type of the results... multiple results or a single resultswitch (uriMatcher.match(uri)) {
caseARTICLE:returnArticleContract.Articles.CONTENT_TYPE;
caseARTICLE_ID:returnArticleContract.Articles.CONTENT_ITEM_TYPE;
default:thrownewIllegalArgumentException("Invalid URI!");
}
}
@Nullable@OverridepublicCursorquery(@NonNullUriuri, @NullableString[] projection, @NullableStringselection, @NullableString[] selectionArgs, @NullableStringsortOrder) {
Cursor c;
switch (uriMatcher.match(uri)) {
// Query for multiple article resultscaseARTICLE:
c = db.query(ArticleContract.Articles.NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
break;
// Query for single article resultcaseARTICLE_ID:long _id =ContentUris.parseId(uri);
c = db.query(ArticleContract.Articles.NAME,
projection,
ArticleContract.Articles.COL_ID+"=?",
newString[] { String.valueOf(_id) },
null,
null,
sortOrder);
break;
default:thrownewIllegalArgumentException("Invalid URI!");
}
// Tell the cursor to register a content observer to observe changes to the// URI or its descendants.assert getContext() !=null;
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Nullable@OverridepublicUriinsert(@NonNullUriuri, @NullableContentValuesvalues) {
Uri returnUri;
long _id;
switch (uriMatcher.match(uri)) {
caseARTICLE:
_id = db.insert(ArticleContract.Articles.NAME, null, values);
returnUri =ContentUris.withAppendedId(ArticleContract.Articles.CONTENT_URI, _id);
break;
default:thrownewIllegalArgumentException("Invalid URI!");
}
// Notify any observers to update the UIassert getContext() !=null;
getContext().getContentResolver().notifyChange(uri, null);
return returnUri;
}
@Overridepublicintupdate(@NonNullUriuri, @NullableContentValuesvalues, @NullableStringselection, @NullableString[] selectionArgs) {
int rows;
switch (uriMatcher.match(uri)) {
caseARTICLE:
rows = db.update(ArticleContract.Articles.NAME, values, selection, selectionArgs);
break;
default:thrownewIllegalArgumentException("Invalid URI!");
}
// Notify any observers to update the UIif (rows !=0) {
assert getContext() !=null;
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
@Overridepublicintdelete(@NonNullUriuri, @NullableStringselection, @NullableString[] selectionArgs) {
int rows;
switch (uriMatcher.match(uri)) {
caseARTICLE:
rows = db.delete(ArticleContract.Articles.NAME, selection, selectionArgs);
break;
default:thrownewIllegalArgumentException("Invalid URI!");
}
// Notify any observers to update the UIif (rows !=0) {
assert getContext() !=null;
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
}
Declare the ContentProvider
In order to use our ContentProvider, we need to declare it in the manifest.xml file. It is important to note that:
android:authorities must match our ContentProvider's authority exactly, and
The next steps requires that we have an Account on the device. This seems like a lot of work or unnecessary, but it is worth it for a prodigious amount of reasons:
Supports the SyncAdapter framework
Supports various access tokens (access rights)
Supports various account settings features
Standardization for authentication
Sharing your account across multiple apps, like Google does
There are many ways you can utilize account creation and authentication (check out the references), but for the scope of our example (syncing RSS feed), we just want a very simple account with stubbed authentication.
Declare Proper Permissions
Before continuing, we must add these NEEDED permissions to the manifest.xml so we can use SyncAdapter, AccountManager, and Sync Settings.
Again, it's a good idea to concretely define our account structure; especially since our account will be really simple.
publicfinalclassAccountGeneral {
/** * This is the type of account we are using. i.e. we can specify our app or apps * to have different types, such as 'read-only', 'sync-only', & 'admin'.*/privatestaticfinalStringACCOUNT_TYPE="com.example.syncaccount";
/** * This is the name that appears in the Android 'Accounts' settings.*/privatestaticfinalStringACCOUNT_NAME="Example Sync";
/** * Gets the standard sync account for our app. * @return {@link Account}*/publicstaticAccountgetAccount() {
returnnewAccount(ACCOUNT_NAME, ACCOUNT_TYPE);
}
/** * Creates the standard sync account for our app. * @param c {@link Context}*/publicstaticvoidcreateSyncAccount(Contextc) {
// Flag to determine if this is a new account or notboolean created =false;
// Get an account and the account managerAccount account = getAccount();
AccountManager manager = (AccountManager)c.getSystemService(Context.ACCOUNT_SERVICE);
// Attempt to explicitly create the account with no password or extra dataif (manager.addAccountExplicitly(account, null, null)) {
finalStringAUTHORITY=ArticleContract.CONTENT_AUTHORITY;
finallongSYNC_FREQUENCY=60*60; // 1 hour (seconds)// Inform the system that this account supports syncContentResolver.setIsSyncable(account, AUTHORITY, 1);
// Inform the system that this account is eligible for auto sync when the network is upContentResolver.setSyncAutomatically(account, AUTHORITY, true);
// Recommend a schedule for automatic synchronization. The system may modify this based// on other scheduled syncs and network utilization.ContentResolver.addPeriodicSync(account, AUTHORITY, newBundle(), SYNC_FREQUENCY);
created =true;
}
// Force a sync if the account was just createdif (created) {
SyncAdapter.performSync();
}
}
}
Create an Account Authenticator
The authenticator will perform all the actions on the account type. It will also know which activity to show for the user to enter their credential and where to find any stored auth-token that the server has previously returned. This can also be common to many different services under a single account type.
For example, Google's authenticator on Android authenticates the Google Mail service (Gmail), Google Calendar, Google Drive, and along with many other Google services.
We need to make an authenticator that extends the AbstractAccountAuthenticator class. This is really simple because we don't use any authentication, therefore, we stub it.
/** * This is stubbed because we don't need any authentication to access the pretend RSS feed.*/publicclassAccountAuthenticatorextendsAbstractAccountAuthenticator {
publicAccountAuthenticator(Contextc) {
super(c);
}
@OverridepublicBundleaddAccount(AccountAuthenticatorResponseresponse, StringaccountType, StringauthTokenType, String[] requiredFeatures, Bundleoptions) throwsNetworkErrorException {
returnnull;
}
@OverridepublicBundleconfirmCredentials(AccountAuthenticatorResponseresponse, Accountaccount, Bundleoptions) throwsNetworkErrorException {
returnnull;
}
@OverridepublicBundlegetAuthToken(AccountAuthenticatorResponseresponse, Accountaccount, StringauthTokenType, Bundleoptions) throwsNetworkErrorException {
returnnull;
}
@OverridepublicStringgetAuthTokenLabel(StringauthTokenType) {
returnnull;
}
@OverridepublicBundleupdateCredentials(AccountAuthenticatorResponseresponse, Accountaccount, StringauthTokenType, Bundleoptions) throwsNetworkErrorException {
returnnull;
}
@OverridepublicBundleeditProperties(AccountAuthenticatorResponseresponse, StringaccountType) {
returnnull;
}
@OverridepublicBundlehasFeatures(AccountAuthenticatorResponseresponse, Accountaccount, String[] features) throwsNetworkErrorException {
returnnull;
}
}
Create the Authentication Service
For Android to use our AccountAuthenticator, it needs to run it in a bound Service that will allow it to do so. To see the in-depth of how AbstractAccountAuthenticator works, look at its Transport inner-class and read about AIDL for inter-process communication.
We need to create this bound Service so we can let Android use our AccountAuthenticator class.
/** * This is used only by Android to run our {@link AccountAuthenticator}.*/publicclassAuthenticatorServiceextendsService {
privateAccountAuthenticator authenticator;
@OverridepublicvoidonCreate() {
// Instantiate our authenticator when the service is createdthis.authenticator =newAccountAuthenticator(this);
}
@Nullable@OverridepublicIBinderonBind(Intentintent) {
// Return the authenticator's IBinderreturn authenticator.getIBinder();
}
}
(Optional) Declare Account Preferences
These preferences will be shown when accessing the account's preferences from the device Settings screen. This allows the user more control over their account.
To create this, we need to create a resource package in 'res' called, 'xml', if not already created, and create 'syncsettings.xml'.
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:key="use_sync"
android:title="Toggle Options"
android:summary="This is an example toggle setting"/>
</PreferenceScreen>
Declare the Authenticator
We need to create a resource package in 'res' called, 'xml', if not already created, and create 'authenticator.xml'.
android:accountType=(string) - identify our account type when an app wants to authenticate us (match EXACTLY in our AccountsGeneral)
android:label=(string) - Name of the account on the device Settings
android:icon=(drawable) - is the normal icon that will appear in the 'Accounts' Android settings
android:smallIcon=(drawable) - (optional) is the small icon the will show for the account
android:accountPreferences=(xml) - (optional) specifies extra settings for our account
The next steps requires us to have our SyncAdapter and another bound Service to allow Android to run it. These are some of the benefits it will offer us:
Status check and start synchronization when network is available
Scheduler that synchronizes using criteria
Auto synchronization if it had previously failed
Saves battery power because the system will batch networking
Create the SyncAdapter
To create our SyncAdapter, we must extend the AbstractThreadedSyncAdapter class.
It's important to note that any logic that's performed in the onPerformSync() method, happens on a new Thread. Do NOT create a new Thread to run networking or long performing tasks because Android handles that for you!
/** * This is used by the Android framework to perform synchronization. IMPORTANT: do NOT create * new Threads to perform logic, Android will do this for you; hence, the name. * * The goal here to perform synchronization, is to do it efficiently as possible. We use some * ContentProvider features to batch our writes to the local data source. Be sure to handle all * possible exceptions accordingly; random crashes is not a good user-experience.*/publicclassSyncAdapterextendsAbstractThreadedSyncAdapter {
privatestaticfinalStringTAG="SYNC_ADAPTER";
/** * This gives us access to our local data source.*/privatefinalContentResolver resolver;
publicSyncAdapter(Contextc, booleanautoInit) {
this(c, autoInit, false);
}
publicSyncAdapter(Contextc, booleanautoInit, booleanparallelSync) {
super(c, autoInit, parallelSync);
this.resolver = c.getContentResolver();
}
/** * This method is run by the Android framework, on a new Thread, to perform a sync. * @param account Current account * @param extras Bundle extras * @param authority Content authority * @param provider {@link ContentProviderClient} * @param syncResult Object to write stats to*/@OverridepublicvoidonPerformSync(Accountaccount, Bundleextras, Stringauthority, ContentProviderClientprovider, SyncResultsyncResult) {
Log.w(TAG, "Starting synchronization...");
try {
// Synchronize our news feed
syncNewsFeed(syncResult);
// Add any other things you may want to sync
} catch (IOException ex) {
Log.e(TAG, "Error synchronizing!", ex);
syncResult.stats.numIoExceptions++;
} catch (JSONException ex) {
Log.e(TAG, "Error synchronizing!", ex);
syncResult.stats.numParseExceptions++;
} catch (RemoteException|OperationApplicationException ex) {
Log.e(TAG, "Error synchronizing!", ex);
syncResult.stats.numAuthExceptions++;
}
Log.w(TAG, "Finished synchronization!");
}
/** * Performs synchronization of our pretend news feed source. * @param syncResult Write our stats to this*/privatevoidsyncNewsFeed(SyncResultsyncResult) throwsIOException, JSONException, RemoteException, OperationApplicationException {
finalString rssFeedEndpoint ="http://www.examplejsonnews.com";
// We need to collect all the network items in a hash tableLog.i(TAG, "Fetching server entries...");
Map<String, Article> networkEntries =newHashMap<>();
// Parse the pretend json news feedString jsonFeed = download(rssFeedEndpoint);
JSONArray jsonArticles =newJSONArray(jsonFeed);
for (int i =0; i < jsonArticles.length(); i++) {
Article article =ArticleParser.parse(jsonArticles.optJSONObject(i));
networkEntries.put(article.getId(), article);
}
// Create list for batching ContentProvider transactionsArrayList<ContentProviderOperation> batch =newArrayList<>();
// Compare the hash table of network entries to all the local entriesLog.i(TAG, "Fetching local entries...");
Cursor c = resolver.query(ArticleContract.Articles.CONTENT_URI, null, null, null, null, null);
assert c !=null;
c.moveToFirst();
String id;
String title;
String content;
String link;
Article found;
for (int i =0; i < c.getCount(); i++) {
syncResult.stats.numEntries++;
// Create local article entry
id = c.getString(c.getColumnIndex(ArticleContract.Articles.COL_ID));
title = c.getString(c.getColumnIndex(ArticleContract.Articles.COL_TITLE));
content = c.getString(c.getColumnIndex(ArticleContract.Articles.COL_CONTENT));
link = c.getString(c.getColumnIndex(ArticleContract.Articles.COL_LINK));
// Try to retrieve the local entry from network entries
found = networkEntries.get(id);
if (found !=null) {
// The entry exists, remove from hash table to prevent re-inserting it
networkEntries.remove(id);
// Check to see if it needs to be updatedif (!title.equals(found.getTitle())
||!content.equals(found.getContent())
||!link.equals(found.getLink())) {
// Batch an update for the existing recordLog.i(TAG, "Scheduling update: "+ title);
batch.add(ContentProviderOperation.newUpdate(ArticleContract.Articles.CONTENT_URI)
.withSelection(ArticleContract.Articles.COL_ID+"='"+ id +"'", null)
.withValue(ArticleContract.Articles.COL_TITLE, found.getTitle())
.withValue(ArticleContract.Articles.COL_CONTENT, found.getContent())
.withValue(ArticleContract.Articles.COL_LINK, found.getLink())
.build());
syncResult.stats.numUpdates++;
}
} else {
// Entry doesn't exist, remove it from the local databaseLog.i(TAG, "Scheduling delete: "+ title);
batch.add(ContentProviderOperation.newDelete(ArticleContract.Articles.CONTENT_URI)
.withSelection(ArticleContract.Articles.COL_ID+"='"+ id +"'", null)
.build());
syncResult.stats.numDeletes++;
}
c.moveToNext();
}
c.close();
// Add all the new entriesfor (Article article : networkEntries.values()) {
Log.i(TAG, "Scheduling insert: "+ article.getTitle());
batch.add(ContentProviderOperation.newInsert(ArticleContract.Articles.CONTENT_URI)
.withValue(ArticleContract.Articles.COL_ID, article.getId())
.withValue(ArticleContract.Articles.COL_TITLE, article.getTitle())
.withValue(ArticleContract.Articles.COL_CONTENT, article.getContent())
.withValue(ArticleContract.Articles.COL_LINK, article.getLink())
.build());
syncResult.stats.numInserts++;
}
// Synchronize by performing batch updateLog.i(TAG, "Merge solution ready, applying batch update...");
resolver.applyBatch(ArticleContract.CONTENT_AUTHORITY, batch);
resolver.notifyChange(ArticleContract.Articles.CONTENT_URI, // URI where data was modifiednull, // No local observerfalse); // IMPORTANT: Do not sync to network
}
/** * A blocking method to stream the server's content and build it into a string. * @param url API call * @return String response*/privateStringdownload(Stringurl) throwsIOException {
// Ensure we ALWAYS close these!HttpURLConnection client =null;
InputStream is =null;
try {
// Connect to the server using GET protocolURL server =newURL(url);
client = (HttpURLConnection)server.openConnection();
client.connect();
// Check for valid response code from the serverint status = client.getResponseCode();
is = (status ==HttpURLConnection.HTTP_OK)
? client.getInputStream() : client.getErrorStream();
// Build the response or error as a stringBufferedReader br =newBufferedReader(newInputStreamReader(is));
StringBuilder sb =newStringBuilder();
for (String temp; ((temp = br.readLine()) !=null);) {
sb.append(temp);
}
return sb.toString();
} finally {
if (is !=null) { is.close(); }
if (client !=null) { client.disconnect(); }
}
}
/** * Manual force Android to perform a sync with our SyncAdapter.*/publicstaticvoidperformSync() {
Bundle b =newBundle();
b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(AccountGeneral.getAccount(),
ArticleContract.CONTENT_AUTHORITY, b);
}
}
Declare the SyncAdapter
We need to create a resource package in 'res' called, 'xml', if not already created, and create 'syncadapter.xml'.
android:contentAuthority=(string) - Specifies our ContentProvider's authority, must EXACTLY match
android:accountType=(string) - must match EXACTLY the account type defined in our AccountGeneral
android:userVisible=(true|false) - True if sync is visible to the user
android:allowParallelSyncs=(true|false) - True if can sync in parallel
android:isAlwaysSyncable=(true|false) - True if can be synced at anytime
android:supportsUploading=(true|false) - True if uploads to the server
For Android to use our SyncAdapter, it needs to run it in a bound Service that will allow it to do so. To see the in-depth of how AbstractThreadedSyncAdapter works, read about AIDL for inter-process communication.
We need to create this bound Service so we can let Android use our SyncAdapter class.
/** * This is used only by Android to run our {@link SyncAdapter}.*/publicclassSyncServiceextendsService {
/** * Lock use to synchronize instantiation of SyncAdapter.*/privatestaticfinalObjectLOCK=newObject();
privatestaticSyncAdapter syncAdapter;
@OverridepublicvoidonCreate() {
// SyncAdapter is not Thread-safesynchronized (LOCK) {
// Instantiate our SyncAdapter
syncAdapter =newSyncAdapter(this, false);
}
}
@Nullable@OverridepublicIBinderonBind(Intentintent) {
// Return our SyncAdapter's IBinderreturn syncAdapter.getSyncAdapterBinder();
}
}
Declare SyncService
We need to declare our SyncService in the manifest.xml file.
You now have created the wonderful ability of server synchronization! Here's an example of how to use SynAdapter.
/** * Your SyncAdapter is good to go! * * Your SyncAdapter will run all on its own by Android if you specified it to sync * automatically and periodically. If not, you can force a sync using our performSync() * method we made. * * Use {@link android.database.ContentObserver} to get callbacks for data changes when * Android runs your SyncAdapter or when you manually run it.*/publicclassMainActivityextendsAppCompatActivity {
/** * This is our example content observer.*/privateArticleObserver articleObserver;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create your sync accountAccountGeneral.createSyncAccount(this);
// Perform a manual sync by calling this:SyncAdapter.performSync();
// Setup example content observer
articleObserver =newArticleObserver();
}
@OverrideprotectedvoidonStart() {
super.onStart();
// Register the observer at the start of our activity
getContentResolver().registerContentObserver(
ArticleContract.Articles.CONTENT_URI, // Uri to observe (our articles)true, // Observe its descendants
articleObserver); // The observer
}
@OverrideprotectedvoidonStop() {
super.onStop();
if (articleObserver !=null) {
// Unregister the observer at the stop of our activity
getContentResolver().unregisterContentObserver(articleObserver);
}
}
privatevoidrefreshArticles() {
Log.i(getClass().getName(), "Articles data has changed!");
}
/** * Example content observer for observing article data changes.*/privatefinalclassArticleObserverextendsContentObserver {
privateArticleObserver() {
// Ensure callbacks happen on the UI threadsuper(newHandler(Looper.getMainLooper()));
}
@OverridepublicvoidonChange(booleanselfChange, Uriuri) {
// Handle your data changes here!!!
refreshArticles();
}
}
}