/*
* Copyright 2016 Google Inc.
*
* 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 com.google.android.libraries.remixer;
import com.google.android.libraries.remixer.sync.LocalValueSyncing;
import com.google.android.libraries.remixer.sync.SynchronizationMechanism;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Contains a list of {@link Variable}s.
*
* <p>The Remixer object is the heart and soul of Remixer as a framework, it coordinates syncing and
* value changes.
*
* <p>For value and data syncing/persistence (both locally across activities, and persisting/saving
* to cloud backends, etc) and keeping a global state, the Remixer object relies on a {@link
* SynchronizationMechanism} set at initialization time (usually in your Application.onCreate()).
*
* <p>If you do not set a SynchronizationMechanism, remixer will use {@link LocalValueSyncing},
* which will not persist any data but will synchronize across activities.
*/
public class Remixer {
private static Remixer instance;
/**
* Datatypes keyed by their serializable name.
*/
private static Map<String, DataType> registeredDataTypes = new HashMap<>();
/**
* This is a map of Remixer keys to a list of variables that have that key.
*
* <p>There may be several variables for the same key because the key can be reused in
* different activities and the value has to be shared across those.
*/
private Map<String, List<Variable>> keyMap;
/**
* This is a map of contexts to a list of variables for the given context.
*/
private Map<Object, List<Variable>> contextMap;
/**
* The synchronization mechanism used to keep values in sync across different instances of the
* variables and save/sync to other devices.
*/
private SynchronizationMechanism synchronizationMechanism;
/**
* Gets the singleton for Remixer.
*
* <p><b>Note this operation is not thread safe and should only be called from the main
* thread.</b>
*/
public static Remixer getInstance() {
if (instance == null) {
instance = new Remixer();
}
return instance;
}
/**
* Register a new data type that can be used with Remixer.
*/
public static void registerDataType(DataType dataType) {
if (registeredDataTypes.containsKey(dataType.getName())) {
throw new IllegalStateException("Adding a data type that has already been added, name: "
+ dataType.getName() );
}
registeredDataTypes.put(dataType.getName(), dataType);
}
public static DataType getDataType(String name) {
return registeredDataTypes.get(name);
}
public static Collection<DataType> getRegisteredDataTypes() {
return registeredDataTypes.values();
}
/**
* Visible only for testing. Do not use.
*/
public static void clearRegisteredDataTypes() {
registeredDataTypes.clear();
}
/**
* Visible only for testing. Users should only use {@link #getInstance()}.
*/
public Remixer() {
keyMap = new HashMap<>();
contextMap = new HashMap<>();
synchronizationMechanism = new LocalValueSyncing();
synchronizationMechanism.setRemixerInstance(this);
}
/**
* Set the synchronization mechanism to keep variables in sync locally and (possibly) with
* external sources.
*
* <p>Remixer relies on a SynchronizationMechanism instance to be the source of truth of the
* values and configuration.
*/
public void setSynchronizationMechanism(SynchronizationMechanism synchronizationMechanism) {
if (this.synchronizationMechanism != null) {
this.synchronizationMechanism.setRemixerInstance(null);
}
this.synchronizationMechanism = synchronizationMechanism;
if (synchronizationMechanism != null) {
synchronizationMechanism.setRemixerInstance(this);
}
}
public SynchronizationMechanism getSynchronizationMechanism() {
return synchronizationMechanism;
}
/**
* This adds a {@link Variable} to be tracked and displayed.
* Checks that the variable is compatible with the existing variables with the same key.
*
* <p>This method also removes old variables whose contexts have been reclaimed by the garbage
* collector which are being replaced by items from the same class of context. No items are
* removed until equivalent ones from the same context class are added to replace them. This
* guarantees that no incompatible items for the same key are ever accepted.
*
* @param variable The variable to be added. It must have a context object otherwise it
* will never be displayed, and thus not be editable.
* @throws IncompatibleRemixerItemsWithSameKeyException Other items with the same key have been
* added other contexts with incompatible types.
* @throws DuplicateKeyException Another item with the same key was added for the same context.
*/
@SuppressWarnings("unchecked")
public void addItem(Variable variable) {
if (!registeredDataTypes.containsKey(variable.getDataType().getName())) {
throw new IllegalStateException(String.format(
Locale.getDefault(),
"There is no registered data type that matches %s. Are you sure you ran "
+ "RemixerInitialization.initRemixer in your application class? See the Remixer README "
+ "for detailed instructions. If this is a custom data type you have to manually add it.",
variable.getDataType().getName()));
}
List<Variable> listForKey = getOrCreateVariableList(variable.getKey(), keyMap);
for (Variable existingItem : listForKey) {
if (variable.getContext() != null
&& variable.getContext() == existingItem.getContext()) {
// An object with the same key for the same parent object, this shouldn't happen so throw
// an exception.
throw new DuplicateKeyException(
String.format(
Locale.getDefault(),
"Duplicate key %s being used in class %s",
variable.getKey(),
existingItem.getContext().getClass().getCanonicalName()
));
}
}
if (synchronizationMechanism != null) {
// Notify the synchronization mechanism, which will take care of keeping the values in sync
// and checking compatibility.
synchronizationMechanism.onAddingVariable(variable);
}
variable.setRemixer(this);
listForKey.add(variable);
getOrCreateVariableList(variable.getContext(), contextMap).add(variable);
}
/**
* Gets the list of items that have the given key.
*/
public List<Variable> getVariablesWithKey(String key) {
return keyMap.get(key);
}
/**
* Gets all the variables associated with {@code context}. {@code context} is expected to be
* an Activity, it is Object here because remixer_core cannot depend on the Android SDK.
*/
public List<Variable> getVariablesWithContext(Object context) {
return contextMap.get(context);
}
/**
* Gets a list of RemixerItems for the given {@code key} from the {@code map} passed in, if such a
* mapping does not exist, it adds a mapping to a new empty list.
*/
private static <T> List<Variable> getOrCreateVariableList(
T key, Map<T, List<Variable>> map) {
List<Variable> list = null;
if (map.containsKey(key)) {
list = map.get(key);
} else {
list = new ArrayList<>();
map.put(key, list);
}
return list;
}
/**
* Notifies the synchronization mechanism that this variable's value has changed.
*/
void onValueChanged(Variable variable) {
synchronizationMechanism.onValueChanged(variable);
}
/**
* Removes variables whose context is {@code activity}. This makes sure {@code activity}
* doesn't leak through their callbacks.
*/
public void onActivityDestroyed(Object activity) {
if (contextMap.containsKey(activity)) {
for (Variable variable : contextMap.get(activity)) {
List<Variable> listForKey = getVariablesWithKey(variable.getKey());
listForKey.remove(variable);
if (listForKey.size() == 0) {
keyMap.remove(variable.getKey());
}
}
contextMap.remove(activity);
}
}
}