Author: keithabeatty

Android Image loading – moving from Picasso to Glide (or perhaps not?)

In my app, I’ve got a background task that adds or update Realm object store and the UI has view that uses a RealmRecyclerView. The app uses Picasso 2.5.2 to load images into a ImageView.

I noticed after the refresh was done on existing data the RealmRecyclerView, Picasso would load up the wrong cached image. If you would move the list up and back, the correct image would show up. It doesn’t do this consistently, but it does do it enough that I do see it. I’ve searched on this problem, but haven’t seen anything that mentions this, other than an open issue that hasn’t received any update since last year.

With the last release of my app, I thought I’d take a care of this by moving from Picasso over to Glide along with moving from Android Studio 2.3.x to Android 3. Unfortunately, most of the documentation is dealing with Glide 3.x, so the only real examples come from Glide’s own documentation, which did provide some information, but it’s not great.¬† When I looked at the latest release, Glide was at 4.3.1. I switched over and used this as my starting point:

Glide.with(mContext)
        .load(picFullUri)
        .into(viewHolder.image);

When I started the app, I noticed that there wasn’t an Picasso .fit() that went like this. When I ran it, I noticed that the various images weren’t loading into the ImageView in the same height size, but in whatever size that they were on the host. I found that there was an option in the Glide 3.x –

.centerCrop()

I added this, but that method doesn’t exist in Glide 4.x. When researching Glide 4.3.1, I found that I needed to:

RequestOptions options = new RequestOptions().centerCrop();
Glide.with(this)
        .load(avatarUri)
        .apply(options)
        .into(mUserAvatar);

Also, with RequestOptions there is a .placeholder() as well as an .error() method to show a .drawable object.

I tried this and it didn’t work. I found this method

 .fitCenter()

and that worked better, but wasn’t 100%, but worked OK. At this point I found Glide had a new release 4.4.0. I tried it and found that it when starting up the app, it would crash. Looking at my app’s gradle.build file, I found that

com.android.support:preference-v14.26.1.0

was showing that Glide was using 27.x version and when I switched back to 4.3.1, it worked again.

Release?

I uploaded a release to the Google Play store and found with the pre-release test on a Sony device, it would get an Out Of Memory error. I found, when going thru the issues list, that it also occurred for others. They suggested that changing from the default PREFER_ARGB_888 to 565 via this method on the RequestOptions:

.format(DecodeFormat.PREFER_RGB_565)

I added this and re-uploaded a release, as I didn’t have the Sony virtual device emulator configuration. The pre-release test went thru without an OOM but on another device, it noted a possible ANR error. I downloaded the logcat from GPS and found that the ANR wasn’t caused by my app, but it did show several Glide OOM errors in the log:

12-04 17:12:11.198: E/GlideExecutor(14563): Request threw uncaught throwable
12-04 17:12:11.198: E/GlideExecutor(14563): java.lang.OutOfMemoryError: Failed to allocate a 12616716 byte allocation with 1783408 free bytes and 1741KB until OOM
12-04 17:12:11.198: E/GlideExecutor(14563): at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
12-04 17:12:11.198: E/GlideExecutor(14563): at android.graphics.Bitmap.nativeCreate(Native Method)
12-04 17:12:11.198: E/GlideExecutor(14563): at android.graphics.Bitmap.createBitmap(Bitmap.java:879)
12-04 17:12:11.198: E/GlideExecutor(14563): at android.graphics.Bitmap.createBitmap(Bitmap.java:856)
12-04 17:12:11.198: E/GlideExecutor(14563): at android.graphics.Bitmap.createBitmap(Bitmap.java:823)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool.get(LruBitmapPool.java:129)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.resource.bitmap.TransformationUtils.centerCrop(TransformationUtils.java:108)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.resource.bitmap.CenterCrop.transform(CenterCrop.java:39)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.resource.bitmap.BitmapTransformation.transform(BitmapTransformation.java:81)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob$DecodeCallback.onResourceDecoded(DecodeJob.java:532)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodePath.decode(DecodePath.java:44)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.LoadPath.loadWithExceptionList(LoadPath.java:56)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.LoadPath.load(LoadPath.java:42)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.runLoadPath(DecodeJob.java:494)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.decodeFromFetcher(DecodeJob.java:466)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.decodeFromData(DecodeJob.java:452)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:406)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:375)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DataCacheGenerator.onDataReady(DataCacheGenerator.java:91)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.model.ByteBufferFileLoader$ByteBufferFetcher.loadData(ByteBufferFileLoader.java:69)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DataCacheGenerator.startNext(DataCacheGenerator.java:71)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:298)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:265)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:229)
12-04 17:12:11.198: E/GlideExecutor(14563): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
12-04 17:12:11.198: E/GlideExecutor(14563): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
12-04 17:12:11.198: E/GlideExecutor(14563): at java.lang.Thread.run(Thread.java:761)
12-04 17:12:11.198: E/GlideExecutor(14563): at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:413)

Unfortunately, there were several of these errors in the logcat. What happened on the UI is that the .error() method is executed and the drawable placeholder is shown instead of the image that caused the OOM error.

The End?

Based on these errors, Glide wasn’t a good replacement for me in this application. From this video, there are two other image loaders, Facebook’s Fresco and Universal Image Loader The problem with UIL is that it is no longer maintained, so that is no longer an option. The problemd with Fresco is that it doesn’t load into an ImageView, but you need to use a custom XML object SimpleDraweeView. At this point I don’t really want to change my ImageViews.

Based on this, Picasso, despite the cache load errors, is making a comeback. <sigh>

 

Advertisements

Bintray redux

I first wrote about my work to have Android Gradle push to Bintray a while ago in Distributing an Android library. In my initial setup, I chose poorly with the name using com.b12kab.tmdblibrary and having the tmdblibrary as my project name. I should have named the package something other than tmdblibrary, however that is too late to change now.

What I found is that when I pushed to Bintray it would strangely push the source, aar & JavaDoc files and put them in two locations. I put a note out on the Gradle Bintray issue asking about this back early this year. I just received a reply from one of the project people (I think). They asked what was going on with it and I replied that I still noted the same issue, but just went over to Bintray and deleted the duplicate uploaded files.

I also noted that if I had used the single Gradle file that they gave as an example, that Android Studio would no longer consider the project as running under Gradle. I don’t know about you, but that’s just a non-starter for me. ūüôā

Android Gradle changes

As I noted in my first article on this, the Bintray plugin has several problems with repeating the data all over the enclosure instead of having one spot to define the variables to use and then in the appropriate space, use the appropriate variable.

With my Android Oreo upgrade that I noted in IntentService to JobScheduler, I went from extending various items to using my library. I did find a few items that I needed to change and found that items that I had used earlier to avoid JavaDoc generated issue with external (to my) project, such as GSon, I had to modify the uploading routine again, commenting out property type checking section and adding this, allowing my JavaDoc generator to work (with errors, but worked):

afterEvaluate {
 javadoc.classpath += files(android.libraryVariants.collect { variant ->
 variant.javaCompile.classpath.files
 })
}

 

If you have a better example of how to upload to Bintray that also plays nice with Android Studio w/o a ton of duplication of strings, etc – please let me know.

RealmRecyclerView upgrade

If you used the RealmRecyclerView in your use of Realm on Android, you probably got it from here. It works OK, but as time goes on, it gets older and older away from the current version of Realm. The RealmRecyclerView’s last Realm version that it supports is Realm 2.2.1.

Luckily m2f started a update of this from a fork. Luckily, the source code change needed was very minor. When you look at the releases, (as of Nov 20, 2017), version 1.3 is the latest version of it supporting the latest version of Realm 4.2.0.¬† Note: when you include 1.3, be sure that you use 1.3 and don’t use 1.3.0 in your app build.gradle file:

implementation 'com.github.m2f.realm-recyclerview:library:1.3'

Also be sure to update your project build.grade file to use the correct Realm gradle file:

classpath 'io.realm:realm-gradle-plugin:4.2.0'

 

How long did your app take to get approved? It took my app…

…almost 2 months to get approved.

I knew it wouldn’t be easy; as I saw others using my service provider’s data had problems and I’d likely be faced with them too.

It starts

I knew from my last app that having a good and accurate name was important. When I looked at my app name and comparing to what it could do in any state (signed in or not signed in) – the name wouldn’t work. Specifically, if you didn’t create an account on the service provider – you couldn’t actually do that function. I had to rename it. I didn’t have a good idea, but another person suggested over dinner (to what I thought) was a great name.

I looked in the app store and no other app had that name, so I was OK there. I looked under trademarks and it wasn’t trademarked, so I was OK there too. I went and changed the app name in the strings.xml file and pushed it up to the Google Play store.

Opening Outlook the next morning: I received the pre-launch report email, and it reported no issues – always good. Unfortunately, I also received an email from Google Play Support. That’s when my trouble started. The email said that I violated a impersonation policy. I went to the appeal form and selected the ‘not enough detail was provided’ option and stated that it wasn’t in the store and it wasn’t trademarked, where was the issue at and hit submit.

And the first wait 72 hour wait period started.

I received a response pointing out that name was associated with a website. Not a website that I had ever heard of, but there you go.

I had received another app name from another person. It wasn’t: 1) in the app store, 2) not trademarked and 3) didn’t have a website. I changed the app name in the strings.xml and pushed to GPS again.

This (relatively straightforward) issue took around a 10 days to resolve.

Opening Outlook the next morning: I received the pre-launch report email, and another from Google Play Support.

It continues

To make this story shorter, this next group (about a month) of rejections dealt with Device and Network Abuse policy. The email said my app was “downloading, monetizing, or otherwise accessing YouTube videos in violation of the YouTube Terms of Service or YouTube API Terms of Service”. I wasn’t using YouTube API’s within my app, but I did link out to either a browser or the YouTube app to play videos. This was really confusing and really poorly explained. I never did understand (until later) what this was really about. To get out of this mess, I changed my app so it wouldn’t show any trailer type unless the original release date was before January 1, 1923 – uncontested movie post-copyright date.

I made the code change described above and pushed it up to the Google Play store.

Opening Outlook the next morning: I received the pre-launch report email, and another from Google Play Support

It continues again

At least this one was not a DNA problem, it’s now a Unauthorized Use of Copyrighted Content policy rejection.

I wasn’t totally surprised about this one, as I mentioned above, some people had already hit issues similar to this. I reviewed what others had done to solve that issue and did some investigation on my own. To solve this: I added a clickwrap dialog box laying out the terms that the user had to agree to or they wouldn’t be able to use the app. While my app does not allow the user to upload or save any copyrighted material, my app’s service provider does allow this on their website under their own terms of service (TOS) and privacy policy (PP). In order to be sure everything was covered, I made the user agree to not only my TOS & PP, but the service provider’s TOS and PP. While this is likely to turn some people away, unfortunately, this was the only way to get through this particular issue.

This took about 3 weeks to get through and I pushed it up to the Google Play store once again.

Opening Outlook the next morning: I received the pre-launch report email, and another from Google Play Support.

Another rejection for the Device and Network Abuse policy.

Surfacing

Interestingly enough, they had attached an actual picture of the problem – the trailer’s attached to the movie were not posted by the content creator (i.e. either the movie maker or the movie distributor). Finally an actual explanation of what was wrong, although several weeks too late. Better late than never, I suppose.

Hey – I had already fixed this! As a matter of fact, this was absolutely not possible, given my prior changes.

I submitted an appeal, with pictures, showing that this couldn’t happen to the current version of the app. I got the standard 72 hour boiler plate email; however I also received an extra “we are still looking it” email in the on the last 24 hour appeal period. I later received a email with the app rejection; however it was interesting as it had a bolded phrase: resubmit the app if you think it is compliant.

I did exactly that and it wasn’t rejected; evidently they can’t admit fault.

I also asked my service provider to either add a tag to the JSON specifying the trailers up into ones created by the movie maker or the movie distributor vs. the rest or to just only use the the movie maker or the movie distributor video trailers. As of this date, it’s still not done.

Totals

This process had a total of 8 rejections that took about 2 months to get through.

Lessons learned

Looking back on this experience, the name change switch was my largest mistake, by not checking for the website. If I had done that, I don’t think I’d have had this 2 month approval problem – at that time. I would have likely hit some of these, but I would have been further along in the process.

Preventive Actions

  1. Research your name and be sure that it will be OK – don’t let your app name be your start down a path like this.
  2. When you get a rejection and it’s not clear to you on why it’s bee rejected; on the app appeal screen choose the ‘not enough information’ option. I’ve seen posts by other developers that hit that first option ‘app does not violate policy’ that they’ve been told by Google that it does violate the policy and don’t bother again until you fix it. Given that they can be opaque, asking what is wrong rather than telling them it’s not wrong is the better way to go. You don’t want to be shut down by them, as it’s one of only 2 games in town.
  3. Always add a build or version number into your app. If Google is reviewing the wrong version of your app, this will allow you to prove that they are looking at the wrong one. It’s also helpful for user bug reports as well.

 

The appeal screenshot – hopefully you won’t see it:

google-appeal

IntentService to JobScheduler

Before reading this, please take a look at my previous post about the EventBus, which I use as a cornerstone for the conversion. If you use something else, good luck.

 

The reason for the conversion to a JobScheduler is, of course, that with the introduction to Android API 26 (Oreo), background services are no longer guaranteed to work when your app isn‚Äôt in the foreground ‚Äď see https://developer.android.com/about/versions/oreo/background.html

 

What I did was create a source Event that had items needed to start the JobScheduler as well as  methods to:

  • Create the PersistableBundle
  • Validate the minimum items needed for the Event to start
  • Re-create the Event given a PersistableBundle

Note: with PersistableBundle in API 26, there is a putBoolean and getBoolean; however, it does not exist in the API’s below that (i.e. to API 21 where it was introduced). I used API 25, so I used an int to represent the boolean true / false values.

Given this example, I think you should be able to convert your IntentService (or Service) to JobScheduler. If you set the send reply flag to true, it should either return a completed or a failed event back to your EventBus receiver in either a Fragment or Activity that you want to process the return message (if sent).

I hope that this helps you in your own conversion.

Good luck!

public class StartEvent {
   private boolean sendReply;
   private int id;

   public static int SEND_REPLY_FALSE = 0;
   public static int SEND_REPLY_TRUE = 1;

   public PersistableBundle createBundle() {
      PersistableBundle bundle = new PersistableBundle();

      if (id != 0) {
         bundle.putInt(JOB_INPUT_ID, id);
      }
      if (isSendReply()) {
          bundle.putInt(JOB_INPUT_SEND_REPLY, SEND_REPLY_TRUE);
      } else {
          bundle.putInt(JOB_INPUT_SEND_REPLY, SEND_REPLY_FALSE);
      }
      return bundle
   }

   public static boolean validateBundle(PersistableBundle bundle) {
      boolean valid = true;

      if (bundle.containsKey(JOB_INPUT_ID)) {
         int myId = bundle.getInt(JOB_INPUT_ID);
         if (myId < 1) {
             valid = false;
         }
      } else {
         valid = false;
      }

      return valid;
   }

   public void setFromBundle(PersistableBundle bundle) {

      if (bundle.containsKey(JOB_INPUT_ID)) {
         int myId = bundle.getInt(JOB_INPUT_ID);
         setId(myId);
      }
    
      setSendReply(false);
      if (bundle.containsKey(JOB_INPUT_SEND_REPLY)) {
         int sendReplyInt = bundle.getInt(JOB_INPUT_SEND_REPLY);
         if (sendReplyInt == SEND_REPLY_TRUE) {
            setSendReply(true);
         }
      }
   }

   -- Getters / Setters
}

Success / Failure events

public class StartFailedEvent {
    private int jobId;
    private String message;
    // These will be used if we can't start the JobScheduler
    private StartEvent errorEvent;

    // These will be used if the job is returning as failed
    private String userId;

    public StartFailedEvent() {
        // Initialize the native field types
        jobId = 0;
    }
    -- Getters / Setters
}

public class StartCompletedEvent {
    private int jobId;
    private String message;

    // These are returning objects from the originating event
    private String userId;

    public StartCompletedEvent() {
        // Initialize the native field types
        jobId = 0;
    }
    -- Getters / Setters
}

In my Application class, I’ve got the EventReceiver

public class MyApplication extends Application {
...
   @Override
   public void onCreate() {
      super.onCreate();
      ...
      EventBus.getDefault().register(this);
      ...
   }
   ...
   static int mJobId = 101;

   synchronized int getJobId() {
       return mJobId;
   }

   @Subscribe
   public void onEvent(StartEvent event) throws ClassNotFoundException {
    if (event != null) {
        PersistableBundle extras = event.createBundle();
        if (!StartEvent.validateBundle(extras)) {
            Log.e(TAG, "app:onEvent - bundle validate failed on fetch session event");
            String errorMessage = "Failed to validate bundle";
            // Send the event that we've failed to schedule the job
            if (EventBus.getDefault().hasSubscriberForEvent(StartFailedEvent.class)) {
                StartFailedEvent returnEvent = new StartFailedEvent()
                returnEvent.setEvent(event);
                returnEvent.setMessage(errorMessage);
                EventBus.getDefault().postSticky(returnEvent);
            } else {
                Log.e(TAG, "No subscriber for StartFailedEvent");
            }
        } else {
            ComponentName serviceComponent = new ComponentName(this, JobSchedulerSession.class);
            JobInfo.Builder builder = new JobInfo.Builder(getJobId(), serviceComponent);
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
            builder.setExtras(extras);

            if (mJobScheduler.schedule(builder.build()) != RESULT_SUCCESS) {
                Log.e(TAG, "app:onEvent - schedule failed: fetch session event");
                String errorMessage = "Failed to schedule fetch session job";
                // Send the event that we've failed to schedule the job
                if (EventBus.getDefault().hasSubscriberForEvent(TmdbSessionFailedEvent.class)) {
                    StartFailedEvent returnEvent = new StartFailedEvent()
                    returnEvent.setEvent(event);
                    returnEvent.setMessage(errorMessage);
                    EventBus.getDefault().postSticky(returnEvent);
                } else {
                    Log.e(TAG, "No subscriber for StartFailedEvent");
                }
            }
        }
    } else {
        Log.e(TAG, "app:onEvent - event is null");
    }
}

 

Note: Be sure that you define your JobScheduler class in your AndroidManifest.xml:

<service
    android:name=".JobSchedulerSession"
    android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE" />

 

Also note: the JobScheduler runs on the main (or UI) thread – you must quickly move the processing to another thread. I do so with an AsyncTask (see below).

 

public class JobSchedulerSession extends JobService {
    private final static String TAG = JobSchedulerSession.class.getSimpleName();
    public static final String JOB_INPUT_ID = "job_input_id";
    public static final String JOB_INPUT_SEND_REPLY = "job_input_send_reply"

    AsyncTaskSession mAsyncTask = null;
    boolean mSendReply = false;
    int mId;

    @Override
    public boolean onStartJob(final JobParameters params) {
        Log.d(TAG, "onStartJob:work service to be called here");
        final String missingBundle = "Failed to get information to fetch";
        final String badBundle = "Bad information to fetch";

        PersistableBundle bundle = params.getExtras();
        if (bundle == null) {
            // send result back to say we didn't get anything
            StartFailedEvent result = new StartFailedEvent();
            result.setMessage(missingBundle);
            EventBus.getDefault().postSticky(result);
            Log.d(TAG, "onStartJob:JobScheduler bundle create returned null. jobId: " + params.getJobId());
            return false;
        }

        if (!FetchMovieListEvent.validateBundle(bundle)) {
            // send result back to say we got a bad bundle
            StartFailedEvent result = new StartFailedEvent();
            result.setMessage(badBundle);
            EventBus.getDefault().postSticky(result);
            Log.d(TAG, "onStartJob:JobScheduler bundle not valid. jobId: " + params.getJobId());
            return false;
        }

        setInfoFromJobBundle(bundle);

        mAsyncTask = new AsyncTaskSession(this) {
            @Override
            protected void onPostExecute(Boolean success) {
                Log.d(TAG, "onStartJob:JobScheduler-onPostExecute");
                // Note: w/o this super, the callback to AsyncTask onPostExecute doesn't work
                super.onPostExecute(success);

                // jobFinished is to re-schedule job, if needed.
                // NOTE: Re-scheduling should be handled by the event receiver,
                // so always send false.
                jobFinished(params, false);
            }
        };

        mAsyncTask.setJobId(params.getJobId());
        mAsyncTask.setSendReply(mSendReply);
        mAsyncTask.setId(mId);
        mAsyncTask.execute();
        Log.d(TAG, "onStartJob:JobScheduler started jobId: " + params.getJobId());
        return true;
     }

     void setInfoFromJobBundle(PersistableBundle bundle) {
        StartEvent event = new StartEvent();
        event.setFromBundle(bundle);
        if (event.isSendReply()) {
            mSendReply = true;
        }
        mId = event.getId();
     }
}

Finally, the AsyncTask – this will run safely on the non-UI thread.

public class AsyncTaskSession extends AsyncTask<Void, Void, Boolean> {
    private final static String TAG = AsyncTaskSession.class.getSimpleName();
    private boolean mSendReply = false;
    private int mId;
    private int mJobId = 0;
    private Context mContext;
    private boolean mCancelled = false;

    private String mMessage;
    public AsyncTaskSession(Context context) {
        mContext = context.getApplicationContext();
    }

    @Override
    protected void onCancelled(Boolean aBoolean) {
        super.onCancelled(aBoolean);
        if (aBoolean != null) {
            mCancelled = aBoolean;
        }
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();
    }

    /***
     * This is where the main process is performed.
     * @param params
     * @return Boolean true - success, false - error
     */
    @Override
    protected Boolean doInBackground(Void... params) {
        Log.e(TAG, "AsyncTaskSession started");

        if (!validArgs()) {
            // Note: mMessage set in validArgs()
            mSendReply = true;
            return false;
        }

        -- Put your main processing here. Please note that you 
        -- should (if it a long task) check the cancelled flag::
        -- mCancelled

        -- you need to return false (if things went bad)
        -- You need to return true (if it went OK)
    }

    /* this will be called back from the JobScheduler */
    @Override
    protected void onPostExecute(Boolean success) {
        Log.e(TAG, "AsyncTaskSession onPostExecute");
        // Be sure we have a message (in case things went castor's up
        if (mMessage == null) {
            if (success == null) {
                mMessage = defaultMessage(false);
            } else {
                mMessage = defaultMessage(success);
            }
        }

        // Perform evaluation of what happened
        if (success == null) {
            super.onPostExecute(success);
        } else if (success) {
            Toast.makeText(mContext, "Event worked", Toast.LENGTH_SHORT).show();
            if (mSendReply) {
                StartCompletedEvent returnEvent = StartCompletedEvent();
                returnEvent.setMessage(mMessage);
                EventBus.getDefault().postSticky(returnEvent);            }
        } else {
            Toast.makeText(mContext, "Event failed", Toast.LENGTH_SHORT).show();
            if (mSendReply) {
                StartFailedEvent returnEvent = new StartFailedEvent()
                returnEvent.setEvent(event);
                returnEvent.setMessage(errorMessage);
                EventBus.getDefault().postSticky(returnEvent);
            }
        }
    }

    boolean validArgs() {
        boolean valid = true;
        if (mId < 1) {
            valid = false;
            mMessage = "Id is less than one";
        }
       return valid;
    }

    String defaultMessage(boolean workedOK) {
        String message = "";
        if (workedOK) {
            message += "completed";
        } else {
            message += "failed";
        }

        return message;
    }
}

 

 

Helpful way to move to using JobScheduler – EventBus

With the introduction to Android API 26 (Oreo), background services are no longer guaranteed to work when your app isn’t in the foreground – see¬†https://developer.android.com/about/versions/oreo/background.html

This meant I had to start moving my app’s from IntentService to using a JobScheduler, Looking into this, I found this article by Google developer advocate going about the use of the JobScheduler. She pointed to an app, Muzei Live Wallpaper,¬† as how to do this. I looked it over and found that it used Greenrobot’s EventBus. EventBus is used to send events to around your app with senders and listeners.

I thought this is a great way to decouple logic to make things simpler, which is always better. While this will help me moving to the JobScheduler, I can also think of other uses where this would also make things simpler instead of levels of interfaces to pass data around.

When I first looked at Greenrobot’s getting started page, it didn’t really make much sense to me. The most helpful items I found were this article and this video, which had more explanation on it.

Give EventBus a shot, I think it will help you out.

MVP a little later

Since having the problems with generating a simple MVP solution back in June I went back to working on my main app.

After thinking about this, I don’t have a simple MVP example to do. I did however have an middling game idea. As soon as I finish up dealing with changing my IntentServices for Android O (and likely other things), I’ll come back to this one.

Update Nov-11-2017

While I’m not completely done with my IntentService to JobScheduler, I thought I could update this one. I think a Tic-Tac-Toe game or similar should be fine for this. Hopefully get to it around the end of the year or in the new year.