/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.base.content;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.achep.base.interfaces.IBackupable;
import com.achep.base.interfaces.IOnLowMemory;
import com.achep.base.interfaces.ISubscriptable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import static com.achep.base.Build.DEBUG;
/**
* Simple list which automatically saves items to private storage and restores on initialize.
* This may be useful for implementing blacklists or something fun.
*
* @author Artem Chepurnoy
*/
public abstract class SharedList<V, T extends SharedList.Saver<V>> implements
ISubscriptable<SharedList.OnSharedListChangedListener<V>>,
IOnLowMemory, Iterable<V>, IBackupable {
private static final String TAG = "SharedList";
/**
* Key's prefix for SharedList's internal usage.
*/
private static final String KEY_PREFIX = "__";
private static final String KEY_NUMBER = KEY_PREFIX + "n";
private static final String KEY_USED_ITEM = KEY_PREFIX + "used_";
private HashMap<V, Integer> mList;
private ArrayList<Integer> mPlaceholder;
private ArrayList<OnSharedListChangedListener<V>> mListeners;
private boolean mRecyclableCreated;
private Comparator<V> mComparator;
private T mSaver;
/**
* Interface definition for a callback to be invoked
* when a shared list changed.
*
* @author Artem Chepurnoy
* @see SharedList
* @see SharedList#registerListener(SharedList.OnSharedListChangedListener)
* @see SharedList#unregisterListener(SharedList.OnSharedListChangedListener)
*/
public interface OnSharedListChangedListener<V> {
/**
* Called on object put to / replaced in the list.
*
* @param objectNew current object
* @param objectOld old object from the list
* @param diff the difference between old and new objects (provided by {@link SharedList.Comparator})
*/
void onPut(@NonNull V objectNew, @Nullable V objectOld, int diff);
/**
* Called on object removed from the list.
*
* @param objectRemoved removed object from the list
*/
void onRemoved(@NonNull V objectRemoved);
}
/**
* The provider of the diffs between "old" and "new" objects in list.
*
* @author Artem Chepurnoy
* @see OnSharedListChangedListener#onPut(Object, Object, int)
*/
public static abstract class Comparator<V> {
/**
* Compares old and new object and returns the difference between them.
*
* @return The difference between old and new objects.
*/
public abstract int compare(@NonNull V object, @Nullable V old);
}
/**
* Skeleton of the saver class which needed to store and get values
* into the {@link android.content.SharedPreferences}.
*
* @author Artem Chepurnoy
*/
// I could use Parcelable for that too.
public static abstract class Saver<V> {
/**
* Should put object's data to given shared prefs editor.
* <b>Note:</b> This should not write any values with
* a key starting with {@link #KEY_PREFIX}!
*
* @param position position of given object in list
* @see #get(android.content.SharedPreferences, int)
*/
@NonNull
public abstract SharedPreferences.Editor put(@NonNull V object,
@NonNull SharedPreferences.Editor editor,
int position);
/**
* Restores previously save Object from shared preferences.
*
* @param position position of given object in list
* @see #put(Object, android.content.SharedPreferences.Editor, int)
*/
public abstract V get(@NonNull SharedPreferences prefs, int position);
}
/**
* Note, that you must unregister your listener lately.
*
* @see #unregisterListener(SharedList.OnSharedListChangedListener)
* @see SharedList.OnSharedListChangedListener
*/
@Override
public void registerListener(@NonNull OnSharedListChangedListener<V> listener) {
mListeners.add(listener);
}
/**
* Unregisters previously registered listener.
*
* @see #registerListener(SharedList.OnSharedListChangedListener)
* @see SharedList.OnSharedListChangedListener
*/
@Override
public void unregisterListener(@NonNull OnSharedListChangedListener<V> listener) {
mListeners.remove(listener);
}
protected SharedList() { /* You must call #init(Context) later! */ }
protected SharedList(@NonNull Context context) {
init(context);
}
protected void init(@NonNull Context context) {
mList = new HashMap<>();
mPlaceholder = new ArrayList<>(3);
mListeners = new ArrayList<>(6);
createRecyclableFields();
// Restore previously saved list.
SharedPreferences prefs = getSharedPreferences(context);
final int n = prefs.getInt(KEY_NUMBER, 0);
for (int i = 0; i < n; i++) {
if (prefs.getBoolean(KEY_USED_ITEM + i, false)) {
// Create previously saved object.
V object = mSaver.get(prefs, i);
mList.put(object, i);
} else {
// This is an empty place which we can re-use
// later.
mPlaceholder.add(i);
}
}
}
@NonNull
private SharedPreferences getSharedPreferences(@NonNull Context context) {
return context.getSharedPreferences(getPreferencesFileName(), Context.MODE_PRIVATE);
}
/**
* @return the name of the shared list's file.
* @see #getSharedPreferences(android.content.Context)
*/
@NonNull
protected abstract String getPreferencesFileName();
/**
* @return Instance of saver which will save your Object to shared preferences.
* @see Saver
*/
@NonNull
protected abstract T onCreateSaver();
/**
* @return The comparator of this shared list (may be null.)
* @see OnSharedListChangedListener#onPut(Object, Object, int)
* @see #put(android.content.Context, Object)
* @see #put(android.content.Context, Object, OnSharedListChangedListener)
* @see #getComparator()
*/
@Nullable
protected Comparator<V> onCreateComparator() {
return null;
}
/**
* @return Previously created comparator.
* @see #onCreateComparator()
*/
@Nullable
public Comparator<V> getComparator() {
return mComparator;
}
protected boolean isOverwriteAllowed(@NonNull V object) {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
mRecyclableCreated = false;
// This probably won't free a lot, but
// yes, we can do it.
mComparator = null;
mSaver = null;
}
private void createRecyclableFields() {
if (mRecyclableCreated & (mRecyclableCreated = true)) return;
mComparator = onCreateComparator();
mSaver = onCreateSaver();
}
public void remove(@NonNull Context context, V object) {
remove(context, object, null);
}
public void remove(@NonNull Context context, V object, @Nullable OnSharedListChangedListener l) {
if (!mList.containsKey(object)) {
Log.w(TAG, "Tried to remove non-existing object from the list.");
return;
}
V objectRemoved = find(object);
assert objectRemoved != null; // Defined by the condition above
int pos = mList.remove(object);
// Put the position of newly removed object
// to sorted list (keeping it sorted).
int i = 0;
final int size = mPlaceholder.size();
for (; i < size; i++)
if (mPlaceholder.get(i) > pos)
break;
mPlaceholder.add(i, pos);
// Mark this item as unused, so we can restore placeholders too.
getSharedPreferences(context).edit()
.putBoolean(KEY_USED_ITEM + pos, false)
.apply();
notifyOnRemoved(objectRemoved, l);
}
@Nullable
public V put(@NonNull Context context, @NonNull V object) {
return put(context, object, null);
}
@Nullable
public V put(@NonNull Context context, @NonNull V object, @Nullable OnSharedListChangedListener l) {
boolean growUp = false;
int pos;
V old = null;
if (contains(object)) {
// This is completely useless if equality-checking
// method had been implemented correctly (content truly equals).
if (!isOverwriteAllowed(object)) {
if (DEBUG) Log.w(TAG, "Trying to put an existing object to the shared list.");
return null; // Do nothing.
}
// Search for an old object...
old = find(object);
// Remember the position of old object
// and pop it out.
pos = mList.get(old);
mList.remove(old);
} else {
// Increase the size of the list if there no
// empty place, that we can use.
growUp = mPlaceholder.size() == 0;
// Get where-to-save this object.
if (!growUp) {
pos = mPlaceholder.get(0);
mPlaceholder.remove(0);
} else {
pos = mList.size();
}
}
mList.put(object, pos);
createRecyclableFields();
// Save object to internal memory.
SharedPreferences.Editor editor = mSaver
.put(object, getSharedPreferences(context).edit(), pos)
.putBoolean(KEY_USED_ITEM + pos, true);
if (growUp) editor.putInt(KEY_NUMBER, mList.size());
editor.apply();
notifyOnPut(object, old, l);
return old;
}
@Nullable
private V find(@NonNull V object) {
for (V o : mList.keySet()) {
if (o.equals(object)) {
return o;
}
}
return null;
}
/**
* Notifies {@link #registerListener(OnSharedListChangedListener) registered} listeners
* about removed from list object.
*
* @param objectRemoved removed object from the list
* @param l Listener that will be ignored while notifying.
*/
protected void notifyOnRemoved(@NonNull V objectRemoved,
@Nullable OnSharedListChangedListener l) {
for (OnSharedListChangedListener<V> listener : mListeners) {
if (listener == l) continue;
listener.onRemoved(objectRemoved);
}
}
/**
* Notifies {@link #registerListener(OnSharedListChangedListener) registered} listeners
* that list got one more item / or one item is overwritten.
*
* @param object new object
* @param old old object from the list
* @param l Listener that will be ignored while notifying.
*/
protected void notifyOnPut(V object, V old, @Nullable OnSharedListChangedListener l) {
createRecyclableFields();
int diff = mComparator != null ? mComparator.compare(object, old) : 0;
for (OnSharedListChangedListener<V> listener : mListeners) {
if (listener == l) continue;
listener.onPut(object, old, diff);
}
}
/**
* Returns whether this list contains the specified object.
*
* @return {@code true} if this list contains the specified object, {@code true} otherwise.
*/
public boolean contains(@Nullable V object) {
return mList.containsKey(object);
}
@NonNull
public Set<V> values() {
return mList.keySet();
}
/**
* {@inheritDoc}
*/
@Override
public Iterator<V> iterator() {
return values().iterator();
}
//-- BACKUP ---------------------------------------------------------------
@Override
@Nullable
public String toBackupText() {
return null;
}
@Override
public boolean fromBackupText(@NonNull Context context, @NonNull String input) {
return false;
}
}