Skip to content Skip to sidebar Skip to footer

Sharedpreferences In Android Not Persisted To Disk When Key Contains Newline

In Android, I'd like to write SharedPreferences key-value pairs where the keys are Base64 strings. // get a SharedPreferences instance SharedPreferences prefs = getSharedPreference

Solution 1:

I took a look at GrepCode and see that the operations will be the following (I do not mention useless ones) :

  1. android.app.SharedPreferencesImpl.commit()
  2. android.app.SharedPreferencesImpl.commitToMemory()
  3. android.app.SharedPreferencesImpl.queueDiskWrite(MemoryCommitResult,Runnable)

    3.1. XmlUtils.writeMapXml(Map, OutputStream)

    3.2. XmlUtils.writeMapXml(Map, String, XmlSerializer)

    3.3. XmlUtils.writeValueXml(Object v, String name, XmlSerializer ser)


First : how your data are converted ?

The method XmlUtils.writeValueXml writes the Object value in a XML tag with the attribute name set to the String value. This String value contains exactly the value you specified at the SharedPreference's name.

(And I confirmed this by doing a step-by-step debug with your piece of code).

The XML will be with an unescaped line break character. Actually, the XmlSerializer instance is a FastXmlSerializer instance and it does not escape the \n character (see the link for this class at the end if you want to read the source code)

Interesting piece of code :

writeValueXml(Object v, String name, XmlSerializer out) {
    // -- "useless" code skippedout.startTag(null, typeStr);
    if (name != null) {
        out.attribute(null, "name", name);
    }
    out.attribute(null, "value", v.toString());
    out.endTag(null, typeStr);
    // -- "useless" code skipped
}

Second : why the result is true ?

The commit method has the following code :

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null/* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        returnfalse;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

So it returns the mcr.writeToDiskResult which is set in the SharedPreferencesImpl.writeToFile(MemoryCommitResult) method. Interesting piece of code :

writeToFile(MemoryCommitResult mcr) {
    // -- "useless" code skippedtry {
        FileOutputStreamstr= createFileOutputStream(mFile);
        if (str == null) {
            mcr.setDiskWriteResult(false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            finalStructStatstat= Libcore.os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        mcr.setDiskWriteResult(true);
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    // -- "useless" code skipped
}

As we see at the previous point : the XML writing is "ok" (do not throw anything, do not fails), so the sync in the file will be too (just a copy of a Stream in another one, nothing checks the XML content here !).

Currently : your key was converted to (badly formatted) XML and correctly wrote in a File. The result of the whole operation is true as everything went OK. Your changes are comitted to the disk and in the memory.

Third and last : why do I get back the correct value the first time and a bad one the second time

Take a quick look to what happen when we commit the changes to memory in SharedPreferences.Editor.commitToMemory(...) method (interesting part only... :)):

for (Map.Entry<String, Object> e : mModified.entrySet()) {
    String k = e.getKey();
    Object v = e.getValue();
    if (v == this) {  // magic value for a removal mutationif (!mMap.containsKey(k)) {
            continue;
        }
        mMap.remove(k);
    } else {
        boolean isSame = false;
        if (mMap.containsKey(k)) {
            Object existingValue = mMap.get(k);
            if (existingValue != null && existingValue.equals(v)) {
                continue;
            }
        }
        mMap.put(k, v);
    }

    mcr.changesMade = true;
    if (hasListeners) {
        mcr.keysModified.add(k);
    }
}

Important point : the changes are commited to the mMap attribute.

Then, take a quick look to how we get back a value :

publicbooleangetBoolean(String key, boolean defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        Boolean v = (Boolean)mMap.get(key);
        return v != null ? v : defValue;
    }
}

We are taking back the key from mMap (no reading of the value in the file for now). So we have the correct value for this time :)

When you reload your application, you will load the data back from the disk, and so the SharedPreferencesImpl constructor will be called, and it will call the SharedPreferencesImpl.loadFromDiskLocked() method. This method will read the file content and load it in the mMap attribute (I let you see the code by yourself, link provided at the end).

A step-by-step debug shown me that the abc\n was written as abc (with a whitespace character). So, when you will try to get it back, you will never succeed.


To finish, thank you to @CommonsWare to give me a hint about the file content in the comment :)

Links

XmlUtils

FastXmlSerializer

SharedPreferencesImpl

SharedPreferencesImpl.EditorImpl.commit()

SharedPreferencesImpl.EditorImpl.commitToMemory()

SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)

SharedPreferencesImpl.writeToFile(MemoryCommitResult)

SharedPreferencesImpl.loadFromDiskLocked()

Post a Comment for "Sharedpreferences In Android Not Persisted To Disk When Key Contains Newline"