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 Slide

  2. @Mathieu_Calba
    +MathieuCalba

    View Slide

  3. View Slide

  4. View Slide

  5. Once upon a time…

    View Slide

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

    View Slide

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

    View Slide

  8. They all seemed
    broken

    View Slide

  9. They always needed an internet connection

    View Slide

  10. Even just to check
    his leisure budget

    View Slide

  11. Julien was very ANGRY

    View Slide

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

    View Slide

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

    View Slide

  14. What does his app need?

    View Slide

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

    View Slide

  16. Mobile application
    UI App
    backend
    Server
    API

    View Slide

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

    View Slide

  18. What is syncing?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. • 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 Slide

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

    View Slide

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

    View Slide

  25. How?

    View Slide

  26. How?
    The Android way

    View Slide

  27. How?
    The Android ways

    View Slide

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

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

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

  31. SyncAdapter

    View Slide

  32. 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 Slide

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

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

  35. 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 Slide

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

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

  38. 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 Slide

  39. 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 Slide

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

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

  42. 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 Slide

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

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

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

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

    View Slide

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

  48. 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 Slide

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

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

    View Slide

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

  52. JobScheduler

    View Slide

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

  54. 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 Slide

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

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

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

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

    View Slide

  59. Conclusion

    View Slide

  60. • 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 Slide

  61. • 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 Slide

  62. Questions?

    View Slide

  63. • 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 Slide