Android scheduled tasks using SyncAdapter
In this post, we are going to create an sample Android app to demonstrate how to set up a scheduled task to run periodically using SyncAdapter, Services and ContentProvider in Android. It’s much like a cron job in unix. This sample app will show a notification message in the status bar every 60 seconds.
1. activity_main.xml in the res/layout
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:layout_centerInParent="true" android:text="Hey! It works if you can see a notification message from the status bar every 1 minute.\n\nUpdate this constant SYNC_INTERVAL in the MyServiceSyncAdapter to change the update frequency." /> </RelativeLayout>
2. In the AndroidManifest.xml, add these permissions. The parent tag of these will be the root tag, manifest
<!-- Permissions required by the sync adapter --> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
3. The MainActivity.java
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyServiceSyncAdapter.initializeSyncAdapter(getApplicationContext()); } }
4. The MyServiceSyncAdapter.java, this is the heavy lifing for the scheduled task.
public class MyServiceSyncAdapter extends AbstractThreadedSyncAdapter { //TODO change this constant SYNC_INTERVAL to change the sync frequency public static final int SYNC_INTERVAL = 60; //60 * 180; // 60 seconds (1 minute) * 180 = 3 hours public static final int SYNC_FLEXTIME = SYNC_INTERVAL/3; private static final int MOVIE_NOTIFICATION_ID = 3004; public MyServiceSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Log.i("MyServiceSyncAdapter", "onPerformSync"); //TODO get some data from the internet, api calls, etc. //TODO save the data to database, sqlite, couchbase, etc notifyDataDownloaded(); } /** * Send the notification message to the status bar */ private void notifyDataDownloaded() { Context context = getContext(); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context) .setSmallIcon(android.support.v7.appcompat.R.drawable.notification_template_icon_bg) .setContentTitle("Sync Adapter") .setContentText("New Data Available!"); // Opening the app when the user clicks on the notification. Intent resultIntent = new Intent(context, MainActivity.class); // The stack builder object will contain an artificial back stack for the started Activity. // This ensures that navigating backward from the Activity leads out of your application to the Home screen. TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); stackBuilder.addNextIntent(resultIntent); PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(resultPendingIntent); NotificationManager mNotificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.notify(MOVIE_NOTIFICATION_ID, mBuilder.build()); // MOVIE_NOTIFICATION_ID allows you to update the notification later on. } /** * Helper method to schedule the sync adapter periodic execution */ public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) { Account account = getSyncAccount(context); String authority = context.getString(R.string.content_authority); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // we can enable inexact timers in our periodic sync SyncRequest request = new SyncRequest.Builder() .syncPeriodic(syncInterval, flexTime) .setSyncAdapter(account, authority) .setExtras(new Bundle()).build(); ContentResolver.requestSync(request); } else { ContentResolver.addPeriodicSync(account, authority, new Bundle(), syncInterval); } } /** * Helper method to have the sync adapter sync immediately * @param context The context used to access the account service */ public static void syncImmediately(Context context) { Log.i("MyServiceSyncAdapter", "syncImmediately"); Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.content_authority), bundle); } /** * Helper method to get the fake account to be used with SyncAdapter, or make a new one * if the fake account doesn't exist yet. If we make a new account, we call the * onAccountCreated method so we can initialize things. * * @param context The context used to access the account service * @return a fake account. */ public static Account getSyncAccount(Context context) { AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); // Get an instance of the Android account manager Account newAccount = new Account(context.getString(R.string.app_name), context.getString(R.string.sync_account_type)); // Create the account type and default account // If the password doesn't exist, the account doesn't exist if (accountManager.getPassword(newAccount) == null) { if (!accountManager.addAccountExplicitly(newAccount, "", null)) { Log.e("MyServiceSyncAdapter", "getSyncAccount Failed to create new account."); return null; } onAccountCreated(newAccount, context); } return newAccount; } private static void onAccountCreated(Account newAccount, Context context) { Log.i("MyServiceSyncAdapter", "onAccountCreated"); MyServiceSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME); ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true); syncImmediately(context); } public static void initializeSyncAdapter(Context context) { Log.d("MyServiceSyncAdapter", "initializeSyncAdapter"); getSyncAccount(context); } }
5. MyServiceSync.java for providing the service to Android to do things you have set up in the SyncAdapter above.
public class MyServiceSync extends Service { private static final Object sSyncAdapterLock = new Object(); private static MyServiceSyncAdapter myServiceSyncAdapter = null; @Override public void onCreate() { super.onCreate(); Log.d("MyServiceSync", "onCreate"); synchronized (sSyncAdapterLock) { if (myServiceSyncAdapter == null) { myServiceSyncAdapter = new MyServiceSyncAdapter(getApplicationContext(), true); } } } @Override public IBinder onBind(Intent intent) { Log.d("MyServiceSync", "onBind"); return myServiceSyncAdapter.getSyncAdapterBinder(); } }
6. MyAuthenticator.java for creating a dummy authenticator for the SyncAdapter to use.
public class MyAuthenticator extends AbstractAccountAuthenticator { public MyAuthenticator(Context context) { super(context); } // No properties to edit. @Override public Bundle editProperties( AccountAuthenticatorResponse r, String s) { throw new UnsupportedOperationException(); } // Because we're not actually adding an account to the device, just return null. @Override public Bundle addAccount( AccountAuthenticatorResponse r, String s, String s2, String[] strings, Bundle bundle) throws NetworkErrorException { return null; } // Ignore attempts to confirm credentials @Override public Bundle confirmCredentials( AccountAuthenticatorResponse r, Account account, Bundle bundle) throws NetworkErrorException { return null; } // Getting an authentication token is not supported @Override public Bundle getAuthToken( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Getting a label for the auth token is not supported @Override public String getAuthTokenLabel(String s) { throw new UnsupportedOperationException(); } // Updating user credentials is not supported @Override public Bundle updateCredentials( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Checking features for the account is not supported @Override public Bundle hasFeatures( AccountAuthenticatorResponse r, Account account, String[] strings) throws NetworkErrorException { throw new UnsupportedOperationException(); } }
7. MyAuthenticatorService.java for providing service for the SyncAdapter to access the authenticator.
public class MyAuthenticatorService extends Service { // Instance field that stores the authenticator object private MyAuthenticator mAuthenticator; @Override public void onCreate() { Log.d("MyAuthenticatorService", "onCreate"); // Create a new authenticator object mAuthenticator = new MyAuthenticator(this); } /* * When the system binds to this Service to make the RPC call * return the authenticator's IBinder. */ @Override public IBinder onBind(Intent intent) { Log.d("MyAuthenticatorService", "onBind"); return mAuthenticator.getIBinder(); } }
8. MyContentProvider.java, a dummy content provider, just to support the sync adapter because your app need a content provider in order for the ContentResolver trigger the SyncAdapter.
public class MyContentProvider extends ContentProvider { @Override public boolean onCreate() { return true; } @Override public String getType(Uri uri) { return null; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int bulkInsert(Uri uri, ContentValues[] values) { return 0; } }
9. Add the following in the res/strings.xml
<resources> <string name="app_name">Sync Adapter</string> <!-- SyncAdapter related --> <string name="sync_account_type">my.example.com</string> <string name="content_authority">com.sample.app.syncadapter</string> </resources>
10. Create a xml directory in res/ folder, and the create the following 2 xml files in it.
authenticator.xml
<?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="@string/sync_account_type" android:icon="@android:drawable/ic_dialog_info" android:label="@string/app_name" android:smallIcon="@android:drawable/ic_dialog_info" />
syncadapter.xml
<?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:contentAuthority="@string/content_authority" android:accountType="@string/sync_account_type" android:userVisible="false" android:supportsUploading="false" android:allowParallelSyncs="false" android:isAlwaysSyncable="true" />
11. Register the content provider and the services defined above in the AndroidManifest.xml, they should be in the application tag
<provider android:authorities="@string/content_authority" android:name=".MyContentProvider" android:exported="false" android:syncable="true" /> <!-- SyncAdapter's dummy authentication service --> <service android:name=".MyAuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service> <!-- The SyncAdapter service --> <service android:name=".MyServiceSync" android:exported="true"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" /> </service>
12. Launch the app and see if it works!
Search within Codexpedia
Search the entire web