1

I had a simple application, with an activity and a service.

The activity have a toggle button to start and stop the service. The service have a notification to show when it is up, and when it is down. The notification text is set by the activity, stored in SharedPreferences, and read inside service onCreate().

The toggle button save a boolean serviceStarted when starting/stopping the service, so when the activity start, I can restart the service if it was started previously.

My bug is: sometimes, the serviceStarted is not saved properly. I can see this by looking at the sharedPreferences file. It works most of the time, but from times to times, it just didn't work. The SharedPreferences instance have correct values, but the file is not correct. And event less frequently, but it happens, the preferences xml file totally disappears.

My preference file only have two values: the serviceStarted boolean, and a short string with the notification text.

The toggle's click listener is:

public void onClick(View v)
{
    ToggleButton toggle = (ToggleButton) v;
    boolean mustStartService = toggle.isChecked();
    if(mustStartService)
    {
        Intent serviceIntent = new Intent(MainActivity.this, MainService.class);
        startService(serviceIntent);
    }
    else
    {
        Intent serviceIntent = new Intent(MainActivity.this, MainService.class);
        stopService(serviceIntent);
    }

    // Save to SharedPreferences
    SharedPreferences settings = getSharedPreferences("prefs", Context.MODE_MULTI_PROCESS);
    SharedPreferences.Editor editor = settings.edit();
    editor.putBoolean("serviceStarted", mustStartService);
    editor.commit();
}

Notes:

  • The service is put in another process, so the system can relese the resources from the activity once it is gone, thus keeping the service/notification up with less resources. Because of that, I use the flag MODE_MULTI_PROCESS.

  • The commit() call returns true, even if the write does not seem to be correct, or when the file is deleted. I do not have any error from SharedPreferencesImpl in the logcat.

  • The only other place where the preferences are written is from listener in the activity (when a TextView is changed), but I never called this code when the bug occurs, so there should not be a write conflict. The service never writes preferences.

The two objects are defined like this in the manifest:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<service
    android:name=".MainService"
    android:process=":mainService"
    android:exported="false">
</service>

Any help will be apreciated. I know I can use another way to store preferences, as suggested here, but I would like to know if I am not missing something.

Community
  • 1
  • 1
biskitt
  • 1,381
  • 1
  • 10
  • 13
  • Firstly, try if this helps http://stackoverflow.com/questions/10186215/sharedpreferences-value-is-not-updated – AndyFaizan Feb 27 '14 at 16:49
  • No, it changes nothing – biskitt Feb 27 '14 at 17:34
  • If you write a pref _with a null key_ the preferences will be cleared on reload currently. Have a look at that – Mr_and_Mrs_D Feb 28 '14 at 11:52
  • No, it always have a valid key, and a valid value. The SharedPreference object is up to date and have correct values, it is the stored file that is not up to date. So after an app restart, all changes are lost – biskitt Feb 28 '14 at 12:30

1 Answers1

2

I found the bug by looking at the SharedPreferencesImpl code from Android sources. I will try to explain it, because it might be useful t other people.

I should have taken more attention at this note, from SharedPreferences doc

Note: currently this class does not support use across multiple processes. This will be added later.

I thought that the doc was not up to date, and that using the MODE_MULTI_PROCESS flag will be enought. I was wrong. While it is needed to make SharedPreferences work on multiple processes of the same application, it has nothing to do with a real "multiple process support", handling all cases.

Some explanations on SharedPreferences:

  • Basically, SharedPreferences is stored in an xml file on your device. When first loading it in a process, Android keeps a your SharedPreferences in memory. So each time you call getSharedPreferences() with the same name, it will return the same object, and avoid reading/parsing the file multiple times.

  • If you use the MODE_MULTI_PROCESS flag (or in version <= Android 2.3), it will check if the xml file has not been modified since last access. Thus, the file will be re-read if another process modified it.

  • The system use a backup file of your SharedPreferences xml when saving or loading preferences. It is here that all my ugly things happened.

  • When you save a modification in your SharedPreferences, either with commit() or apply(), the system first makes a backup of your previous file, removes it, saves the new file, and then removes the backup (assuming there is no error). In pseudo code, simplified from , it gives:

  • When you read the preferences from the file, it first replace the "normal file" by the backup if there is a backup, then reads the file.

The Save() pseudo code, simplified from SharedPreferencesImpl.java is:

void SavePreferenceFile()
{
    // Rename the current file so it may be used as a backup during the next read
    if(mFile.exists())
    {
        if(!mBackupFile.exists())
        {
            // file without backup (as usual first time): current file will be a backup
            mFile.renameTo(mBackupFile);
        }
        else
        {
            // file AND backup: delete the file and keep only the backup
            mFile.delete();
        }
    }

    // Attempt to write the file, delete the backup and return true as atomically as
    // possible.  If any exception occurs, delete the new file; next time we will restore
    // from the backup.
    mFile.WriteData();
    if(success)
    {
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
    }
    else
    {
        // error
        mFile.delete();
    }
}

And now the Load() function:

void LoadPreferenceFile()
{
    if(mBackupFile.exists())
    {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }
    mFile.ReadData();
}

On a "normal" usage, the load happens when the previous write is over, so there is no issue. What happened in my case, is that I have:

Process 1: Start service (Process 2)
Process 1: SavePreferenceFile() begin
Process 2 startup: LoadPreferenceFile()
Process 1: SavePreferenceFile() end

If the LoadPreferenceFile() is just after the real write on the disk (mFile.WriteData()), but before we delete the backup, then the LoadPreferenceFile() from Process 2 will replace our new file by the old backup.

My solution, for my specific issue: using editor.commit() before starting the service. By doing that, I can assure that the write is over before the read occurs. When I want to change preference and tell my other process to re-load preferences, I do the same: commit() my changes, then sending a signal to the service to load the new file.

Hope it may help people.

biskitt
  • 1,381
  • 1
  • 10
  • 13
  • I would put all the accesses to the preferences in a ContentProvider to make sure it is always accessed in the same process. – njzk2 Mar 05 '14 at 16:29