Saturday, 3 November 2012

Display a Preference Fragment compatible with old devices by using the compatibility library

Objective


Display a Preference Fragment compatible with old devices by using the compatibility library.

Issue


The compatibility library does not contain the PreferenceFragment class, so you can't display preferences inside a fragment.

Solution

Steps
1. Add the compatibility library to your project
2. Add the PreferenceListFragment class to your project
3. Create a fragment that extends the PreferenceListFragment class
4. Create the xml file corresponding to the settings you want to display and add it to the res/xml directory
5. Implements the interface OnPreferenceAttachedListener in your activity
6. Add the settings fragment to your activity
Details
1. Add the compatibility library to your project

If you are using Eclipse, you just have to copy and paste the android-support-v4.jar into the libs directory of your project.

2. Add the PreferenceListFragment class to your project

The original source code has been posted on this forum :
http://forum.xda-developers.com/showthread.php?t=1363906

   
public class PreferenceListFragment extends ListFragment{
    
    private PreferenceManager mPreferenceManager;
    
    /**
     * The starting request code given out to preference framework.
     */
    private static final int FIRST_REQUEST_CODE = 100;
    
    private static final int MSG_BIND_PREFERENCES = 0;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                
                case MSG_BIND_PREFERENCES:
                    bindPreferences();
                    break;
            }
        }
    };
    private ListView lv;
    private int xmlId;
    
    public PreferenceListFragment(int xmlId){
        this.xmlId = xmlId;
    }
    //must be provided
    public PreferenceListFragment(){
        
    }
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle b){
        postBindPreferences();
        return lv;
    }
    
    @Override
    public void onDestroyView(){
        super.onDestroyView();
        ViewParent p = lv.getParent();
        if(p != null)
            ((ViewGroup)p).removeView(lv);
    }

    @Override
    public void onCreate(Bundle b) {
        super.onCreate(b);
        if(b != null)
            xmlId = b.getInt("xml");
        mPreferenceManager = onCreatePreferenceManager();
        lv = (ListView) LayoutInflater.from(getActivity()).inflate(R.layout.preference_list_content, null);
        lv.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
        addPreferencesFromResource(xmlId);
        postBindPreferences();
        ((OnPreferenceAttachedListener)getActivity()).onPreferenceAttached(getPreferenceScreen(), xmlId);
    }

    @Override
    public void onStop(){
        super.onStop();
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
            m.setAccessible(true);
            m.invoke(mPreferenceManager);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        lv = null;
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
            m.setAccessible(true);
            m.invoke(mPreferenceManager);
           }catch(Exception e){
               e.printStackTrace();
           }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putInt("xml", xmlId);
        super.onSaveInstanceState(outState);

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
            m.setAccessible(true);
            m.invoke(mPreferenceManager, requestCode, resultCode, data);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    /**
     * Posts a message to bind the preferences to the list view.
     * <p>
     * Binding late is preferred as any custom preference types created in
     * {@link #onCreate(Bundle)} are able to have their views recycled.
     */
    private void postBindPreferences() {
        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
    }
    
    private void bindPreferences() {
        final PreferenceScreen preferenceScreen = getPreferenceScreen();
        if (preferenceScreen != null) {
            preferenceScreen.bind(lv);
        }
    }
    
    /**
     * Creates the {@link PreferenceManager}.
     * 
     * @return The {@link PreferenceManager} used by this activity.
     */
    private PreferenceManager onCreatePreferenceManager() {
        try{
            Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
            c.setAccessible(true);
            PreferenceManager preferenceManager = c.newInstance(this.getActivity(), FIRST_REQUEST_CODE);
            return preferenceManager;
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * Returns the {@link PreferenceManager} used by this activity.
     * @return The {@link PreferenceManager}.
     */
    public PreferenceManager getPreferenceManager() {
        return mPreferenceManager;
    }

    /**
     * Sets the root of the preference hierarchy that this activity is showing.
     * 
     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
     */
    public void setPreferenceScreen(PreferenceScreen preferenceScreen){
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
            m.setAccessible(true);
            boolean result = (Boolean) m.invoke(mPreferenceManager, preferenceScreen);
            if (result && preferenceScreen != null) {
                postBindPreferences();
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    
    /**
     * Gets the root of the preference hierarchy that this activity is showing.
     * 
     * @return The {@link PreferenceScreen} that is the root of the preference
     *         hierarchy.
     */
    public PreferenceScreen getPreferenceScreen(){
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
            m.setAccessible(true);
            return (PreferenceScreen) m.invoke(mPreferenceManager);
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * Adds preferences from activities that match the given {@link Intent}.
     * 
     * @param intent The {@link Intent} to query activities.
     */
    public void addPreferencesFromIntent(Intent intent) {
        throw new RuntimeException("too lazy to include this bs");
    }
    
    /**
     * Inflates the given XML resource and adds the preference hierarchy to the current
     * preference hierarchy.
     * 
     * @param preferencesResId The XML resource ID to inflate.
     */
    public void addPreferencesFromResource(int preferencesResId) {   
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
            m.setAccessible(true);
            PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(mPreferenceManager, getActivity(), preferencesResId, getPreferenceScreen());
            setPreferenceScreen(prefScreen);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    
    /**
     * Finds a {@link Preference} based on its key.
     * 
     * @param key The key of the preference to retrieve.
     * @return The {@link Preference} with the key, or null.
     * @see PreferenceGroup#findPreference(CharSequence)
     */
    public Preference findPreference(CharSequence key) {
        if (mPreferenceManager == null) {
            return null;
        }
        return mPreferenceManager.findPreference(key);
    }
    
    public interface OnPreferenceAttachedListener{
        public void onPreferenceAttached(PreferenceScreen root, int xmlId);
    }
    
}

3. Create a fragment that extends the PreferenceListFragment class
   

    public class SettingsFragment extends PreferenceListFragment implements
        SharedPreferences.OnSharedPreferenceChangeListener,
        PreferenceListFragment.OnPreferenceAttachedListener {
    public static final String SHARED_PREFS_NAME = "settings";

    @Override
    public void onCreate(Bundle icicle) {

        super.onCreate(icicle);
        PreferenceManager preferenceManager = getPreferenceManager();
        preferenceManager.setSharedPreferencesName(SHARED_PREFS_NAME);
        addPreferencesFromResource(R.xml.settings);
        preferenceManager.getSharedPreferences()
                .registerOnSharedPreferenceChangeListener(this);

    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
            String key) {
    }

    @Override
    public void onPreferenceAttached(PreferenceScreen root, int xmlId) {
        if (root == null)
            return;
    }
}


4. Create the xml file corresponding to the settings you want to display and add it to the res/xml directory.

Here is an example of how it could look like :
   

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

    <PreferenceCategory android:title="My Settings" >
        <CheckBoxPreference
            android:summary="My option 1"
            android:title="Description of my option 1" />
        
        <CheckBoxPreference
            android:summary="My option 1"
            android:title="Description of my option 2" />
    </PreferenceCategory>
   

</PreferenceScreen>
5. Implements the interface OnPreferenceAttachedListener in your activity
  

Public class MainActivity extends FragmentActivity implements OnPreferenceAttachedListener {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    public void onPreferenceAttached(PreferenceScreen root, int xmlId) {
    }
}
6. Add the settings fragment to your activity

There are several ways to do this, in this example, I am just adding it in the layout of the main activity.

   
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<fragment
    android:id="@+id/myfragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    class="com.fordemobile.preferencelistfragment.SettingsFragment" />

</LinearLayout>

24 comments:

  1. Nice post.Give it up. Thanks for share this article. For more visit:Web App Development

    ReplyDelete
  2. Hi. thank you for this article . I implement the code. I only have a null exception when screen rotates .

    ReplyDelete
  3. Hi. Thank you for this article. I tried to implement it but I have some troubles with the inflating of "preference_list_content" (PreferenceListFragment:53).
    It seems that I don't have access to this android's file. I tried this Resources.getSystem().getIdentifier("preference_list_content", "layout", "android") (http://mpigulski.blogspot.ch/2011/03/accessing-comandroidinternalr-resources.html) and an other problem : The line returned a LinearLayout and not a ListView. (I also try to get the listview in the layout but a problem: "The specified child already has a parent")

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. The first comment didn't appear because of the less than signs, so i had to delete it.
      Change the (lt)-s to the less than sign and this should work.

      (lt)?xml version="1.0" encoding="utf-8"?>
      (lt)!--
      /* //device/apps/common/assets/res/layout/list_content.xml
      **
      ** Copyright 2006, The Android Open Source Project
      **
      ** Licensed under the Apache License, Version 2.0 (the "License");
      ** you may not use this file except in compliance with the License.
      ** You may obtain a copy of the License at
      **
      ** http://www.apache.org/licenses/LICENSE-2.0
      **
      ** Unless required by applicable law or agreed to in writing, software
      ** distributed under the License is distributed on an "AS IS" BASIS,
      ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      ** See the License for the specific language governing permissions and
      ** limitations under the License.
      */
      -->
      (lt)ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:drawSelectorOnTop="false"
      android:scrollbarAlwaysDrawVerticalTrack="true"
      />

      Delete
  4. Thanks for providing such a useful information. Hope to get some more information in future also.
    Husband Wife Dispute Solution Specialist Shri Mukesh Aghori Ji. 100% satisfaction guarantee Call @ +91-9815872813.

    Regards,
    Shri Mukesh Aghori Ji

    ReplyDelete
  5. Great... Excellent sharing.. This is very helpful for beginers. Read that provide me more enthusiastic. This helps me get a more knowledge about this topic. Thanks for this.
    Android Training in Chennai

    ReplyDelete
  6. No.1 Astrologer in India Pt. Ravikant Shastri Ji 8 Time Gold Medallist Vashikaran specialist Love problem , Marriage problem , Family problem and all problem solution. 1001 % Guarantee.

    Website :- http://www.ravikantshastri.com

    ReplyDelete
  7. Astrologer RK Swami Ji Vashikaran specialist , Love problem , Marriage problem , Lottery number specialist , Black magic specialist and all problem solution. For more info:- call at:- +91-8284851117

    Website :- http://www.rkswamiji.com

    ReplyDelete
  8. Love marriage specialist astrologer RK Swami world NO.1 famous pandit ji 11 time goldmedalist and 25 years experience. our service in USA, UK, UAE, India, Australia, Canada, Singapore, Malaysia etc.

    Vashikaran Specialist

    ReplyDelete
  9. I was having serious relationship problems with my boyfriend and it had resulted in him moving out to his friend’s apartment. Everything got worse because he started going to bars and strip clubs frequently with his friend, getting drunk and passing out. He always threatens me on phone whenever I call him because of all the bad advises that his friend has given him. I really love him and we had been dating for 8 years which gave us a beautiful daughter. I had also lost a lot of money on therapists until I was introduced to Dr.Ogudugu by a friend whom he helped to marry her childhood boyfriend; this gave me total confidence and strength to get him back. I did all he asked and after 48 hours my boyfriend called me and rushed back home, things just changed between us emotionally. He has a job and stopped drinking and keeping irrelevant friends. It’s a miracle I never believed was possible because I had lost all hope until I found Dr.Ogudugu. So that’s why I promised to share my testimony all over the universe. All thanks goes to Dr.Ogudugu for the excessive work that he has done for me. Below is the email address in situation you are undergoing a heart break, and I assure you that as he has done mine for me, he will definitely help you too GREATOGUDUGU@GMAIL.COM.

    ReplyDelete
  10. Hello friends i am provide social bookmarking site list and SEO updates and you can read google update and site...
    http://seosafe.blogspot.in/2016/10/best-new-social-bookmarking-sites-list.html

    ReplyDelete
  11. Uncharted 5 will be unbelievable if Cassie came in as Nate's replacement! As the daughter of Elena and Nathan, she has all the resources she will ever need to become the next treasure hunter in the family. And having her as the protagonist will ensure that we get to see Nate, Elena, and everyone else in at least some capacity. Uncharted 5 Official Trailer

    ReplyDelete