Domanda

I'm working on an app that consist of many component.
The app is using AlarmManager to do some polling from a server. There are also regular Activities that present data (that stored on Sqlite and SharedPreferences)

Everything worked great until I tried to add a feature that start the polling when the device finish boot (BOOT_COMPLETED) when I did it i discovered that I cant access the SharedPreferences with the Context I get from the onReceive(Context context, Intent intent) method of the Class that extends BroadcastReceiver.

Another thing is that I'm using Singleton to handle all the SharedPreferences and DB functionality. this Singleton holds the Context of the first lunched activity of the app (LoginActivity). and use it all over the app and the Polling BroadcastReciver.

So I understand (believe...) that when the device finishes the Boot I get different Context (Not the LoginActivity context I used to get) and this is the source of the problem (is it???)

After all this preamble what I really need is a BEST-PRACTICE approach for a task like that - How to store and get data on SharedPreferences and DB all over the app:

  1. when the user run it
  2. when it do backgroung tasks via AlarmManager
  3. when it autostart via BOOT_COMPLETED Broadcast

without suffering this Context issues. An Example will be awesome.

EDIT: Here is my code snippets:

ConnectionManager.java - this class holds REST requests implementations and stores stuff to the SharedPreferences:

public class ConnectionManager {

    //There are many more variables here - irrelevant for the example

    private CookieStore _cookieStore;
    private static ConnectionManager _instance;
    private SharedPreferences _sharedPref;
    private Context _context;
    private DataPollingBroadcastReceiver _dataPoller;

    private ConnectionManager(Context caller) {
        _context = caller;
        _sharedPref = PreferenceManager.getDefaultSharedPreferences(_context);
    }

    public static ConnectionManager getInstance(Context caller) {
        if (_instance == null) {
            _instance = new ConnectionManager(caller);
        }
        return _instance;
    }

public void setPollingActive(boolean active) {
    if (active) {
        SharedPreferences.Editor editor = _sharedPref.edit();
        editor.putString("myapp.polling", "true");
        editor.commit();
        startRepeatingTimer();
    } else {
        SharedPreferences.Editor editor = _sharedPref.edit();
        editor.putString("myapp.polling", "false");
        editor.commit();
        cancelRepeatingTimer();
    }
}

private void startRepeatingTimer() {
    if (_dataPoller!= null) {
        _dataPoller.SetAlarm(_context);
    } else {
        Toast.makeText(_context, "_dataPoller object is null",
                Toast.LENGTH_SHORT).show();
    }
}

private void cancelRepeatingTimer() {
    if (_dataPoller!= null) {
        _dataPoller.CancelAlarm(_context);
    } else {
        Toast.makeText(_context, "_dataPoller object is null",
                Toast.LENGTH_SHORT).show();
    }
}

    //There are many more methods here - irrelevant for the example

} 

MainBootListener.java: this class suppose to activate the polling mechanism - it's not working since there are exception on the ConnectionManager.

public class MainBootListener extends BroadcastReceiver{

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Activated by boot event",
                Toast.LENGTH_LONG).show();
        ConnectionManager cm = ConnectionManager.getInstance(context);
        cm.setPollingActive(true);
    }
}

DataPollingBroadcastReceiver.java : this class polling the data from the server

public class DataPollingBroadcastReceiver extends BroadcastReceiver {

    private ConnectionManager _mngr;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (_mngr == null) {
            _mngr = ConnectionManager.getInstance(context);
        }
        PowerManager pm = (PowerManager) context
            .getSystemService(Context.POWER_SERVICE);
        PowerManager.WakeLock wl = pm.newWakeLock(
            PowerManager.PARTIAL_WAKE_LOCK, TAG);
        // Acquire the lock
        wl.acquire();
        // You can do the processing here update the widget/remote views.
        Bundle extras = intent.getExtras();
        StringBuilder msgStr = new StringBuilder();
        Format formatter = new SimpleDateFormat("hh:mm:ss");
        msgStr.append(formatter.format(new Date()));
        // /////
        _mngr.updateDataFromServer();
        msgStr.append(" [Updated AccessControlTable]");
        Log.i(TAG, msgStr.toString());
        Toast.makeText(context, msgStr, Toast.LENGTH_SHORT).show();
        // ////
        // Release the lock
        wl.release();
    }

    public void SetAlarm(Context context) {
        if (_mngr == null) {
            _mngr = ConnectionManager.getInstance(context);
        }
        AlarmManager am = (AlarmManager) context
            .getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context, DataPollingBroadcastReceiver.class);
        intent.putExtra(ONE_TIME, Boolean.TRUE);
        PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
        am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
            1000 * _mngr.getPollingIntervalInSeconds(), pi);
    }

    public void CancelAlarm(Context context) {
        if (_mngr == null) {
            _mngr = ConnectionManager.getInstance(context);
        }
        Intent intent = new Intent(context, DataPollingBroadcastReceiver.class);
        PendingIntent sender = PendingIntent
            .getBroadcast(context, 0, intent, 0);
        AlarmManager alarmManager = (AlarmManager) context
            .getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(sender);
    }
}

of course there are more classes - I trier to bring the minimum requiered.

EDIT The exceptions I recived where 2: if the polling mechanism was active from the application (the singletone holds the LoginActivity as Context) and I closed the application from task manager, the polling stopped and showed this exception:

12-29 14:02:03.061: E/AndroidRuntime(9402): FATAL EXCEPTION: main
12-29 14:02:03.061: E/AndroidRuntime(9402): java.lang.RuntimeException: Unable to start receiver my.app.DataPollingBroadcastReceiver : java.lang.NullPointerException
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.app.ActivityThread.handleReceiver(ActivityThread.java:2277)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.app.ActivityThread.access$1500(ActivityThread.java:140)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.os.Handler.dispatchMessage(Handler.java:99)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.os.Looper.loop(Looper.java:137)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.app.ActivityThread.main(ActivityThread.java:4898)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at java.lang.reflect.Method.invokeNative(Native Method)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at java.lang.reflect.Method.invoke(Method.java:511)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1006)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:773)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at dalvik.system.NativeStart.main(Native Method)
12-29 14:02:03.061: E/AndroidRuntime(9402): Caused by: java.lang.NullPointerException
12-29 14:02:03.061: E/AndroidRuntime(9402):     at my.app.ConnectionManager.<init>(ConnectionManager.java:172)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at my.app.ConnectionManager.getInstance(ConnectionManager.java:196)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at my.app.DataPollingBroadcastReceiver .onReceive(DataPollingBroadcastReceiver .java:27)
12-29 14:02:03.061: E/AndroidRuntime(9402):     at android.app.ActivityThread.handleReceiver(ActivityThread.java:2270)
12-29 14:02:03.061: E/AndroidRuntime(9402):     ... 10 more

the secont exception acured when the app wasn't running and I sent BOOT_COMPLETED from adb, than the singletone tried to be init. with the BroadcastReciver Context. this was the exception:

12-26 11:54:58.556: E/AndroidRuntime(12373): FATAL EXCEPTION: main
12-26 11:54:58.556: E/AndroidRuntime(12373): java.lang.RuntimeException: Unable to start receiver my.app.MainBootListener : java.lang.NullPointerException
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.app.ActivityThread.handleReceiver(ActivityThread.java:2277)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.app.ActivityThread.access$1500(ActivityThread.java:140)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.os.Handler.dispatchMessage(Handler.java:99)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.os.Looper.loop(Looper.java:137)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.app.ActivityThread.main(ActivityThread.java:4898)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at java.lang.reflect.Method.invokeNative(Native Method)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at java.lang.reflect.Method.invoke(Method.java:511)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1006)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:773)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at dalvik.system.NativeStart.main(Native Method)
12-26 11:54:58.556: E/AndroidRuntime(12373): Caused by: java.lang.NullPointerException
12-26 11:54:58.556: E/AndroidRuntime(12373):    at my.app.ConnectionManager.<init>(ConnectionManager.java:172)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at my.app.ConnectionManager.getInstance(ConnectionManager.java:196)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at my.app.MainBootListener .onReceive(MainBootListener .java:14)
12-26 11:54:58.556: E/AndroidRuntime(12373):    at android.app.ActivityThread.handleReceiver(ActivityThread.java:2270)
12-26 11:54:58.556: E/AndroidRuntime(12373):    ... 10 more
È stato utile?

Soluzione

Your problem should be related to the fact that the context you receive in the BR is a ReceiverRestrictedContext - and I bet you get the exception ReceiverCallNotAllowedException. You always must post an exception if you have one - so please post it so we can understand what goes on exactly! That being said - you are doing too much in your receiver !

And, please, please, please, simplify your code. Example :

public void setPollingActive(boolean active) {
    _sharedPref.edit().putBoolean("myapp.polling", active).commit();
    (active) ? startRepeatingTimer() : cancelRepeatingTimer();
}

Also you do not need the wakelock in the receiver if you are woken up by the alarm manager ! The alarm manager holds a wakelock ! If you do a lot of stuff in your receiver you do need a WakefulIntentService though there.
Finally if you want a singleton do it right and use an enum. Your implementation is wrong - it is not thread safe to begin with.

EDIT: as per the exception posted the problem was not context related - it was a NPE due to a static field becoming null at some point

Altri suggerimenti

First you need to know that the "Context" is actually the application context, and that's used among all the different building blocks that compound that application. If you cannot access the shared preferences with that Context is because maybe you are making use of "Activity Preference", this is a narrow scoped preference which usually is used as follows:

SharedPreferences preferences = getPreferences(MODE_PRIVATE);
int storedPreference = preferences.getInt("storedInt", 0);

In order to share preferences along the whole application context, you need to use preferences like this:

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

In this case, as long as you have any Context from any component in your application, you will be able to access to the list of key/value pair elements, in order to read/write in this shared preferences all you have to do is this:

//Write
Editor editor = preferences.edit();
editor.putString("yourkey", "value");
editor.commit();//Do not forget to commit..

//Read
String value = preferences.getString(key, defValue);

Now, going back to the point of which is the best way to share data between application components, it all depends, there's different mechanisms, SharedPreference might be OK for simple objects maybe primitives, however for Large ammount of data, DB is definitely the way to go, and there's also Application Cached elements, you can always create a class that extends from Application, declare it in your manifest and cache objects there, notice that this class lives as long as you application lives, follows the singleton design pattern and you can rely on that object to be instantiated as soon as your application is created and be destroyed when the app is killed, this could be considered one of the largest scopes available in memory, of course it doesnt persist after rebooting the device, so for this specific scenario a DB might be better. If you want to know more about other ways to do it see this post

Hope this Helps.

Regards!

Keeping an activity context in singleton is a bad idea for many reasons, generally it could lead you to leak activities.

To have an application context in your singleton is not a bad idea at all, however those who wrote Android framework may disagree with me as they suggest to make each activity to be a mini application. But let's say you share my opinion.

You got two ways to do this:

In your singleton change:

 _context = caller;

to

 _context = caller.getApplicationContext();

Or you can use the static method

Context.getApplicationContext()

And there is a third method, that allows you to extend the Application class. Read about it here.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top