org.dmfs.android.retentionmagic.RetentionMagic.java Source code

Java tutorial

Introduction

Here is the source code for org.dmfs.android.retentionmagic.RetentionMagic.java

Source

/*
 * Copyright (C) 2013 Marten Gajda <marten@dmfs.org>
 *
 * 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.
 * 
 */

package org.dmfs.android.retentionmagic;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.dmfs.android.retentionmagic.annotations.Parameter;
import org.dmfs.android.retentionmagic.annotations.ParameterArrayList;
import org.dmfs.android.retentionmagic.annotations.Retain;
import org.dmfs.android.retentionmagic.annotations.RetainArrayList;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Fragment;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
import android.util.SparseArray;

/**
 * Helper to store and restore instance values.
 * 
 * @author Marten Gajda <marten@dmfs.org>
 */
public final class RetentionMagic {
    /**
     * Map of final classes to their respective {@link PersistenceHelper}s. Since we don't have to expect subclasses of these classes we can get the helpers
     * with a simple <code>get()</code>.
     * <p>
     * Since we're always called from the UI thread, there is no need to synchronize access to this map.
     * </p>
     */
    private final static Map<Class<?>, PersistenceHelper> FINAL_CLASS_HELPERS = new HashMap<Class<?>, PersistenceHelper>();

    /**
     * Map of non-final classes and interfaces to their respective {@link PersistenceHelper}s. A simple <code>get()</code> won't match, so we have to check each
     * key separately here.
     * <p>
     * Since we're always called from the UI thread, there is no need to synchronize access to this map.
     * </p>
     */
    private final static Map<Class<?>, PersistenceHelper> OTHER_CLASS_HELPERS = new HashMap<Class<?>, PersistenceHelper>();

    /**
     * Map of final generic type classes to their respective {@link PersistenceHelper}s. Since we don't have to expect subclasses of these classes we can get
     * the helpers with a simple <code>get()</code>.
     * <p>
     * Since we're always called from the UI thread, there is no need to synchronize access to this map.
     * </p>
     */
    private final static Map<Class<?>, PersistenceHelper> ARRAYLIST_FINAL_CLASS_HELPERS = new HashMap<Class<?>, PersistenceHelper>();

    /**
     * Map of non-final generic type classes and interfaces to their respective {@link PersistenceHelper}s. A simple <code>get()</code> won't match, so we have
     * to check each key
     * <p>
     * Since we're always called from the UI thread, there is no need to synchronize access to this map.
     * </p>
     */
    private final static Map<Class<?>, PersistenceHelper> ARRAYLIST_OTHER_CLASS_HELPERS = new HashMap<Class<?>, PersistenceHelper>();

    /**
     * Maps Activity and Fragment classes to a maps of fields to their respective {@link PersistenceHelper}s.
     */
    private final static Map<Class<?>, Map<Field, PersistenceHelper>> CLASS_CACHE = new HashMap<Class<?>, Map<Field, PersistenceHelper>>();

    static {
        FINAL_CLASS_HELPERS.put(boolean.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setBoolean(instance, bundle.getBoolean(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putBoolean(key, field.getBoolean(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setBoolean(instance, prefs.getBoolean(key, field.getBoolean(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putBoolean(key, field.getBoolean(instance));
            }
        });

        // TODO: support storing of boolean arrays as base64 encoded bit fields in SharedPreferences
        FINAL_CLASS_HELPERS.put(boolean[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getBooleanArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putBooleanArray(key, (boolean[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(byte.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setByte(instance, bundle.getByte(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putByte(key, field.getByte(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setByte(instance, (byte) (prefs.getInt(key, field.getByte(instance)) & 0xff));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putInt(key, field.getByte(instance));
            }
        });

        // TODO: support storing byte arrays as Base64 encoded arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(byte[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getByteArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putByteArray(key, (byte[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(short.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setShort(instance, bundle.getShort(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putShort(key, field.getShort(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setShort(instance, (short) prefs.getInt(key, field.getShort(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putInt(key, field.getShort(instance));
            }
        });

        // TODO: support storing short arrays as Base64 encoded arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(short[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getShortArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putShortArray(key, (short[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(char.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setChar(instance, bundle.getChar(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putChar(key, field.getChar(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setChar(instance, (char) prefs.getInt(key, field.getChar(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putInt(key, field.getChar(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(char[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getCharArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putCharArray(key, (char[]) field.get(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.set(instance, prefs.getString(key, new String((char[]) field.get(instance))).toCharArray());
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putString(key, new String((char[]) field.get(instance)));
            }

        });

        FINAL_CLASS_HELPERS.put(int.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setInt(instance, bundle.getInt(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putInt(key, field.getInt(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setInt(instance, prefs.getInt(key, field.getInt(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putInt(key, field.getInt(instance));
            }

        });

        // TODO: support storing integer arrays as Base64 encoded arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(int[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getIntArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putIntArray(key, (int[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(long.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setLong(instance, bundle.getLong(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putLong(key, field.getLong(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setLong(instance, prefs.getLong(key, field.getLong(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putLong(key, field.getLong(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(long[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getLongArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putLongArray(key, (long[]) field.get(instance));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key, Editor editor)
                    throws IllegalAccessException {
                StringBuilder arrayStringBuilder = new StringBuilder(1024);
                long[] longArray = (long[]) field.get(instance);

                if (longArray != null) {
                    boolean first = true;
                    for (int i = 0; i < longArray.length; i++) {
                        if (first) {
                            first = !first;
                        } else {
                            arrayStringBuilder.append(",");
                        }
                        arrayStringBuilder.append(longArray[i]);
                    }
                    editor.putString(key, arrayStringBuilder.toString());
                } else {
                    editor.putString(key, null);
                }
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                String longArrayPref = prefs.getString(key, (String) field.get(instance));
                long[] longArray = null;

                if (longArrayPref != null) {
                    if (longArrayPref.length() > 0) {
                        String[] arrayString = longArrayPref.split(",");
                        longArray = new long[arrayString.length];
                        for (int i = 0; i < longArray.length; i++) {
                            longArray[i] = Long.valueOf(arrayString[i]);
                        }
                    } else {
                        longArray = new long[0];
                    }
                }
                field.set(instance, longArray);
            }
        });

        FINAL_CLASS_HELPERS.put(float.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setFloat(instance, bundle.getFloat(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putFloat(key, field.getFloat(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.setFloat(instance, prefs.getFloat(key, field.getFloat(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putFloat(key, field.getFloat(instance));
            }
        });

        // TODO: support storing float arrays as Base64 encoded arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(float[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getFloatArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putFloatArray(key, (float[]) field.get(instance));
            }
        });

        // TODO: support douple in SharedPreferences
        FINAL_CLASS_HELPERS.put(double.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.setDouble(instance, bundle.getDouble(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putDouble(key, field.getDouble(instance));
            }
        });

        // TODO: support storing double arrays as Base64 encoded arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(double[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getDoubleArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putDoubleArray(key, (double[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(String.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getString(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putString(key, (String) field.get(instance));
            }

            @Override
            public void restoreFromPreferences(Field field, Object instance, String key, SharedPreferences prefs)
                    throws IllegalAccessException {
                field.set(instance, prefs.getString(key, (String) field.get(instance)));
            }

            @Override
            public void storeInPreferences(Field field, Object instance, String key,
                    SharedPreferences.Editor editor) throws IllegalAccessException {
                editor.putString(key, (String) field.get(instance));
            }

        });

        // TODO: support storing string arrays in SharedPreferences
        FINAL_CLASS_HELPERS.put(String[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getStringArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putStringArray(key, (String[]) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(Bundle.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getBundle(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putBundle(key, (Bundle) field.get(instance));
            }
        });

        FINAL_CLASS_HELPERS.put(SparseArray.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getSparseParcelableArray(key));
            }

            @SuppressWarnings("unchecked")
            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putSparseParcelableArray(key, (SparseArray<Parcelable>) field.get(instance));
            }
        });

        ARRAYLIST_FINAL_CLASS_HELPERS.put(Integer.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getIntegerArrayList(key));
            }

            @SuppressWarnings("unchecked")
            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putIntegerArrayList(key, (ArrayList<Integer>) field.get(instance));
            }
        });

        ARRAYLIST_FINAL_CLASS_HELPERS.put(String.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getStringArrayList(key));
            }

            @SuppressWarnings("unchecked")
            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putStringArrayList(key, (ArrayList<String>) field.get(instance));
            }
        });

        if (VERSION.SDK_INT >= VERSION_CODES.FROYO) {
            // Bundle doesn't support CharSequence ArrayLists prior to SDK version 8
            ARRAYLIST_OTHER_CLASS_HELPERS.put(CharSequence.class, new PersistenceHelper() {

                @TargetApi(Build.VERSION_CODES.FROYO)
                @Override
                public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    field.set(instance, bundle.getCharSequenceArrayList(key));
                }

                @TargetApi(Build.VERSION_CODES.FROYO)
                @SuppressWarnings("unchecked")
                @Override
                public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    bundle.putCharSequenceArrayList(key, (ArrayList<CharSequence>) field.get(instance));
                }
            });
        }

        ARRAYLIST_OTHER_CLASS_HELPERS.put(Parcelable.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getParcelableArrayList(key));
            }

            @SuppressWarnings("unchecked")
            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putParcelableArrayList(key, (ArrayList<Parcelable>) field.get(instance));
            }
        });

        OTHER_CLASS_HELPERS.put(CharSequence.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getCharSequence(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putCharSequence(key, (CharSequence) field.get(instance));
            }
        });

        if (VERSION.SDK_INT >= VERSION_CODES.FROYO) {
            // Bundle doesn't support CharSequence arrays prior to SDK version 8
            OTHER_CLASS_HELPERS.put(CharSequence[].class, new PersistenceHelper() {

                @TargetApi(Build.VERSION_CODES.FROYO)
                @Override
                public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    field.set(instance, bundle.getCharSequenceArray(key));
                }

                @TargetApi(Build.VERSION_CODES.FROYO)
                @Override
                public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    bundle.putCharSequenceArray(key, (CharSequence[]) field.get(instance));
                }
            });
        }

        OTHER_CLASS_HELPERS.put(Parcelable.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getParcelable(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putParcelable(key, (Parcelable) field.get(instance));
            }
        });

        OTHER_CLASS_HELPERS.put(Parcelable[].class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getParcelableArray(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putParcelableArray(key, (Parcelable[]) field.get(instance));
            }
        });

        OTHER_CLASS_HELPERS.put(Serializable.class, new PersistenceHelper() {

            @Override
            public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                field.set(instance, bundle.getSerializable(key));
            }

            @Override
            public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                    throws IllegalAccessException {
                bundle.putSerializable(key, (Serializable) field.get(instance));
            }
        });

        if (Build.VERSION.SDK_INT >= 18) {
            // Bundle doesn't support IBinders prior to SDK version 18

            OTHER_CLASS_HELPERS.put(IBinder.class, new PersistenceHelper() {

                @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
                @Override
                public void restoreFromBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    field.set(instance, bundle.getBinder(key));
                }

                @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
                @Override
                public void storeInBundle(Field field, Object instance, String key, Bundle bundle)
                        throws IllegalAccessException {
                    bundle.putBinder(key, (IBinder) field.get(instance));
                }
            });
        }
    }

    /**
     * Don't allow instances.
     */
    private RetentionMagic() {
    }

    /**
     * Store all retainable fields of an Activity in a {@link Bundle}.
     * 
     * @param activity
     *            The {@link Activity}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void store(final Activity activity, final Bundle instanceState) {
        try {
            storeAndRestore(activity.getClass(), activity, instanceState, true /* store */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Store all retainable fields of a {@link Fragment} in a {@link Bundle}.
     * 
     * @param fragment
     *            The {@link Fragment}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void store(final Fragment fragment, final Bundle instanceState) {
        try {
            storeAndRestore(fragment.getClass(), fragment, instanceState, true /* store */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Store all retainable fields of a {@link android.support.v4.app.Fragment} in a {@link Bundle}.
     * 
     * @param fragment
     *            The {@link android.support.v4.app.Fragment}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void store(final android.support.v4.app.Fragment fragment, final Bundle instanceState) {
        try {
            storeAndRestore(fragment.getClass(), fragment, instanceState, true /* store */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Restore all retainable fields of an Activity from a {@link Bundle}.
     * 
     * @param activity
     *            The {@link Activity}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void restore(final Activity activity, final Bundle instanceState) {
        try {
            storeAndRestore(activity.getClass(), activity, instanceState, false /* restore */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Restore all retainable fields of a {@link Fragment} from a {@link Bundle}.
     * 
     * @param fragment
     *            The {@link Fragment}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void restore(final Fragment fragment, final Bundle instanceState) {
        try {
            storeAndRestore(fragment.getClass(), fragment, instanceState, false /* restore */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Restore all retainable fields of a {@link android.support.v4.app.Fragment} from a {@link Bundle}.
     * 
     * @param fragment
     *            The {@link android.support.v4.app.Fragment}.
     * @param instanceState
     *            The {@link Bundle} to store the state in.
     */
    public static void restore(final android.support.v4.app.Fragment fragment, final Bundle instanceState) {
        try {
            storeAndRestore(fragment.getClass(), fragment, instanceState, false /* restore */);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static void storeAndRestore(final Class<?> classInstance, final Object instance,
            final Bundle instanceState, final boolean store) throws IllegalAccessException {
        if (instanceState == null) {
            // nothing to do
            return;
        }

        Map<Field, PersistenceHelper> helperCache = CLASS_CACHE.get(classInstance);

        if (helperCache == null) {
            helperCache = new HashMap<Field, PersistenceHelper>();
            for (Field field : classInstance.getDeclaredFields()) {
                Retain retain = field.getAnnotation(Retain.class);
                if (retain != null && !ArrayList.class.isAssignableFrom(field.getType())) {
                    field.setAccessible(true);

                    String key = retain.key();
                    if (key == null || key.length() == 0) {
                        key = field.getName();
                    }

                    PersistenceHelper helper = getHelper(field.getType());
                    if (helper != null) {
                        if (store) {
                            helper.storeInBundle(field, instance, key, instanceState);
                        } else {
                            helper.restoreFromBundle(field, instance, key, instanceState);
                        }
                        helperCache.put(field, helper);
                    } else {
                        throw new UnsupportedOperationException(
                                "field of class " + field.getType().getCanonicalName() + " not supported");
                    }
                } else if (retain != null) {
                    throw new UnsupportedOperationException(
                            "@Retain does not support ArrayLists, use @RetainArrayList instead");
                } else {
                    RetainArrayList retainList = field.getAnnotation(RetainArrayList.class);
                    if (retainList != null && ArrayList.class.isAssignableFrom(field.getType())) {
                        field.setAccessible(true);
                        String key = retainList.key();
                        if (key == null || key.length() == 0) {
                            key = field.getName();
                        }

                        PersistenceHelper helper = getArrayListHelper(retainList.genericType());
                        if (helper != null) {
                            if (store) {
                                helper.storeInBundle(field, instance, key, instanceState);
                            } else {
                                helper.restoreFromBundle(field, instance, key, instanceState);
                            }
                            helperCache.put(field, helper);
                        } else {
                            throw new UnsupportedOperationException("list with generic type of "
                                    + field.getType().getCanonicalName() + " not supported");
                        }
                    } else if (retainList != null) {
                        throw new UnsupportedOperationException(
                                "@RetainArrayList supports only ArrayList fields, use @Retain instead");
                    }
                }
            }
            CLASS_CACHE.put(classInstance, helperCache);
        } else {
            for (Entry<Field, PersistenceHelper> entry : helperCache.entrySet()) {
                PersistenceHelper helper = entry.getValue();
                Field field = entry.getKey();
                if (helper != null) {
                    Retain retain = field.getAnnotation(Retain.class);
                    String key;
                    if (retain != null) {
                        key = retain.key();
                    } else {
                        key = field.getAnnotation(RetainArrayList.class).key();
                    }

                    if (key == null || key.length() == 0) {
                        key = field.getName();
                    }

                    if (store) {
                        helper.storeInBundle(field, instance, key, instanceState);
                    } else {
                        helper.restoreFromBundle(field, instance, key, instanceState);
                    }
                }
            }
        }
    }

    public static void init(final Activity activity, final SharedPreferences prefs) {
        try {
            init(activity.getClass(), activity, prefs);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void init(final Fragment fragment, final SharedPreferences prefs) {
        try {
            init(fragment.getClass(), fragment, prefs);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void init(final android.support.v4.app.Fragment fragment, final SharedPreferences prefs) {
        try {
            init(fragment.getClass(), fragment, prefs);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void init(final Activity activity, final Bundle extras) {
        try {
            init(activity.getClass(), activity, extras);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void init(final Fragment fragment, final Bundle arguments) {
        try {
            init(fragment.getClass(), fragment, arguments);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void init(final android.support.v4.app.Fragment fragment, final Bundle arguments) {
        try {
            init(fragment.getClass(), fragment, arguments);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static void persist(final Activity activity, final SharedPreferences prefs) {
        SharedPreferences.Editor editor = prefs.edit();

        persist(activity, editor);
        if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            // write out asynchronously on newer platforms
            editor.apply();
        } else {
            // use the synchronous call on older platforms
            editor.commit();
        }
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static void persist(final Fragment fragment, final SharedPreferences prefs) {
        SharedPreferences.Editor editor = prefs.edit();

        persist(fragment, editor);
        if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            // write out asynchronously on newer platforms
            editor.apply();
        } else {
            // use the synchronous call on older platforms
            editor.commit();
        }
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static void persist(final android.support.v4.app.Fragment fragment, final SharedPreferences prefs) {
        SharedPreferences.Editor editor = prefs.edit();

        persist(fragment, editor);
        if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            // write out asynchronously on newer platforms
            editor.apply();
        } else {
            // use the synchronous call on older platforms
            editor.commit();
        }
    }

    public static void persist(final Activity activity, final SharedPreferences.Editor editor) {
        try {
            persist(activity.getClass(), activity, editor);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void persist(final Fragment fragment, final SharedPreferences.Editor editor) {
        try {
            persist(fragment.getClass(), fragment, editor);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void persist(final android.support.v4.app.Fragment fragment,
            final SharedPreferences.Editor editor) {
        try {
            persist(fragment.getClass(), fragment, editor);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static void init(final Class<?> classInstance, final Object instance, final SharedPreferences prefs)
            throws IllegalAccessException {
        Map<Field, PersistenceHelper> helperCache = CLASS_CACHE.get(classInstance);

        if (helperCache == null) {
            for (Field field : classInstance.getDeclaredFields()) {
                Retain retain = field.getAnnotation(Retain.class);
                if (retain != null && !ArrayList.class.isAssignableFrom(field.getType())) {
                    if (!retain.permanent()) {
                        continue;
                    }

                    field.setAccessible(true);

                    String key = retain.key();
                    key = getTag(classInstance, retain.instanceNSField(), retain.classNS(), instance)
                            .append(key == null || key.length() == 0 ? field.getName() : key).toString();

                    PersistenceHelper helper = getHelper(field.getType());
                    if (helper != null) {
                        helper.restoreFromPreferences(field, instance, key, prefs);
                    } else {
                        throw new UnsupportedOperationException("field of class "
                                + field.getType().getCanonicalName() + " not supported for permanent storage");
                    }
                } else if (retain != null) {
                    throw new UnsupportedOperationException(
                            "@Retain does not support ArrayLists, use @RetainArrayList instead");
                }
            }
        } else {
            for (Entry<Field, PersistenceHelper> entry : helperCache.entrySet()) {
                PersistenceHelper helper = entry.getValue();
                Field field = entry.getKey();
                if (helper != null) {
                    Retain retain = field.getAnnotation(Retain.class);
                    if (retain == null || !retain.permanent()) {
                        continue;
                    }
                    String key = retain.key();
                    key = getTag(classInstance, retain.instanceNSField(), retain.classNS(), instance)
                            .append(key == null || key.length() == 0 ? field.getName() : key).toString();

                    helper.restoreFromPreferences(field, instance, key, prefs);
                }
            }
        }
    }

    private static void persist(final Class<?> classInstance, final Object instance,
            final SharedPreferences.Editor editor) throws IllegalAccessException {
        Map<Field, PersistenceHelper> helperCache = CLASS_CACHE.get(classInstance);

        if (helperCache == null) {
            for (Field field : classInstance.getDeclaredFields()) {
                Retain retain = field.getAnnotation(Retain.class);
                if (retain != null && !ArrayList.class.isAssignableFrom(field.getType())) {
                    if (!retain.permanent()) {
                        continue;
                    }

                    field.setAccessible(true);

                    String key = retain.key();
                    key = getTag(classInstance, retain.instanceNSField(), retain.classNS(), instance)
                            .append(key == null || key.length() == 0 ? field.getName() : key).toString();

                    PersistenceHelper helper = getHelper(field.getType());
                    if (helper != null) {
                        helper.storeInPreferences(field, instance, key, editor);
                    } else {
                        throw new UnsupportedOperationException("field of class "
                                + field.getType().getCanonicalName() + " not supported for permanent storage");
                    }
                } else if (retain != null) {
                    throw new UnsupportedOperationException(
                            "@Retain does not support ArrayLists, use @RetainArrayList instead");
                }
            }
        } else {
            for (Entry<Field, PersistenceHelper> entry : helperCache.entrySet()) {
                PersistenceHelper helper = entry.getValue();
                Field field = entry.getKey();
                if (helper != null) {
                    Retain retain = field.getAnnotation(Retain.class);
                    if (retain == null || !retain.permanent()) {
                        continue;
                    }

                    String key = retain.key();
                    key = getTag(classInstance, retain.instanceNSField(), retain.classNS(), instance)
                            .append(key == null || key.length() == 0 ? field.getName() : key).toString();

                    helper.storeInPreferences(field, instance, key, editor);
                }
            }
        }
    }

    private static void init(final Class<?> classInstance, final Object instance, final Bundle bundle)
            throws IllegalAccessException {
        if (bundle == null || bundle.size() == 0) {
            return;
        }

        for (Field field : classInstance.getDeclaredFields()) {
            Parameter param = field.getAnnotation(Parameter.class);
            if (param != null && !ArrayList.class.isAssignableFrom(field.getType())) {
                field.setAccessible(true);

                String key = param.key();
                if (key == null || key.length() == 0) {
                    key = field.getName();
                }

                PersistenceHelper helper = getHelper(field.getType());
                if (helper != null) {
                    helper.restoreFromBundle(field, instance, key, bundle);
                } else {
                    throw new UnsupportedOperationException("field of class " + field.getType().getCanonicalName()
                            + " not supported for initialization from a Bundle");
                }
            } else if (param != null) {
                throw new UnsupportedOperationException(
                        "@Parameter does not support ArrayLists, use @ParameterArrayList instead");
            } else {
                ParameterArrayList paramList = field.getAnnotation(ParameterArrayList.class);
                if (paramList != null && ArrayList.class.isAssignableFrom(field.getType())) {
                    field.setAccessible(true);
                    String key = paramList.value();
                    if (key == null || key.length() == 0) {
                        key = field.getName();
                    }

                    PersistenceHelper helper = getArrayListHelper(paramList.genericType());
                    if (helper != null) {
                        helper.restoreFromBundle(field, instance, key, bundle);
                    } else {
                        throw new UnsupportedOperationException("list with generic type of "
                                + field.getType().getCanonicalName() + " not supported");
                    }
                } else if (paramList != null) {
                    throw new UnsupportedOperationException(
                            "@ParameterArrayList supports only ArrayList fields, use @Parameter instead");
                }
            }

        }
    }

    private static PersistenceHelper getHelper(final Class<?> fieldType) {
        return getHelper(fieldType, FINAL_CLASS_HELPERS, OTHER_CLASS_HELPERS);
    }

    private static PersistenceHelper getArrayListHelper(final Class<?> genericArrayListType) {
        return getHelper(genericArrayListType, ARRAYLIST_FINAL_CLASS_HELPERS, ARRAYLIST_OTHER_CLASS_HELPERS);
    }

    private static PersistenceHelper getHelper(final Class<?> genericType,
            final Map<Class<?>, PersistenceHelper> finalClassHelper,
            final Map<Class<?>, PersistenceHelper> otherClassHelper) {
        PersistenceHelper result = finalClassHelper.get(genericType);
        if (result != null) {
            return result;
        }

        for (Class<?> classClass : otherClassHelper.keySet()) {
            if (classClass.isAssignableFrom(genericType)) {
                return otherClassHelper.get(classClass);
            }
        }
        return null;
    }

    private static StringBuilder getTag(final Class<?> classType, String instanceTag, String classTag,
            Object instance) throws IllegalAccessException {
        StringBuilder result = new StringBuilder(256);

        if (classTag != null && classTag.length() > 0) {
            if (classTag.length() == 1 && classTag.charAt(0) == '.') {
                try {
                    Field tagField = classType.getDeclaredField("TAG");
                    tagField.setAccessible(true);
                    result.append(tagField.get(instance).toString());
                } catch (Exception e) {
                    result.append(classType.getCanonicalName());
                }
            } else {
                result.append(classTag);
            }
            result.append('.');
        }

        if (instanceTag != null && instanceTag.length() > 0) {
            try {
                Field tagField = classType.getDeclaredField(instanceTag);
                tagField.setAccessible(true);
                Object value = tagField.get(instance);
                if (value != null) {
                    result.append(value.toString());
                    result.append('.');
                }
            } catch (NoSuchFieldException e) {
                // ignore
            } catch (SecurityException e) {
                // ignore
            }
        }

        return result;
    }
}