18

I have some code that downloads a "Current" object's JSON. But this same code needs to be called by an IntentService whenever an alarm goes off (when the app is not running any UI), and also by an AsyncTask while the app is running.

However, I got an error saying Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created. However, I do not understand how or why this stack trace got on a different thread.

I was able to get rid of the error by copying all the shared code and sticking it directly into DownloadDealService's onHandleIntent method, but it is very sloppy and I'm looking for a better solution that doesn't require duplicating code.

How can I get rid of this error, without duplicating code? Thanks.

public class DownloadDealService extends IntentService
{
    ...
    @Override
    protected void onHandleIntent(Intent intent)
    {
        Current todaysCurrent = Utils.downloadTodaysCurrent(); //<--- included for background info
        String dateString = Utils.getMehHeadquartersDate(); //(omitted)
        Utils.onDownloadCurrentCompleteWithAlarm(todaysCurrent, dateString); //<------ calling this...
    }
}

public class Utils
{
    // ...other methods ommitted...

    //This method is not in the stack trace, but I included it for background information.
    public static Current downloadTodaysCurrent()
    {
        //Set up Gson object... (omitted)
        //Set up RestAdapter object... (omitted)
        //Set up MehService class... (omitted)

        //Download "Current" object from the internet.
        Current current = mehService.current(MehService.API_KEY);
        return current;
    }

    //Included for background info- this method is not in the stack trace.
    public static void onDownloadCurrentComplete(Current result, String dateString)
    {
        if(result.getVideo() == null)
        {
            Log.e("HomePage", "Current was not added on TaskComplete");
            return;
        }
        remainder(result, dateString);
    }

    public static void onDownloadCurrentCompleteWithAlarm(Current result, String dateString)
    {
        //Set alarm if download failed and exit this function... (omitted)

        remainder(result, dateString);//<------ calling this...
        Utils.sendMehNewDealNotification(App.getContext());
    }

    public static void remainder(Current result, String dateString)
    {
        Realm realm = RealmDatabase.getInstance();

        //Add "Current" to Realm
        Current current = Utils.addCurrentToRealm(result, realm); //<------ calling this...
    }

    public static Current addCurrentToRealm(Current current, Realm realm)
    {
        realm.beginTransaction(); //<---- Error is here
        Current result = realm.copyToRealmOrUpdate(current);
        realm.commitTransaction();
        return result;
    }
}

Stack trace:

E/AndroidRuntime: FATAL EXCEPTION: IntentService[DownloadDealService]
Process: com.example.lexi.meh, PID: 13738
java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
    at io.realm.Realm.checkIfValid(Realm.java:191)
    at io.realm.Realm.beginTransaction(Realm.java:1449)
    at com.example.lexi.meh.Utils.Utils.addCurrentToRealm(Utils.java:324)
    at com.example.lexi.meh.Utils.Utils.remainder(Utils.java:644)
    at com.example.lexi.meh.Utils.Utils.onDownloadCurrentCompleteWithAlarm(Utils.java:635)
    at com.example.lexi.meh.Home.DownloadDealService.onHandleIntent(DownloadDealService.java:42)
    at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:136)
    at android.os.HandlerThread.run(HandlerThread.java:61)

I have an AsyncTask that calls some of those Utils methods also:

public class DownloadAsyncTask extends AsyncTask<Void, Integer, Current>
{
    // ... (more methods ommitted)...

    protected Current doInBackground(Void... voids)
    {
        return Utils.downloadTodaysCurrent(); //<---- shared Utils method
    }
}

//Async class's callback in main activity:
public class HomePage extends AppCompatActivity implements DownloadAsyncTaskCallback, DownloadAsyncTaskGistCallback<Current, String>
{
    // ... (more methods ommitted)...

    public void onTaskComplete(Current result, String dateString)
    {
        Utils.onDownloadCurrentComplete(result, dateString);
    }
}
Rock Lee
  • 9,146
  • 10
  • 55
  • 88

3 Answers3

21

[UPDATED] based on the additional info

RealmDatabase.getInstance() was returning the Realm instance created on the main thread. And this instance was used on the IntentService's thread. Which lead to the crash.

Realm instances can't be used on any other thread except the one on which they were created.


You can't pass Realm objects between the threads. What you can do is to pass a unique identifier of the object (i.e. @PrimaryKey), and then fetch the object by its' id on another thread. Like this: realm.where(YourRealmModel.class).equalTo("primaryKeyVariable", id).findFirst().

For more details check out Realm's official documentation and example:

mikeDOTexe
  • 487
  • 5
  • 14
  • Can you look at my code and tell me where I passed anything between threads? I don't understand. I honestly thought that I was on the same thread. As far as I know, I only shared methods between threads, not pass any realm objects between threads. – Rock Lee Nov 24 '15 at 03:10
  • It's pretty hard to understand what goes where from your code example. But I assume that you obtain the result object in doInBackround of the AsyncTask (which is executed on the worker thread) and then pass it as a result to onPostExecute() (which is executed on the main thread). And then trying to access it from there on remainder() method, which blows up your app. – Roberto Artiles Astelarra Nov 24 '15 at 03:19
  • You can look in the stack trace I provided to see the path of execution. The IntentService `DownloadDealService` calls some static methods in my `Util.java` class. – Rock Lee Nov 24 '15 at 03:23
  • In the past, my AsyncMethod has actually always worked, even when passing a Realm object to the UI thread via the `onPostExecute()` callback, and from there called static methods from the Util.java class. – Rock Lee Nov 24 '15 at 03:25
  • Can you post RealmDatabase.getInstance() method? – Roberto Artiles Astelarra Nov 24 '15 at 03:28
  • 2
    My suspicion is that you are doing some custom caching of the Realm instance in your RealmDatabase class. Which leads you to calling it from the different thread than the original one, on which you obtained it. Just use Realm.getDefaultInstance(). – Roberto Artiles Astelarra Nov 24 '15 at 03:46
  • It's just a static Realm instance that I instantiate with the result of `Realm.getInstance(this)` in the `onCreate` of every Activity. I was trying to mimic a singleton, which I now realize is unnecessary. – Rock Lee Nov 24 '15 at 03:51
  • 1
    Here you go. Realm instance can be used only on the thread it was created. In your case it was created on main thread, but used in IntentService thread. – Roberto Artiles Astelarra Nov 24 '15 at 03:55
  • 2
    I was able to solve the issue by using `Realm.getInstance(this)` in my `IntentService`, and pass that to the Util methods to use (since those methods have no way to access the thread context). So now, the Util methods can be used by multiple threads, with a correct Realm instance for each thread. – Rock Lee Nov 24 '15 at 04:13
1

If you use the Realm in IntentService ,you will use new Realm .for example

Realm.getInstance(RealmConfiguration config) 

I used the Realm in IntentService

@Override
protected void onHandleIntent(Intent intent) {
    Realm realm = Realm.getDefaultInstance();
    ...
    realm.close(); // important on background thread
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
ayac3j
  • 81
  • 6
0

The problem is you are calling methods from differents threads, you have to use the same thread, create your own HandlerThread.

josedlujan
  • 5,357
  • 2
  • 27
  • 49