Search code examples
androidandroid-servicesharedpreferencesandroid-notifications

SharedPreferences not saving in BroadcastReceiver when main activity is running


I'm making an android library for showing scheduled notifications. For example, throw a notification the next week at 4pm.

I need to have a register of scheduled notifications which has not been yet shown, so i can cancel them if i want. So whenever i schedule a new notification, i save it into SharedPreferences. Then i start a BroadcastReceiver that will be excecuted when the time arrives. At that moment, the receiver unregisters the notification in SharedPreferences.

That works fine when the app is not running. But when the app is running those changes made by the receiver are not getting affected in the running app so i never register that the notification has been shown.

Here is my sample activity code:

public class MainActivity extends AppCompatActivity {
NotificationJSONStorage storage;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    storage = new NotificationJSONStorage(context,
            "notifications");

    // show notification in the next 10 seconds
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.SECOND, 10);

    sendNotification(getApplicationContext(), calendar.getTimeInMillis(), ...);
}

public int sendNotification(Context context, long dateTriggerMilliseconds, ...) {
    // create a unique notification id
    int id = UUID.randomUUID().hashCode();

    // Create an explicit intent for an Activity in your app
    Intent intent = new Intent(context, DelayedNotificationReceiver.class);
    // pass data to the receiver
    intent.putExtra("notification_id", id);
    ...

    // Set the Activity to start in a new, empty task
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
            id, intent, PendingIntent.FLAG_UPDATE_CURRENT);

    AlarmManager alarm = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    alarm.set(AlarmManager.RTC_WAKEUP, dateTriggerMilliseconds, pendingIntent);

    // adds the notification id to shared preferences
    try {
        storage.addScheduledNotification(id);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    return id;
}

}

Here is my BroadcastReceiver code:

public class DelayedNotificationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
    // receive custom data from intent
    int id = intent.getIntExtra("notification_id", -1);
    ...

    try {
        Intent launchIntent = new Intent(context, Class.forName("my.launcher.class"));
        PendingIntent pendingIntent = PendingIntent.getActivity(context, id, launchIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        // build the notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
                ...;

        // show the notification
        NotificationManagerCompat.from(context)
                .notify(id, builder.build());

        // removes the notification id from shared preferences
        NotificationJSONStorage storage = new NotificationJSONStorage(context,
            "notifications");
        storage.removeScheduledNotification(id);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

}

This is a class designed for saving and reading notification data from SharedPreferences:

public class NotificationJSONStorage {
private final String SCHEDULED_NOTIFICATIONS_KEY = "scheduled_notifications";

private Context context;
private JSONObject jsonRoot;
private String preferencesNamespace;

public NotificationJSONStorage(Context context, String preferencesNamespace) {
    this.context = context;
    this.preferencesNamespace = preferencesNamespace;
}

public void addScheduledNotification(int id) throws JSONException {
    JSONObject root = getJsonRoot();
    // do stuff with json root
    ...

    // persist it
    save(SCHEDULED_NOTIFICATIONS_KEY);
}

public boolean removeScheduledNotification(int id) throws JSONException {
    JSONObject root = getJsonRoot();
    // do stuff with json root
    ...

    // persist it
    save(SCHEDULED_NOTIFICATIONS_KEY);

    return result;
}

public JSONObject load(String key) throws JSONException {
    SharedPreferences preferences = context.getSharedPreferences(preferencesNamespace,
            Context.MODE_PRIVATE);
    String raw = preferences.getString(key, null);
    JSONObject root = null;

    if (raw != null && !raw.isEmpty()) {
        root = new JSONObject(raw);
    } else {
        root = new JSONObject();
    }

    return root;
}

public void save(String key) {
    String out = getJsonRoot().toString();
    // write to shared preferences
    SharedPreferences preferences = context.getSharedPreferences(preferencesNamespace,
            Context.MODE_PRIVATE);
    preferences.edit()
            .putString(key, out)
            .apply();
}

public JSONObject getJsonRoot() {
    if (jsonRoot == null) {
        try {
            jsonRoot = load(SCHEDULED_NOTIFICATIONS_KEY);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    return jsonRoot;
}

}

This is the android manifest of the main activity:

<?xml version="1.0" encoding="utf-8"?>

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

This is the android manifest of the notification module:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.l33tbit.androidnotifications">
<uses-permission android:name="android.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.GET_TASKS"/>
<application>
    <receiver android:name=".DelayedNotificationReceiver"/>
</application>

Any ideas?


Solution

  • I finally solved the issue. The problem was that jsonRoot reference in NotificationJSONStorage class was not getting updated when the receiver was changing the state, because it was getting loaded only once.

    What i did is to add a listener to SharedPreferences that forces to reload the json data when any change occurs.

    public class NotificationJSONStorage implements SharedPreferences.OnSharedPreferenceChangeListener {
    private final String SCHEDULED_NOTIFICATIONS_KEY = "scheduled_notifications";
    
    private Context context;
    private JSONObject jsonRoot;
    private String preferencesNamespace;
    private boolean rootDirty = true;
    
    public NotificationJSONStorage(Context context, String preferencesNamespace) {
        this.context = context;
        this.preferencesNamespace = preferencesNamespace;
    
        context.getSharedPreferences(preferencesNamespace, Context.MODE_PRIVATE)
                .registerOnSharedPreferenceChangeListener(this);
    }
    
    public void onDestroy() {
        context.getSharedPreferences(preferencesNamespace, Context.MODE_PRIVATE)
                .unregisterOnSharedPreferenceChangeListener(this);
    }
    
    public void addScheduledNotification(int id) throws JSONException {
        JSONObject root = getJsonRoot();
        // do stuff with json root
        ...
    
        // persist it
        save(SCHEDULED_NOTIFICATIONS_KEY);
    }
    
    public boolean removeScheduledNotification(int id) throws JSONException {
        JSONObject root = getJsonRoot();
        // do stuff with json root
        ...
    
        // persist it
        save(SCHEDULED_NOTIFICATIONS_KEY);
    
        return result;
    }
    
    public JSONObject load(String key) throws JSONException {
        SharedPreferences preferences = context.getSharedPreferences(preferencesNamespace,
                Context.MODE_PRIVATE);
        String raw = preferences.getString(key, null);
        JSONObject root = null;
    
        if (raw != null && !raw.isEmpty()) {
            root = new JSONObject(raw);
        } else {
            root = new JSONObject();
        }
    
        return root;
    }
    
    public void save(String key) {
        String out = getJsonRoot().toString();
        // write to shared preferences
        SharedPreferences preferences = context.getSharedPreferences(preferencesNamespace,
                Context.MODE_PRIVATE);
        preferences.edit()
                .putString(key, out)
                .apply();
    }
    
    public JSONObject getJsonRoot() {
        // also check if the data is dirty so it should be reloaded
        if (jsonRoot == null || rootDirty) {
            try {
                jsonRoot = load(SCHEDULED_NOTIFICATIONS_KEY);
                rootDirty = false;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    
        return jsonRoot;
    }
    
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (key.equals(SCHEDULED_NOTIFICATIONS_KEY)) {
            rootDirty = true;
        }
    }
    

    }