/** * * Funf: Open Sensing Framework * Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland. * Acknowledgments: Alan Gardner * Contact: nadav@media.mit.edu * * This file is part of Funf. * * Funf is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Funf is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Funf. If not, see <http://www.gnu.org/licenses/>. * */ package edu.mit.media.funf.util; import static edu.mit.media.funf.util.LogUtil.TAG; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.WeakHashMap; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.util.Log; /** * A convenience class to make sure that writes happen in a separate thread, * but data is changed in the SharedPreferences object immediately. * * Listeners still receive notifications when disk storage is updated. * * @author alangardner * */ public class AsyncSharedPrefs implements SharedPreferences, OnSharedPreferenceChangeListener { private static final Object mContent = new Object(); private final Map<String,Object> mMap; private final SharedPreferences prefs; private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners; private AsyncSharedPrefs(SharedPreferences prefs) { this.prefs = prefs; this.mMap = new HashMap<String, Object>(); this.mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); prefs.registerOnSharedPreferenceChangeListener(this); this.mMap.putAll(prefs.getAll()); } private static final Map<SharedPreferences, AsyncSharedPrefs> instances = new HashMap<SharedPreferences, AsyncSharedPrefs>(); public static AsyncSharedPrefs async(SharedPreferences prefs) { AsyncSharedPrefs asyncPrefs = instances.get(prefs); if (asyncPrefs == null) { synchronized (instances) { // Check one more time when we are synchronized asyncPrefs = instances.get(prefs); if (asyncPrefs == null) { asyncPrefs = new AsyncSharedPrefs(prefs); instances.put(prefs, asyncPrefs); } } } return asyncPrefs; } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Set<OnSharedPreferenceChangeListener> listeners = new HashSet<OnSharedPreferenceChangeListener>(); synchronized (this) { if (prefs.contains(key)) { mMap.put(key, sharedPreferences.getAll().get(key)); } else { mMap.remove(key); } listeners.addAll(mListeners.keySet()); } for (OnSharedPreferenceChangeListener listener : listeners) { if (listener != null) { listener.onSharedPreferenceChanged(this, key); } } } public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.put(listener, mContent); } } public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.remove(listener); } } public Map<String, ?> getAll() { synchronized(this) { //noinspection unchecked return new HashMap<String, Object>(mMap); } } public String getString(String key, String defValue) { synchronized (this) { String v = (String)mMap.get(key); return v != null ? v : defValue; } } public int getInt(String key, int defValue) { synchronized (this) { Integer v = (Integer)mMap.get(key); return v != null ? v : defValue; } } public long getLong(String key, long defValue) { synchronized (this) { Long v = (Long)mMap.get(key); return v != null ? v : defValue; } } public float getFloat(String key, float defValue) { synchronized (this) { Float v = (Float)mMap.get(key); return v != null ? v : defValue; } } public boolean getBoolean(String key, boolean defValue) { synchronized (this) { Boolean v = (Boolean)mMap.get(key); return v != null ? v : defValue; } } public boolean contains(String key) { synchronized (this) { return mMap.containsKey(key); } } public Editor edit() { return new AsyncEditorImpl(); } public final class AsyncEditorImpl implements SharedPreferences.Editor { private final Map<String, Object> mModified = new HashMap<String, Object>(); private boolean mClear = false; private SharedPreferences.Editor editor = prefs.edit(); public Editor putString(String key, String value) { synchronized (this) { mModified.put(key, value); editor.putString(key, value); return this; } } public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); editor.putInt(key, value); return this; } } public Editor putLong(String key, long value) { synchronized (this) { mModified.put(key, value); editor.putLong(key, value); return this; } } public Editor putFloat(String key, float value) { synchronized (this) { mModified.put(key, value); editor.putFloat(key, value); return this; } } public Editor putBoolean(String key, boolean value) { synchronized (this) { mModified.put(key, value); editor.putBoolean(key, value); return this; } } public Editor remove(String key) { synchronized (this) { mModified.put(key, this); editor.remove(key); return this; } } public Editor clear() { synchronized (this) { mClear = true; editor.clear(); return this; } } @Override public boolean commit() { synchronized (AsyncSharedPrefs.this) { synchronized (this) { if (mClear) { if (!mMap.isEmpty()) { mMap.clear(); } mClear = false; } for (Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); if (v == this) { // magic value for a removal mutation mMap.remove(k); } else { mMap.put(k, v); } } mModified.clear(); } } new Thread(new Runnable() { @Override public void run() { boolean success = editor.commit(); if (!success) { Log.w(TAG, "AsyncSharedPrefs failed to commit changes to disk. Rolling back."); synchronized (AsyncSharedPrefs.this) { // Reset to prefs mMap.clear(); mMap.putAll(prefs.getAll()); } } } }).start(); return true; } } // STATIC apply private static final Method applyMethod = getApplyMethod(); private static Method getApplyMethod() { try { return SharedPreferences.Editor.class.getMethod("apply"); } catch (NoSuchMethodException e) { Log.i(TAG, "Apply method does not exist, using async commit."); } return null; } /** * Asynchronous commit of shared preferences values * @param editor */ public static void apply(final SharedPreferences.Editor editor) { // Use the apply method if it exists try { applyMethod.invoke(editor); return; } catch (InvocationTargetException unused) { // fall through } catch (IllegalAccessException unused) { // fall through } // Commit if for some reason using apply does not work // No apply method, spin up thread to commit // TODO: we should have only one thread committing, and the ability to retry if the commit fails. new Thread(new Runnable() { @Override public void run() { editor.commit(); } }).start(); } }