Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The death of the refresh button

Mathieu Calba
September 23, 2014

The death of the refresh button

Almost every app mobile need to synchronise data with a server, on Android too. But their is many ways to do it : AlertManagers & Services, SyncAdapter... and every ones has its pros & cons. Recently Google added a new way to do asynchronous work on Android in the L preview release : JobScheduler.
We are going to see how each techniques work and we've done it in the Capitaine Train Android application for a feedback with a complex data model.

Video: https://skillsmatter.com/skillscasts/5991-the-death-of-the-refresh-button or https://www.youtube.com/watch?v=Dvshiz9pbUc

Mathieu Calba

September 23, 2014

Transcript

63 slides
  1. So he decided to create his own app, focused on

    user experience and simplicity
  2. BACKGROUND JOB • Non-user facing operation • Upload and/or download

    • Requires network connectivity • Always leave the data in good state
  3. • Only when needed, and when relevant (network availability, battery

    level, etc) • Restore after reboot • Exponential back-off • Be careful with battery life • etc. WHEN APPROPRIATE aka scheduling & triggering
  4. • Best solution: push message • Not always available, fallback:

    periodic polling WHEN APPROPRIATE aka scheduling & triggering
  5. AsyncTask & AlarmManager Create your AsyncTask: ! public final class

    PeriodicTaskRunnable extends AsyncTask<Void, Void, Void> {
 @Override
 protected Void doInBackground(Void... voids) {
 // TODO the task !
 return null;
 }
 }
 Start the periodic run with the AlarmManager: 
 private void triggerPeriodicAsyncTask() {
 final Intent intent = new Intent(this, PeriodicTaskReceiver.class);
 
 final PendingIntent receiverPendingIntent = PendingIntent.
 getBroadcast(this, 140916, intent, PendingIntent.FLAG_UPDATE_CURRENT);
 
 final AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
 alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, //
 SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR, //
 AlarmManager.INTERVAL_HOUR, //
 receiverPendingIntent);
 }
  6. Create your BroadcastReceiver: ! public class PeriodicTaskReceiver extends BroadcastReceiver {


    
 public static final String ACTION_SYNC = “com.mathieucalba.tasks.ACTION_SYNC";
 
 @Override
 public void onReceive(Context context, Intent intent) {
 if (ACTION_SYNC.equals(intent.getAction())) {
 new PeriodicTaskRunnable().execute();
 }
 }
 }
 And declare it in the Manifest: ! <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.mathieucalba.testjobscheduler">
 <application>
 
 <receiver
 android:name=".tasks.PeriodicTaskReceiver"
 android:exported="false">
 <intent-filter>
 <action android:name="com.mathieucalba.tasks.ACTION_SYNC" />
 </intent-filter>
 </receiver>
 
 </application>
 </manifest> AsyncTask & AlarmManager
  7. • AsyncTask & Receiver can be replaced by an IntentService

    for simplicity • Restoring periodic sync after a reboot? • Handling connectivity availability? • Indicate sync state to others • etc. AsyncTask & AlarmManager
  8. Definition One method to do the UPLOAD & DOWNLOAD SYNC

    (SyncAdapter) for one DATA PROVIDER (ContentProvider) associated with a USER ACCOUNT (AccountAuthenticator) which can be TRIGGERED MANUALLY or by the SYSTEM.
  9. public final class AccountAuthenticator extends AbstractAccountAuthenticator {
 
 public static

    final String ACCOUNT_TYPE = "com.mathieucalba.testjobscheduler";
 public static final String ACCOUNT_NAME_SYNC = "com.mathieucalba.testjobscheduler";
 
 public AccountAuthenticator(Context context) {
 super(context);
 }
 
 @Override
 public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
 return null;
 }
 
 @Override
 public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
 return null;
 }
 
 @Override
 public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
 throw new UnsupportedOperationException();
 } //… } 1- Account
  10. public final class AccountAuthenticator extends AbstractAccountAuthenticator {
 //…
 @Override
 public

    Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
 throw new UnsupportedOperationException();
 }
 
 @Override
 public String getAuthTokenLabel(String authTokenType) {
 throw new UnsupportedOperationException();
 }
 
 @Override
 public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
 throw new UnsupportedOperationException();
 }
 
 @Override
 public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
 throw new UnsupportedOperationException();
 }
 } 1- Account
  11. Bind the AccountAuthenticator to the framework: ! public final class

    AccountAuthenticatorService extends Service {
 
 private static final Object LOCK = new Object();
 
 private static AccountAuthenticator sAuthenticator;
 
 @Override
 public void onCreate() {
 super.onCreate();
 synchronized (LOCK) {
 if (sAuthenticator == null) {
 sAuthenticator = new AccountAuthenticator(getApplicationContext());
 }
 }
 }
 
 @Override
 public IBinder onBind(Intent intent) {
 synchronized (LOCK) {
 return sAuthenticator.getIBinder();
 }
 }
 } 1- Account
  12. Declare your AccountAuthenticatorService into the Manifest: ! <?xml version="1.0" encoding="utf-8"?>


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.mathieucalba.testsyncadapter">
 <application>
 <service
 android:exported="true"
 android:name=".accounts.AccountAuthenticatorService">
 <intent-filter>
 <action android:name="android.accounts.AccountAuthenticator" />
 </intent-filter>
 
 <meta-data
 android:name="android.accounts.AccountAuthenticator"
 android:resource="@xml/account_authenticator" />
 </service> 
 </application>
 </manifest> ! Configure your AccountAuthenticator: ! <?xml version="1.0" encoding="utf-8"?>
 <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
 android:accountType="@string/config_accountType"
 android:icon="@drawable/ic_launcher"
 android:label="@string/app_name"
 android:smallIcon="@drawable/ic_launcher" /> 1- Account
  13. Add the Account required by the framework: ! private void

    initAccountAuthenticator() {
 final AccountManager accountManager = AccountManager.get(this);
 final Account[] accounts = accountManager.getAccountsByType(AccountAuthenticator.ACCOUNT_TYPE);
 for (Account account : accounts) {
 if (AccountAuthenticator.ACCOUNT_NAME_SYNC.equals(account.name)) {
 return;
 }
 }
 
 accountManager.addAccountExplicitly(new Account(AccountAuthenticator.ACCOUNT_NAME_SYNC,
 AccountAuthenticator.ACCOUNT_TYPE), null, null);
 } 1- Account For more info on implementing an AccountAuthenticator: http:// udinic.wordpress.com/2013/04/24/write-your-own-android-authenticator/
  14. Create a stub ContentProvider: ! public class StubProvider extends ContentProvider

    {
 
 @Override
 public boolean onCreate() { return true; }
 
 @Override
 public String getType(Uri uri) { return new String(); }
 
 @Override
 public Cursor query(Uri uri, String[] strings, String s, String[] strings2, String s2) { return null; }
 
 @Override
 public Uri insert(Uri uri, ContentValues contentValues) { return null; }
 
 @Override
 public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { return 0; }
 
 @Override
 public int delete(Uri uri, String s, String[] strings) { return 0; }
 
 } 2- ContentProvider
  15. Declare your ContentProvider in the Manifest: ! <?xml version="1.0" encoding="utf-8"?>


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.mathieucalba.testsyncadapter">
 
 <application>
 
 <provider
 android:authorities="com.mathieucalba.testsyncadapter.provider.stub"
 android:exported="false"
 android:name=".provider.StubProvider"
 android:syncable="true" />
 
 </application>
 
 </manifest> 2- ContentProvider For more details about ContentProviders, see my slides at http://bit.ly/ContentProvider
  16. Implement the synchronization mechanism: ! public class SyncAdapter extends AbstractThreadedSyncAdapter

    {
 
 public SyncAdapter(Context context) {
 super(context, false, false); // Context, auto initialize, parallel sync
 }
 
 @Override
 public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
 // TODO the sync
 }
 
 } 3- SyncAdapter
  17. Bind the SyncAdapter to the framework: ! public class SyncAdapterService

    extends Service {
 
 private static final Object LOCK = new Object();
 
 private static SyncAdapter sSyncAdapter = null;
 
 @Override
 public void onCreate() {
 super.onCreate();
 synchronized (LOCK) {
 if (sSyncAdapter == null) {
 sSyncAdapter = new SyncAdapter(getApplicationContext());
 }
 }
 }
 
 @Override
 public IBinder onBind(Intent intent) {
 synchronized (LOCK) {
 return sSyncAdapter.getSyncAdapterBinder();
 }
 }
 } 3- SyncAdapter
  18. Declare your SyncAdapterService into the Manifest: ! <?xml version="1.0" encoding="utf-8"?>


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.mathieucalba.testsyncadapter">
 
 <application>
 
 <service
 android:exported="true"
 android:name=".sync.SyncAdapterService">
 <intent-filter>
 <action android:name="android.content.SyncAdapter" />
 </intent-filter>
 
 <meta-data
 android:name="android.content.SyncAdapter"
 android:resource="@xml/sync_adapter" />
 </service>
 
 </application>
 
 </manifest> 3- SyncAdapter
  19. Configure your SyncAdapter: ! <?xml version="1.0" encoding="utf-8"?>
 <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
 android:accountType="@string/config_accountType"


    android:allowParallelSyncs="false"
 android:contentAuthority="com.mathieucalba.testsyncadapter.provider.stub"
 android:isAlwaysSyncable="true"
 android:supportsUploading="true"
 android:userVisible="false" /> 3- SyncAdapter
  20. How to trigger a one time sync: ! private static

    void triggerSyncAdapter(Account account, boolean now) {
 final Bundle extras = new Bundle();
 
 if (now) {
 // ignore backoff && settings extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // put the request at the front of the queue
 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
 }
 
 ContentResolver.requestSync(account, StubProvider.AUTHORITY, extras);
 } 4- Triggering
  21. How to configure an account for periodic sync, auto sync

    when network is up: ! public static void setupSync(Account account) {
 if (account == null) {
 return;
 }
 
 // Inform the system that this account supports sync
 ContentResolver.setIsSyncable(account, StubProvider.AUTHORITY, 1);
 // Inform the system that this account is eligible for auto sync when the network is up
 ContentResolver.setSyncAutomatically(account, StubProvider.AUTHORITY, true);
 // Recommend a schedule for automatic synchronisation. The system may modify this based
 // on other scheduled syncs and network utilisation.
 ContentResolver.addPeriodicSync(account, // StubProvider.AUTHORITY, // new Bundle(), // TimeUnit.HOURS.toSeconds(1));
 } 4- Triggering ! Periodic & automatic sync doesn’t works if user has deactivated it
  22. Mark the item to be deleted instead of deleting it:

    ! @Override
 public int delete(Uri uri, String s, String[] strings) {
 final int count;
 if (StubContract.hasNeedSyncToNetworkParameter(uri)) {
 final int match = URI_MATCHER.match(uri);
 switch (match) {
 case OPTION_ID:
 final ContentValues values = new ContentValues();
 values.put(SyncColumns.SYNC_DELETED, SyncColumns.SYNC_DELETED_MARKED);
 count = buildSimpleSelection(uri).
 where(selection, selectionArgs).
 update(getDatabaseHelper().getWritableDatabase(), values);
 break;
 
 default:
 throw new SyncToNetworkUnknownUriException(uri);
 }
 
 } else {
 count = buildSimpleSelection(uri).
 where(selection, selectionArgs).
 delete(getDatabaseHelper().getWritableDatabase());
 }
 
 if (count > 0) {
 notifyChange(uri);
 }
 return count;
 } 5- ContentProvider Triggering
  23. Notify the change to the SyncAdapter: ! private void notifyChange(Uri

    uri) {
 getContext().getContentResolver(). notifyChange(uri, null, isCallerUriUploadReady(uri) && !isCallerSyncAdapter(uri));
 }
 
 public boolean isCallerSyncAdapter(Uri uri) {
 return StubContract.hasCallerIsSyncAdapterParameter(uri);
 }
 
 private boolean isCallerUriUploadReady(Uri uri) {
 if (StubContract.hasNeedSyncToNetworkParameter(uri)) {
 final int match = URI_MATCHER.match(uri);
 switch (match) {
 case OPTION_ID:
 return true;
 
 default:
 throw new SyncToNetworkUnknownUriException(uri);
 }
 }
 return false;
 } 5- ContentProvider Triggering
  24. How we detect an URI is from the SyncAdapter, and

    when a SyncAdapter trigger is needed: ! interface SyncExtras {
 String IS_CALLER_SYNC_ADAPTER = "is_caller_sync_adapter";
 
 String NEED_SYNC_TO_NETWORK = "need_sync_to_network";
 }
 
 public static Uri addCallerIsSyncAdapterParameter(Uri uri) {
 return uri.buildUpon().appendQueryParameter(SyncExtras.IS_CALLER_SYNC_ADAPTER, Boolean.toString(true)).build();
 }
 
 public static boolean hasCallerIsSyncAdapterParameter(Uri uri) {
 final String parameter = uri.getQueryParameter(SyncExtras.IS_CALLER_SYNC_ADAPTER);
 return parameter != null && Boolean.parseBoolean(parameter);
 }
 
 public static Uri addNeedSyncToNetworkParameter(Uri uri) {
 return uri.buildUpon().appendQueryParameter(SyncExtras.NEED_SYNC_TO_NETWORK, Boolean.toString(true)).build();
 }
 
 public static boolean hasNeedSyncToNetworkParameter(Uri uri) {
 final String parameter = uri.getQueryParameter(SyncExtras.NEED_SYNC_TO_NETWORK);
 return parameter != null && Boolean.parseBoolean(parameter);
 } 5- ContentProvider Triggering
  25. Be careful, your corresponding query should not return the deleted

    data if it’s not the SyncAdapter querying. 5- ContentProvider Triggering
  26. • Google way • Easily triggered by the system when

    appropriate (network availability, change in associated ContentProvider) • Needs an AccountAuthenticator (at least a stub) • Needs a ContentProvider (at least a stub) SyncAdapter
  27. Definition SCHEDULE the execution of a JOB (via a Service)

    with VARIOUS PARAMETERS about WHEN the execution SHOULD HAPPEN (during a window of time, periodically, with network needed, etc.)
  28. The JobService is where we are awaken: ! public class

    MyJobService extends JobService {
 
 private ExecutorService mExecutor;
 private final Handler mHandler = new Handler(Looper.getMainLooper());
 
 @Override
 public void onCreate() {
 super.onCreate();
 mExecutor = Executors.newSingleThreadExecutor();
 }
 
 @Override
 public void onDestroy() {
 mExecutor.shutdown();
 super.onDestroy();
 }
 
 @Override
 public boolean onStartJob(JobParameters jobParameters) { // We are on the Main thread, so post the Task to a background thread
 mExecutor.execute(new Task(jobParameters));
 return true;
 }
 
 @Override
 public boolean onStopJob(JobParameters jobParameters) {
 // TODO interrupt Task
 return true;
 } } 1- JobService
  29. private final class Task implements Runnable { 
 private final

    JobParameters mJobParameters;
 
 private Task(JobParameters jobParameters) { mJobParameters = jobParameters; }
 
 @Override
 public void run() {
 // TODO the network call
 mHandler.post(new FinishedTask(mJobParameters, true));
 }
 }
 
 private final class FinishedTask implements Runnable { 
 private final JobParameters mJobParameters;
 private final boolean mIsSuccess;
 
 private FinishedTask(JobParameters jobParameters, boolean isSuccess) {
 mJobParameters = jobParameters;
 mIsSuccess = isSuccess;
 }
 
 @Override
 public void run() {
 // Notify that the job has ended
 jobFinished(mJobParameters, mIsSuccess);
 }
 } 1- JobService
  30. Declare your MyJobService into the Manifest: ! <?xml version="1.0" encoding="utf-8"?>


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.mathieucalba.testjobscheduler">
 
 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
 <application>
 
 <service
 android:name=".tasks.MyJobService"
 android:permission="android.permission.BIND_JOB_SERVICE"
 android:exported="true"/>
 
 </application>
 
 </manifest> 1- JobService
  31. Exemple for triggering a one time Job: ! JobInfo.Builder builder

    = new JobInfo.Builder(JOB_ID_1, new ComponentName(this, MyJobService.class)).
 setBackoffCriteria(TimeUnit.MINUTES.toMillis(1), JobInfo.BackoffPolicy.EXPONENTIAL).
 setMinimumLatency(TimeUnit.SECONDS.toMillis(5)).
 setOverrideDeadline(TimeUnit.HOURS.toMillis(1)).
 setRequiredNetworkCapabilities(JobInfo.NetworkType.ANY);
 ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(builder.build());
 ! Exemple for triggering a periodic Job: ! builder = new JobInfo.Builder(JOB_ID_PERIODIC, new ComponentName(this, MyJobService.class)).
 setPeriodic(TimeUnit.HOURS.toMillis(1)).
 setRequiredNetworkCapabilities(JobInfo.NetworkType.UNMETERED).
 setRequiresDeviceIdle(true).
 setRequiresCharging(true);
 
 ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(builder.build()); 2- Triggering
  32. JobScheduler • Configurable scheduling (idle-mode, network availability, etc) across all

    the system • Simple, based upon a Service • Persisted state • Android-Lollipop+ only
  33. • SyncAdapter perfect if account and ContentProvider, great otherwise but

    can be tricky • JobScheduler very promising, but Android Lollipop+ only, a limited compat library would help spread its use (based upon Service & AlarmManager). Conclusion
  34. • We have the tools to create an app the

    works seamlessly without an internet connection so the user never have to worry about his internet connection Conclusion
  35. • Up & Down icons by Guillaume Berry • The

    Search Underneath the Bed by Arielle Nadel • Yorkshire Moors Milky Way 3 by Matthew Savage • Injured Piggy Bank With Crutches by Ken Teegardin • No Internet by Marcelo Graciolli • Balancing The Account By Hand by Ken Teegardin • Rage by Amy McTigue • Daniel Foster by Online Shopping • 3D Bright Idea by Chris Potter • Landscape by Ben Simo • Christmas Eve Sunrise by Jay Parker Credits

More Decks by Mathieu Calba

Other Decks in Programming