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
Tweet

More Decks by Mathieu Calba

Other Decks in Programming

Transcript

  1. The
    Death
    of the
    Refresh Button

    View full-size slide

  2. @Mathieu_Calba
    +MathieuCalba

    View full-size slide

  3. Once upon a time…

    View full-size slide

  4. Julien was searching for an app to
    manage his bank account

    View full-size slide

  5. But he couldn’t find the one
    that meets his needs

    View full-size slide

  6. They all seemed
    broken

    View full-size slide

  7. They always needed an internet connection

    View full-size slide

  8. Even just to check
    his leisure budget

    View full-size slide

  9. Julien was very ANGRY

    View full-size slide

  10. He needed a simple
    debit/credit managing
    app with a good
    backup

    View full-size slide

  11. So he decided to create his own app, focused on
    user experience and simplicity

    View full-size slide

  12. What does his app need?

    View full-size slide

  13. A great app backend
    What does his app need?

    View full-size slide

  14. Mobile application
    UI App
    backend
    Server
    API

    View full-size slide

  15. With a great app backend,
    you have more flexibility
    at the UI level

    View full-size slide

  16. What is syncing?

    View full-size slide

  17. What is syncing?
    Executing a
    BACKGROUND JOB
    to update local data
    WHEN APPROPRIATE

    View full-size slide

  18. BACKGROUND JOB
    • Non-user facing operation
    • Upload and/or download
    • Requires network connectivity
    • Always leave the data in good state

    View full-size slide

  19. What is syncing?
    Executing a
    BACKGROUND JOB
    to update local data
    WHEN APPROPRIATE

    View full-size slide

  20. • 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

    View full-size slide

  21. • Best solution: push message
    • Not always available, fallback: periodic
    polling
    WHEN APPROPRIATE
    aka scheduling & triggering

    View full-size slide

  22. What is syncing?
    Executing a
    BACKGROUND JOB
    to update local data
    WHEN APPROPRIATE

    View full-size slide

  23. How?
    The Android way

    View full-size slide

  24. How?
    The Android ways

    View full-size slide

  25. AsyncTask & AlarmManager
    Create your AsyncTask:
    !
    public final class PeriodicTaskRunnable extends AsyncTask {

    @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);

    }

    View full-size slide

  26. 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:
    !

    package="com.mathieucalba.testjobscheduler">



    android:name=".tasks.PeriodicTaskReceiver"

    android:exported="false">








    AsyncTask & AlarmManager

    View full-size slide

  27. • 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

    View full-size slide

  28. 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.

    View full-size slide

  29. 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

    View full-size slide

  30. 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

    View full-size slide

  31. 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

    View full-size slide

  32. Declare your AccountAuthenticatorService into the Manifest:
    !

    package="com.mathieucalba.testsyncadapter">


    android:exported="true"

    android:name=".accounts.AccountAuthenticatorService">





    android:name="android.accounts.AccountAuthenticator"

    android:resource="@xml/account_authenticator" />





    !
    Configure your AccountAuthenticator:
    !

    android:accountType="@string/config_accountType"

    android:icon="@drawable/ic_launcher"

    android:label="@string/app_name"

    android:smallIcon="@drawable/ic_launcher" />
    1- Account

    View full-size slide

  33. 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/

    View full-size slide

  34. 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

    View full-size slide

  35. Declare your ContentProvider in the Manifest:
    !

    package="com.mathieucalba.testsyncadapter">




    android:authorities="com.mathieucalba.testsyncadapter.provider.stub"

    android:exported="false"

    android:name=".provider.StubProvider"

    android:syncable="true" />





    2- ContentProvider
    For more details about ContentProviders,
    see my slides at http://bit.ly/ContentProvider

    View full-size slide

  36. 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

    View full-size slide

  37. 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

    View full-size slide

  38. Declare your SyncAdapterService into the Manifest:
    !

    package="com.mathieucalba.testsyncadapter">




    android:exported="true"

    android:name=".sync.SyncAdapterService">





    android:name="android.content.SyncAdapter"

    android:resource="@xml/sync_adapter" />






    3- SyncAdapter

    View full-size slide

  39. Configure your SyncAdapter:
    !

    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

    View full-size slide

  40. 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

    View full-size slide

  41. 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

    View full-size slide

  42. One neat functionality: run the
    SyncAdapter when ContentProvider’s
    data changes
    5- ContentProvider Triggering

    View full-size slide

  43. 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

    View full-size slide

  44. 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

    View full-size slide

  45. 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

    View full-size slide

  46. Be careful, your corresponding query should not
    return the deleted data if it’s not the
    SyncAdapter querying.
    5- ContentProvider Triggering

    View full-size slide

  47. • 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

    View full-size slide

  48. JobScheduler

    View full-size slide

  49. 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.)

    View full-size slide

  50. 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

    View full-size slide

  51. 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

    View full-size slide

  52. Declare your MyJobService into the Manifest:
    !

    package="com.mathieucalba.testjobscheduler">






    android:name=".tasks.MyJobService"

    android:permission="android.permission.BIND_JOB_SERVICE"

    android:exported="true"/>





    1- JobService

    View full-size slide

  53. 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

    View full-size slide

  54. JobScheduler
    • Configurable scheduling (idle-mode,
    network availability, etc) across all the
    system
    • Simple, based upon a Service
    • Persisted state
    • Android-Lollipop+ only

    View full-size slide

  55. • 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

    View full-size slide

  56. • 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

    View full-size slide

  57. • 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

    View full-size slide