package com.alexvasilkov.android.commons.state; import android.os.Bundle; import android.os.Parcelable; import com.alexvasilkov.android.commons.utils.GsonHelper; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; /** * Helps saving and restoring {@link android.app.Activity} or {@link android.app.Fragment} instance state.<br/> * Only local fields marked with {@link InstanceState} annotation will be saved.<br/> * Supported fields types: boolean, boolean[], byte, byte[], char, char[], CharSequence, CharSequence[], double, * double[], float, float[], int, int[], long, long[], short, short[], String, String[], Bundle and all objects * implementing Serializable.<br/> * See also {@link #saveInstanceState(Object, android.os.Bundle)} and * {@link #restoreInstanceState(Object, android.os.Bundle)} methods. */ public class InstanceStateManager<T> { public static final String PREFIX = "instance_state:"; /** * Saving instance state of the given {@code obj} into {@code outState}.<br/> * Supposed to be called from {@link android.app.Activity#onSaveInstanceState(android.os.Bundle)} or * {@link android.app.Fragment#onSaveInstanceState(android.os.Bundle)}.<br/> * Activity or Fragment itself can be used as {@code obj} parameter. */ public static <T> Bundle saveInstanceState(T obj, Bundle outState) { if (outState == null) outState = new Bundle(); return new InstanceStateManager<T>(obj).saveState(outState); } /** * Restoring instance state from given {@code savedInstanceState} into the given {@code obj}.<br/> * Supposed to be called from {@link android.app.Activity#onCreate(android.os.Bundle)} or * {@link android.app.Fragment#onCreate(android.os.Bundle)} prior to using of local fields marked with * {@link InstanceState} annotation. */ public static <T> void restoreInstanceState(T obj, Bundle savedInstanceState) { if (savedInstanceState == null) return; new InstanceStateManager<T>(obj).restoreState(savedInstanceState); } private T mObj; private final HashMap<String, Field> mFieldsMap = new HashMap<String, Field>(); private final HashMap<String, Boolean> mIsGsonMap = new HashMap<String, Boolean>(); private final HashMap<Field, String> mKeysMap = new HashMap<Field, String>(); private InstanceStateManager(T obj) { mObj = obj; InstanceState an; String key; Class<?> clazz = obj.getClass(); while (clazz != null) { addFields(clazz.getDeclaredFields()); clazz = clazz.getSuperclass(); } } private void addFields(Field[] fields) { String key; boolean isGson; for (Field f : fields) { if (f.getAnnotation(InstanceState.class) != null) { isGson = false; } else if (f.getAnnotation(InstanceStateGson.class) != null) { if (!GsonHelper.hasGson()) throw new RuntimeException("Gson library not found for InstanceStateGson annotation"); isGson = true; } else { continue; } key = f.getName(); if (mFieldsMap.containsKey(key)) { throw new RuntimeException("Duplicate key \"" + key + "\" of InstanceState annotation"); } else { f.setAccessible(true); // removing private fields access restriction mFieldsMap.put(key, f); mIsGsonMap.put(key, isGson); mKeysMap.put(f, key); } } } private Bundle saveState(Bundle outState) { try { String key; for (Field f : mKeysMap.keySet()) { key = mKeysMap.get(f); setBundleValue(f, mObj, outState, PREFIX + key, mIsGsonMap.get(key)); } } catch (IllegalAccessException e) { throw new RuntimeException("Can't access field value", e); } return outState; } private void restoreState(Bundle savedInstanceState) { try { String key; for (Field f : mKeysMap.keySet()) { key = mKeysMap.get(f); setInstanceValue(f, mObj, savedInstanceState, PREFIX + key, mIsGsonMap.get(key)); } } catch (IllegalAccessException e) { throw new RuntimeException("Can't set field value", e); } } @SuppressWarnings("unchecked") private static void setBundleValue(Field f, Object obj, Bundle bundle, String key, boolean isGson) throws IllegalAccessException { if (isGson) { bundle.putString(key, GsonHelper.toJson(f.get(obj))); return; } Class<?> type = f.getType(); Type[] genericTypes = null; if (f.getGenericType() instanceof ParameterizedType) { genericTypes = ((ParameterizedType) f.getGenericType()).getActualTypeArguments(); } if (type.equals(Boolean.TYPE)) { bundle.putBoolean(key, f.getBoolean(obj)); } else if (type.equals(boolean[].class)) { bundle.putBooleanArray(key, (boolean[]) f.get(obj)); } else if (type.equals(Bundle.class)) { bundle.putBundle(key, (Bundle) f.get(obj)); } else if (type.equals(Byte.TYPE)) { bundle.putByte(key, f.getByte(obj)); } else if (type.equals(byte[].class)) { bundle.putByteArray(key, (byte[]) f.get(obj)); } else if (type.equals(Character.TYPE)) { bundle.putChar(key, f.getChar(obj)); } else if (type.equals(char[].class)) { bundle.putCharArray(key, (char[]) f.get(obj)); } else if (type.equals(CharSequence.class)) { bundle.putCharSequence(key, (CharSequence) f.get(obj)); } else if (type.equals(CharSequence[].class)) { bundle.putCharSequenceArray(key, (CharSequence[]) f.get(obj)); } else if (type.equals(Double.TYPE)) { bundle.putDouble(key, f.getDouble(obj)); } else if (type.equals(double[].class)) { bundle.putDoubleArray(key, (double[]) f.get(obj)); } else if (type.equals(Float.TYPE)) { bundle.putFloat(key, f.getFloat(obj)); } else if (type.equals(float[].class)) { bundle.putFloatArray(key, (float[]) f.get(obj)); } else if (type.equals(Integer.TYPE)) { bundle.putInt(key, f.getInt(obj)); } else if (type.equals(int[].class)) { bundle.putIntArray(key, (int[]) f.get(obj)); } else if (type.equals(Long.TYPE)) { bundle.putLong(key, f.getLong(obj)); } else if (type.equals(long[].class)) { bundle.putLongArray(key, (long[]) f.get(obj)); } else if (type.equals(Short.TYPE)) { bundle.putShort(key, f.getShort(obj)); } else if (type.equals(short[].class)) { bundle.putShortArray(key, (short[]) f.get(obj)); } else if (type.equals(String.class)) { bundle.putString(key, (String) f.get(obj)); } else if (type.equals(String[].class)) { bundle.putStringArray(key, (String[]) f.get(obj)); } else if (Parcelable.class.isAssignableFrom(type)) { bundle.putParcelable(key, (Parcelable) f.get(obj)); } else if (type.equals(ArrayList.class) && genericTypes[0] instanceof Class && Parcelable.class.isAssignableFrom((Class<?>) genericTypes[0])) { bundle.putParcelableArrayList(key, (ArrayList<? extends Parcelable>) f.get(obj)); } else if (type.isArray() && Parcelable.class.isAssignableFrom(type.getComponentType())) { bundle.putParcelableArray(key, (Parcelable[]) f.get(obj)); } else if (Serializable.class.isAssignableFrom(type)) { bundle.putSerializable(key, (Serializable) f.get(obj)); } else { throw new RuntimeException("Unsupported field type: " + f.getName() + ", " + type.getName()); } } private static void setInstanceValue(Field f, Object obj, Bundle bundle, String key, boolean isGson) throws IllegalArgumentException, IllegalAccessException { if (isGson) { f.set(obj, GsonHelper.fromJson(bundle.getString(key), f.getGenericType())); return; } Class<?> type = f.getType(); Type[] genericTypes = null; if (f.getGenericType() instanceof ParameterizedType) { genericTypes = ((ParameterizedType) f.getGenericType()).getActualTypeArguments(); } if (type.equals(Boolean.TYPE)) { f.setBoolean(obj, bundle.getBoolean(key)); } else if (type.equals(boolean[].class)) { f.set(obj, bundle.getBooleanArray(key)); } else if (type.equals(Bundle.class)) { f.set(obj, bundle.getBundle(key)); } else if (type.equals(Byte.TYPE)) { f.setByte(obj, bundle.getByte(key)); } else if (type.equals(byte[].class)) { f.set(obj, bundle.getByteArray(key)); } else if (type.equals(Character.TYPE)) { f.setChar(obj, bundle.getChar(key)); } else if (type.equals(char[].class)) { f.set(obj, bundle.getCharArray(key)); } else if (type.equals(CharSequence.class)) { f.set(obj, bundle.getCharSequence(key)); } else if (type.equals(CharSequence[].class)) { f.set(obj, bundle.getCharSequenceArray(key)); } else if (type.equals(Double.TYPE)) { f.setDouble(obj, bundle.getDouble(key)); } else if (type.equals(double[].class)) { f.set(obj, bundle.getDoubleArray(key)); } else if (type.equals(Float.TYPE)) { f.setFloat(obj, bundle.getFloat(key)); } else if (type.equals(float[].class)) { f.set(obj, bundle.getFloatArray(key)); } else if (type.equals(Integer.TYPE)) { f.setInt(obj, bundle.getInt(key)); } else if (type.equals(int[].class)) { f.set(obj, bundle.getIntArray(key)); } else if (type.equals(Long.TYPE)) { f.setLong(obj, bundle.getLong(key)); } else if (type.equals(long[].class)) { f.set(obj, bundle.getLongArray(key)); } else if (type.equals(Short.TYPE)) { f.setShort(obj, bundle.getShort(key)); } else if (type.equals(short[].class)) { f.set(obj, bundle.getShortArray(key)); } else if (type.equals(String.class)) { f.set(obj, bundle.getString(key)); } else if (type.equals(String[].class)) { f.set(obj, bundle.getStringArray(key)); } else if (Parcelable.class.isAssignableFrom(type)) { f.set(obj, bundle.getParcelable(key)); } else if (type.equals(ArrayList.class) && genericTypes[0] instanceof Class && Parcelable.class.isAssignableFrom((Class<?>) genericTypes[0])) { f.set(obj, bundle.getParcelableArrayList(key)); } else if (type.isArray() && Parcelable.class.isAssignableFrom(type.getComponentType())) { f.set(obj, bundle.getParcelableArray(key)); } else if (Serializable.class.isAssignableFrom(type)) { f.set(obj, bundle.getSerializable(key)); } else { throw new RuntimeException("Unsupported field type: " + f.getName() + ", " + type.getSimpleName()); } bundle.remove(key); } }