Android’s PreferenceActivity for all API versions

I have spent the last few days learning about how to use the new Android PreferenceFragment which requires PreferenceActivity to override a new v11 (Honeycomb) method called onBuildHeaders().  Unfortunately, the documentation is not very clear how one would create a single PreferenceActivity that could play well in all versions, utilizing the newest features if you have it and avoiding an app crash on older Android versions. I encountered several solutions to this issue by creating two different activities for the two different mechanisms requiring two entries in your AndroidManifest.xml file.  Having two different PreferenceActivities means if you have library code that extends that class, you now have to duplicate it.  Then, if your app descends your library class, now has to be duplicated yet again.  The end result is … less than ideal.

Thankfully, after spending a great deal of time on the subject, I have come up with a single class solution that will work in all Android versions. A couple of the newer methods need to be found using reflection and based off of them, in addition to using the newly introduced xml file of <preference-headers>, allow for a single class solution.

First, take your old <PreferenceScreen> xml layouts and break them up into several files based on how you would like to see them categorized using the new header format.  Simply breaking them apart so that each PreferenceCategory has its own file is a good start.  For our example here,  we shall assume there are 3 such files, all in the res/xml folder: app_prefs_cat1.xml, app_prefs_cat2.xml, and app_prefs_cat3.xml

Now let us create the descendant PrefsActivity class:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;

public class PrefsActivity extends PreferenceActivity {
    protected Method mLoadHeaders = null;
    protected Method mHasHeaders = null;

    /**
     * Checks to see if using new v11+ way of handling PrefsFragments.
     * @return Returns false pre-v11, else checks to see if using headers.
     */
    public boolean isNewV11Prefs() {
        if (mHasHeaders!=null && mLoadHeaders!=null) {
            try {
                return (Boolean)mHasHeaders.invoke(this);
            } catch (IllegalArgumentException e) {
            } catch (IllegalAccessException e) {
            } catch (InvocationTargetException e) {
            }
        }
        return false;
    }

    @Override
    public void onCreate(Bundle aSavedState) {
        //onBuildHeaders() will be called during super.onCreate()
        try {
            mLoadHeaders = getClass().getMethod("loadHeadersFromResource", int.class, List.class );
            mHasHeaders = getClass().getMethod("hasHeaders");
        } catch (NoSuchMethodException e) {
        }
        super.onCreate(aSavedState);
        if (!isNewV11Prefs()) {
            addPreferencesFromResource(R.xml.app_prefs_cat1);
            addPreferencesFromResource(R.xml.app_prefs_cat2);
            addPreferencesFromResource(R.xml.app_prefs_cat3);
        }
    }

    @Override
    public void onBuildHeaders(List<Header> aTarget) {
        try {
            mLoadHeaders.invoke(this,new Object[]{R.xml.pref_headers,aTarget});
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        } catch (InvocationTargetException e) {
        }	
    }

    static public class PrefsFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle aSavedState) {
            super.onCreate(aSavedState);
            Context anAct = getActivity().getApplicationContext();
            int thePrefRes = anAct.getResources().getIdentifier(getArguments().getString("pref-resource"),
                    "xml",anAct.getPackageName());
            addPreferencesFromResource(thePrefRes);
        }
    }
}

On older Android versions, the reflected methods will be null and thus avoids calling the newer methods, using the older mechanism of adding the various preference xml files in onCreate(). In order to use the new Honeycomb (v11+) preference mechanism, we need to create one more xml file – the newer <preference-headers> xml file that ties all the category files together. The res/xml resource file “pref_headers.xml”:

<?xml version="1.0" encoding="utf-8"?>
<preference-headers 
    xmlns:android="http://schemas.android.com/apk/res/android" >

<header android:fragment="my.domain.app.PrefsActivity$PrefsFragment"
    android:icon="@android:drawable/ic_menu_sort_by_size"
    android:title="Category 1 prefs" >
    <extra android:name="pref-resource" android:value="app_prefs_cat1" />
</header>        

<header android:fragment="my.domain.app.PrefsActivity$PrefsFragment"
    android:icon="@android:drawable/ic_menu_gallery"
    android:title="Category 2 prefs" >
    <extra android:name="pref-resource" android:value="app_prefs_cat2" />
</header>

<header android:fragment="my.domain.app.PrefsActivity$PrefsFragment"
    android:icon="@android:drawable/ic_menu_edit"
    android:title="Category 3 prefs" >
    <extra android:name="pref-resource" android:value="app_prefs_cat3" />
</header>

<header 
    android:icon="@drawable/icon_question"
    android:title="Blackmoon Info Tech Services"
    android:summary="link to BITS blog">
    <intent android:action="android.intent.action.VIEW" 
            android:data="http://www.blackmoonit.com/2012/07/all_api_prefsactivity/" />
</header>

</preference-headers>

The first three headers list the three standard category xml files we created, but you are also free to add more items as you see fit, just remember that those will only be visible on devices with Android 3.0 or later.

Now that we have our xml file and single preference activity, we can define it in our AndroidManifest.xml as usual. Note: do not use android:launchMode=”singleTop” for your prefs activity or else it will break phones using 4.0+ by never showing more than just the header list.

    <activity android:name="PrefsActivity" 
        android:label="Settings" 
        android:enabled="true" >
        <intent-filter>
            <category android:name="android.intent.category.PREFERENCE" />
        </intent-filter>
    </activity>

We can have a menu call our prefs activity with just one line:

    startActivity(new Intent(this, PrefsActivity.class);

 

Now that you have a descendant class like this one, you can freely use it in all versions of Android and it will automatically use the newer mechanism where appropriate.

Enjoy!

29 thoughts on “Android’s PreferenceActivity for all API versions

  1. Dude! Thanks! This is awesome… Just a bit of feedback: for newbies, could you include the package imports to ensure they have imported the right stuff.
    Additionally, if your minSDK<11, Eclipse will still tell you that the addPreferencesFromResources methods is depecrated.
    Is it necessary to break the PreferenceScreen into multiple parts for the Activity's onCreate? I can see why it's important for the Fragment, since we are checking for headers…

    • >…could you include the package imports…
      Since I can’t think of a reason for “why not”, sure. 😉 Post updated.

      Eclipse/Lint will warn you about using the deprecated method, but just ignore the warning. We are using the method only when we have to, which is whenever we do not have v11+ style preferences and must use it, which is OK. Do not be frightened about Deprecated code when you have accounted for it, Android won’t remove deprecated methods anytime soon. If it ever did occur, you won’t even need this class anymore as you would be forced to only target newer devices. The Deprecated mechanism is there to warn you that there is a better way to handle something on the latest API version, but once you have accounted for it, you can safely ignore the warning from then on. Removing all calls to deprecated methods would only result in forcing your code to only run on newer devices — thus negating the need to be backward compatible at all.

      The reason you are breaking up the PreferenceScreens has nothing to do with Activities vs Fragments. You are breaking them up so that each preference file can be it’s own category using the new mechanism while still being able to put them all back together as one using the old mechanism — all the while not duplicating PreferenceScreen definitions across multiple files (one for old way, one for new way) which just causes bugs. If you used just a single PreferenceScreen file under the new mechanism you would see just one category listing all your preferences under it. That is fine for apps with only a few preferences, but if you have more than a few, it is visually more appealing if you separated them out into distinctive areas.

  2. I was under the impression that this code should compile with minSDK<11, but "static public class PrefsFragment extends PreferenceFragment" does not compile, because the PreferenceFragment does not exists before API level 11.

    The settings for my project currently are: minSdkVersion="10" targetSdkVersion="16"

    Maybe I am missing something…

    • Considering my projects are minSdkVersion=”3″ and targetSdkVersion=”16″, I can assure you that it does indeed compile and work on API’s lower than 11. Make sure you are using a build target of Android 3.0+ or you will run into that issue. The XML file does not tell the compiler what to use, you need to change that in the project’s properties (Project properties> Android section> Project Build Target).

      • I am having the same problem “Call requires API level 11 (current min is 8): android.preference.PreferenceFragment#”, my Project properties> Android section> Project Build Target are set to API level 16.

      • My IDE: Eclipse Helios Service Release 2
        My AndroidManifest tag: <uses-sdk android:minSdkVersion=”3″ android:targetSdkVersion=”16″ />
        My Project Properties> Android> Project Build Target = Android 4.1, API 16.
        You should see Android 3.0 or better in your project’s Package Explorer window as an included library. If you see an Android version lower than 3.0 here, you will get these “Call requires…” issues.

  3. How do you add a dialogfragment to the preferenceactivity class?Right now i’m using the compatibility package and it’s fragment.show() method expects a FragmentManager object of the v4 type. The getFragmentManager from PreferenceActivity returns a android.app.FragmentManager instead ofandroid.support.v4.app.FragmentManager one.

    regards,

    • App preferences on a single screen for pre-Honeycomb devices are typical, but definitely not the only way to do it. Official documentation tries to accomplish creating new apps across older devices, whereas my approach has been to upgrade older apps to function on newer devices. Changing the look on older phones is not always desirable. Neither way is “more correct” than the other and each approach has their pro’s and con’s. Use what works best.

  4. Can you please an idea because I can’t seem to retrieve the value of the specified preference.

    appPrefs = getSharedPreferences(“net.xiomai.LXPreferenceActivity_preferences”, MODE_PRIVATE);

    lxIDNo = appPrefs.getString(“idNoTextPref”,””);

    • There is not enough information to give you any advice. If you are expected the XML defined default value to be there, it will not be returned. You must save a value to it, supply a default in the 2nd parameter to getString() or call the static method PreferenceManager.setDefaultValues() during your Activity’s onCreate().

  5. Pingback: Binary App Dev: Apps | Preference Activity is crashing on Android 2.3

    • The Java class validation that takes place checks for the existence of all properties, consts, and methods referred to in the file regardless if they exist in dead branches or not. Putting if() statements around newly introduced methods will still throw a validation exception in older Android versions.

  6. Pingback: How to: Was PreferenceFragment intentionally excluded from the compatibility package? | SevenNet

Leave a Reply

Your email address will not be published. Required fields are marked *