title: android SharedPreferences null keys, values and sets<?> - corner cases
body: <p>Been extensively testing the <code>SharedPreferences</code> framework. While most works as one would expect I run across some cases where I wonder what's the reasoning behind them. I give some tests all of which pass - their common set up is :</p>
  
  <pre><code>Context ctx;
  SharedPreferences prefs;
  Editor ed;
  protected void setUp() throws Exception {
      super.setUp();
      ctx = getContext();
      prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
      ed = prefs.edit();
  }
  protected void tearDown() throws Exception {
      if (ed != null) ed.clear().commit();
      super.tearDown();
  }
  </code></pre>
  
  <p>Now the weird points :</p>
  
  <ol>
  <li><p>When I put a null value in, <em>I have to ask for it with a null default to get it back</em> - if the default is non null I get this default back. <em>Even if the default is of a different type from the one I put in</em>. Applies to <code>String</code> and <code>Set&lt;String&gt;</code> (but I can get back a boolean for instance) :</p>
  
  <pre><code>public void testNullString() {
      ed.putString("string_key", null); // putString() and putStringSet() only
      ed.commit();
      assertTrue(prefs.contains("string_key"));
      assertEquals(null, prefs.getString("string_key", null));
      // if I specify a non null default I get this default back, not null
      assertEquals("a string", prefs.getString("string_key", "a string"));
      // *even if I ask for a boolean*
      assertEquals(true, prefs.getBoolean("string_key", true));
  }
  </code></pre></li>
  <li><p>I can easily put a <code>Set&lt;Integer&gt;</code> inside the prefs :</p>
  
  <pre><code>@SuppressWarnings("unchecked")
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public void testNullStringSetsRaw() {
      if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.HONEYCOMB) {
          @SuppressWarnings("rawtypes")
          final Set integerHashSet = new HashSet();
          integerHashSet.add(1);
          ed.putStringSet("set_key", integerHashSet);
          ed.commit();
          final Set&lt;String&gt; defValues = new HashSet&lt;String&gt;();
          defValues.add("a string");
          Set&lt;String&gt; s = prefs.getStringSet("set_key", defValues);
          final String msg = "The set I put in: " + integerHashSet
              + " and what I got out :" + s;
          Log.e(TAG, msg); // the same - [1]
          assertTrue(msg, integerHashSet.equals(s));
          assertTrue(s.contains(1)); // !
      }
  }
  </code></pre></li>
  <li><p>What's the deal with null keys ? They seem to be perfectly legal keys:</p>
  
  <pre><code>public void testNullKeyNonNullString() {
      final String NULL_KEY = null;
      ed.putString(NULL_KEY, "a string");
      ed.commit();
      assertTrue("Contains null key", prefs.contains(NULL_KEY));
      assertEquals("Retrieve the value giving null default", "a string",
          prefs.getString(NULL_KEY, null));
      assertEquals("Retrieve the value giving default", "a string",
          prefs.getString(NULL_KEY, "a string" + "blah"));
      try {
          // no deal !
          prefs.getBoolean(NULL_KEY, true);
          fail("Expected CCE");
      } catch (ClassCastException e) {}
  }
  </code></pre>
  
  <p>but I have seen in my logs things like : <code>org.xmlpull.v1.XmlPullParserException: Map value without name attribute: boolean</code> - see <a href="https://stackoverflow.com/questions/5368442/android-getsharedpreferences-error-map-value-without-name-attribute-boolean">android getSharedPreferences error: Map value without name attribute: boolean</a> for a discussion. I wonder if this is related to <code>null</code> keys. <strong>EDIT</strong> : it is related- reproducer :</p>
  
  <pre><code>public class XmlExceptionTest extends AndroidTestCase {
  
      /** Run it twice - on the second run the exception is thrown */
      public void testXmlException() {
          Context ctx = getContext();
          SharedPreferences prefs = PreferenceManager
              .getDefaultSharedPreferences(ctx);//exception thrown here(line 18)
          // and apparently it clears the prefs as the condition below is false
          if (prefs.contains("run_once")) { // false
              Log.w("XmlExceptionTest",
                  "contains null key :" + prefs.contains(null));
          }
          Editor e = prefs.edit();
          e.putBoolean("run_once", true).commit();
          e.putString(null, "I put a sting with null key").commit();
          assertTrue("Contains null", prefs.contains(null));
          PreferenceManager.getDefaultSharedPreferences(ctx); // exception
          // NOT thrown here  - why ? - apparently there is a static factory
          // returning the instance it already constructed
          // e.clear().commit(); // this eliminates the exception
      }
  }
  </code></pre>
  
  <p>exception :</p>
  
  <pre><code>W/ApplicationContext(): getSharedPreferences
  W/ApplicationContext(): org.xmlpull.v1.XmlPullParserException: Map value without name attribute: string
  W/ApplicationContext():     at com.android.internal.util.XmlUtils.readThisMapXml(XmlUtils.java:521)
  W/ApplicationContext():     at com.android.internal.util.XmlUtils.readThisValueXml(XmlUtils.java:733)
  W/ApplicationContext():     at com.android.internal.util.XmlUtils.readValueXml(XmlUtils.java:667)
  W/ApplicationContext():     at com.android.internal.util.XmlUtils.readMapXml(XmlUtils.java:470)
  W/ApplicationContext():     at android.app.ContextImpl.getSharedPreferences(ContextImpl.java:361)
  W/ApplicationContext():     at android.preference.PreferenceManager.getDefaultSharedPreferences(PreferenceManager.java:348)
  W/ApplicationContext():     at gr.uoa.di.android.helpers.test.XmlExceptionTest.testXmlException(XmlExceptionTest.java:18)
  W/ApplicationContext():     at java.lang.reflect.Method.invokeNative(Native Method)
  W/ApplicationContext():     at java.lang.reflect.Method.invoke(Method.java:521)
  W/ApplicationContext():     at junit.framework.TestCase.runTest(TestCase.java:154)
  W/ApplicationContext():     at junit.framework.TestCase.runBare(TestCase.java:127)
  W/ApplicationContext():     at junit.framework.TestResult$1.protect(TestResult.java:106)
  W/ApplicationContext():     at junit.framework.TestResult.runProtected(TestResult.java:124)
  W/ApplicationContext():     at junit.framework.TestResult.run(TestResult.java:109)
  W/ApplicationContext():     at junit.framework.TestCase.run(TestCase.java:118)
  W/ApplicationContext():     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:169)
  W/ApplicationContext():     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:154)
  W/ApplicationContext():     at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:520)
  W/ApplicationContext():     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1447)
  </code></pre>
  
  <p>and apparently <em>the preferences are cleared when the exception is thrown</em> (bad bad bad)
  <br/></p></li>
  </ol>
  
  <p>So my questions are : is indeed the behavior as I state it (or have I missed something silly) ? What is the motivation behind it (especially the null values behavior) ? Is it documented and guaranteed to stay so - or may change ? Is point 2 an oversight ?</p>
  
  <p><a href="https://github.com/Utumno/AndroidHelpersTest/tree/master/src/gr/uoa/di/android/helpers/test" rel="nofollow noreferrer">Test project.</a></p>