Partially addressing performance issues

This commit is contained in:
jahorton 2019-06-11 09:47:49 +07:00
parent 2843672e07
commit 2ebd3dbaa2
9 changed files with 165 additions and 77 deletions

View file

@ -22,7 +22,7 @@ import com.tavultesoft.kmea.data.Dataset;
import com.tavultesoft.kmea.data.Keyboard;
import com.tavultesoft.kmea.data.adapters.NestedAdapter;
final class KMKeyboardPickerAdapter extends NestedAdapter<Keyboard, Dataset.Keyboards> implements OnClickListener {
final class KMKeyboardPickerAdapter extends NestedAdapter<Keyboard, Dataset.Keyboards, Void> implements OnClickListener {
private final static int KEYBOARD_LAYOUT_RESOURCE = R.layout.list_row_layout3;
private Context context;

View file

@ -202,15 +202,15 @@ public final class ModelPickerActivity extends AppCompatActivity {
// Uses the repo dataset's master lexical model list to create a filtered adapter for use here.
// As this one is specific to this class, we can implement Activity-specific functionality within it.
static private class FilteredLexicalModelAdapter extends NestedAdapter<LexicalModel, Dataset.LexicalModels> {
static private class FilteredLexicalModelAdapter extends NestedAdapter<LexicalModel, Dataset.LexicalModels, Void> {
static final int RESOURCE = R.layout.models_list_row_layout;
private final Context context;
public FilteredLexicalModelAdapter(@NonNull Context context, Dataset.LexicalModels lexicalModels, final String languageCode) {
// Goal: to not need a custom filter here, instead relying on LanguageDataset's built-in filters.
super(context, RESOURCE, lexicalModels, new AdapterFilter<LexicalModel, Dataset.LexicalModels>() {
super(context, RESOURCE, lexicalModels, new AdapterFilter<LexicalModel, Dataset.LexicalModels, Void>() {
@Override
public List<LexicalModel> selectFrom(Dataset.LexicalModels adapter) {
public List<LexicalModel> selectFrom(Dataset.LexicalModels adapter, Void dummy) {
List<LexicalModel> list = new ArrayList<>();
// Highly unoptimized version: O(n^2).
@ -222,7 +222,7 @@ public final class ModelPickerActivity extends AppCompatActivity {
return list;
}
});
}, null);
this.context = context;
}

View file

@ -262,9 +262,10 @@ public class CloudRepository {
String kbVersion = keyboardJSON.optString(KMManager.KMKey_KeyboardVersion, "1.0");
String kbFont = keyboardJSON.optString(KMManager.KMKey_Font, "");
String kbKey = String.format("%s_%s", langID, kbID);
//String kbKey = String.format("%s_%s", langID, kbID);
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put(KMManager.KMKey_KeyboardName, kbName);
hashMap.put(KMManager.KMKey_KeyboardID, kbID);
hashMap.put(KMManager.KMKey_LanguageName, langName);
hashMap.put(KMManager.KMKey_LanguageID, langID);
hashMap.put(KMManager.KMKey_KeyboardVersion, kbVersion);

View file

@ -12,38 +12,41 @@ import com.tavultesoft.kmea.data.adapters.ListBacked;
import com.tavultesoft.kmea.data.adapters.NestedAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Dataset extends ArrayAdapter<LanguageDataset> {
public class LanguageFilter<Type extends LanguageCoded, Adapter extends LanguageCodedAdapter<Type>> implements AdapterFilter<Type, Adapter> {
private final String lgCode;
LanguageFilter(String lgCode) {
this.lgCode = lgCode;
}
public abstract class LanguageFilter<Type extends LanguageCoded, Adapter extends LanguageCodedAdapter<Type>> implements AdapterFilter<Type, Adapter, String> {
abstract Set<Type> getSetFrom(LanguageMetadata metadata);
@Override
public List<Type> selectFrom(Adapter adapter) {
public List<Type> selectFrom(Adapter adapter, String lgCode) {
// Since we know that the adapter is connected to the master Dataset, we can pre-filter and optimize
// performance of our linked adapters.
LanguageMetadata metadata = Dataset.this.languageMetadata.get(lgCode);
List<Type> list = new ArrayList<>();
// Highly unoptimized version: O(n^2).
for(Type elem: adapter.asList()) {
if(elem.getLanguageCode().equals(lgCode)) {
list.add(elem);
}
}
return list;
return new ArrayList<>(getSetFrom(metadata));
}
}
private final LanguageFilter<Keyboard, Keyboards> keyboardFilter = new LanguageFilter<Keyboard, Keyboards>() {
Set<Keyboard> getSetFrom(LanguageMetadata metadata) {
return metadata.keyboards;
}
};
private final LanguageFilter<LexicalModel, LexicalModels> lexicalModelFilter = new LanguageFilter<LexicalModel, LexicalModels>() {
Set<LexicalModel> getSetFrom(LanguageMetadata metadata) {
return metadata.lexicalModels;
}
};
// Implements common language-tracking functionality for each internally-managed master list.
private class LanguageCodedAdapter<Type extends LanguageCoded> extends ArrayAdapter<Type> implements ListBacked<Type> {
private final List<Type> data;
@ -99,20 +102,7 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
@Override
public void addAll(Type... items) {
boolean notify = doNotify; // Save initial value.
if(notify) {
Dataset.this.setNotifyOnChange(false);
}
super.addAll(items);
for(Type kbd: items) {
ensureLanguageDatasetExists(kbd);
}
if(notify) {
Dataset.this.notifyDataSetChanged();
}
this.addAll(Arrays.asList(items));
}
@Override
@ -124,8 +114,30 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
super.addAll(collection);
for(Type kbd: collection) {
ensureLanguageDatasetExists(kbd);
HashSet<String> mutatedLanguages = new HashSet<>();
for(Type item: collection) {
LanguageMetadata data = getMetadataFor(item);
if(item instanceof Keyboard) {
if(!data.keyboards.contains(item)) {
data.keyboards.add((Keyboard) item);
}
} else if(item instanceof LexicalModel) {
if(!data.lexicalModels.contains(item)) {
data.lexicalModels.add((LexicalModel) item);
}
} else {
continue;
}
mutatedLanguages.add(data.code);
}
// Ensures that the LanguageDataset is only constructed after we've already analyzed the keyboards
// and determined the language sets.
for(String lgCode: mutatedLanguages) {
ensureLanguageDatasetExists(Dataset.this.languageMetadata.get(lgCode).name, lgCode);
}
if(notify) {
@ -187,6 +199,10 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
super.notifyDataSetChanged();
}
void _notifyDataSetChanged() {
super.notifyDataSetChanged();
}
// Override other class methods (following 'Decorator' pattern) to facilitate data tracking as needed.
}
@ -202,13 +218,31 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
}
}
class LanguageMetadata {
public final String name;
public final String code;
public Set<Keyboard> keyboards = new HashSet<>();
public Set<LexicalModel> lexicalModels = new HashSet<>();
public LanguageMetadata(String name, String code) {
this.name = name;
this.code = code;
}
}
// Stores internal adapters for keyboard, lexical model listings
public final Keyboards keyboards;
public final LexicalModels lexicalModels;
private Context context;
// Maps the relevant adapters for UI usage.
private final Map<String, LanguageDataset> languageIndexMap = new HashMap<>();
// Tracks keyboards and model by language tag separately from the adapters; useful for
// optimizing Adapter functionality.
private final Map<String, LanguageMetadata> languageMetadata = new HashMap<>();
// 'Items' are language-specific datasets holding their own language-specific adapters.
public Dataset(@NonNull Context context) {
@ -223,37 +257,52 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
String lgCode = coded.getLanguageCode();
LanguageDataset lgData = languageIndexMap.get(lgCode);
if(lgData.keyboards.getCount() == 0 && lgData.lexicalModels.getCount() == 0) {
// We're no longer storing anything for this language - remove lgData from tracking.
this.remove(lgData);
languageIndexMap.remove(lgCode);
}
// if(lgData.keyboards.getCount() == 0 && lgData.lexicalModels.getCount() == 0) {
// // We're no longer storing anything for this language - remove lgData from tracking.
// this.remove(lgData);
// languageIndexMap.remove(lgCode);
// }
}
protected void ensureLanguageDatasetExists(LanguageCoded coded) {
String lgCode = coded.getLanguageCode();
protected LanguageMetadata getMetadataFor(LanguageCoded coded) {
final String lgCode = coded.getLanguageCode();
LanguageMetadata data = Dataset.this.languageMetadata.get(lgCode);
if(data == null) {
data = new LanguageMetadata(coded.getLanguageName(), coded.getLanguageCode());
Dataset.this.languageMetadata.put(lgCode, data);
}
return data;
}
protected void ensureLanguageDatasetExists(String lgName, String lgCode) {
LanguageDataset lgData = languageIndexMap.get(lgCode);
if(lgData == null) {
lgData = constructLanguageDataset(coded.getLanguageName(), lgCode);
lgData = constructLanguageDataset(lgName, lgCode);
languageIndexMap.put(lgCode, lgData);
Dataset.this.add(lgData);
}
}
protected LanguageDataset constructLanguageDataset(String languageName, String languageCode) {
// Construct nested adapters for the individual language datasets.
NestedAdapter<Keyboard, Keyboards> nestedKbds =
new NestedAdapter<>(context, 0, keyboards, new LanguageFilter<Keyboard, Keyboards>(languageCode));
NestedAdapter<LexicalModel, LexicalModels> nestedLexicals =
new NestedAdapter<>(context, 0, lexicalModels, new LanguageFilter<LexicalModel, LexicalModels>(languageCode));
protected void ensureLanguageDatasetExists(LanguageCoded coded) {
this.ensureLanguageDatasetExists(coded.getLanguageName(), coded.getLanguageCode());
}
return new LanguageDataset(nestedKbds, nestedLexicals, languageName, languageCode);
protected LanguageDataset constructLanguageDataset(String languageName, String languageCode) {
// Building a large set of nested adapters seriously drains performance.
// FIXME: replace LanguageDatasets with the current LanguageMetadata - we want to work from that.
return new LanguageDataset(/*nestedKbds, nestedLexicals,*/ languageName, languageCode);
}
public void clear() {
keyboards.clear();
lexicalModels.clear();
this.languageIndexMap.clear();
this.languageMetadata.clear();
}
// TODO: Do we need to override any of this class's menbers in case someone SOMEHOW constructs
@ -272,4 +321,8 @@ public class Dataset extends ArrayAdapter<LanguageDataset> {
keyboards.notifyDataSetChanged();
lexicalModels.notifyDataSetChanged();
}
void _notifyDataSetChanged() {
super.notifyDataSetChanged();
}
}

View file

@ -35,4 +35,22 @@ public class Keyboard implements Serializable, LanguageCoded {
public String getLanguageName() {
return this.map.get(KMManager.KMKey_LanguageName);
}
public boolean equals(Object obj) {
if(obj instanceof Keyboard) {
boolean lgCodeMatch = ((Keyboard) obj).getLanguageCode().equals(this.getLanguageCode());
boolean idMatch = ((Keyboard) obj).getId().equals(this.getId());
return lgCodeMatch && idMatch;
}
return false;
}
@Override
public int hashCode() {
String id = getId();
String lgCode = getLanguageCode();
return id.hashCode() * lgCode.hashCode();
}
}

View file

@ -8,21 +8,19 @@ import com.tavultesoft.kmea.data.adapters.AdapterFilter;
import com.tavultesoft.kmea.data.adapters.NestedAdapter;
public class LanguageDataset {
// The following adapters should actually be CUSTOM subclassed versions, not normal ArrayAdapters.
// See https://guides.codepath.com/android/Using-an-ArrayAdapter-with-ListView, "Using a Custom ArrayAdapter".
public final NestedAdapter<Keyboard, Dataset.Keyboards> keyboards;
public final NestedAdapter<LexicalModel, Dataset.LexicalModels> lexicalModels;
// public final NestedAdapter<Keyboard, Dataset.Keyboards, String> keyboards;
// public final NestedAdapter<LexicalModel, Dataset.LexicalModels, String> lexicalModels;
public final String languageName;
public final String languageCode;
public LanguageDataset(NestedAdapter<Keyboard, Dataset.Keyboards> keyboards,
NestedAdapter<LexicalModel, Dataset.LexicalModels> lexicalModels,
public LanguageDataset(/*NestedAdapter<Keyboard, Dataset.Keyboards, String> keyboards,
NestedAdapter<LexicalModel, Dataset.LexicalModels, String> lexicalModels,*/
String languageName,
String languageCode) {
this.keyboards = keyboards;
this.lexicalModels = lexicalModels;
// this.keyboards = keyboards;
// this.lexicalModels = lexicalModels;
this.languageName = languageName;
this.languageCode = languageCode;

View file

@ -36,4 +36,20 @@ public class LexicalModel implements Serializable, LanguageCoded{
public String getLanguageName() {
return this.map.get(KMManager.KMKey_LanguageName);
}
public boolean equals(Object obj) {
if(obj instanceof LexicalModel) {
boolean lgCodeMatch = ((LexicalModel) obj).getLanguageCode().equals(this.getLanguageCode());
boolean idMatch = ((LexicalModel) obj).getId().equals(this.getId());
return lgCodeMatch && idMatch;
}
return false;
}
@Override
public int hashCode() {
return getId().hashCode() * getLanguageCode().hashCode();
}
}

View file

@ -8,11 +8,11 @@ import java.util.List;
* Used by NestedAdapters to selectively filter which elements are selected from the original Adapter.
* @param <Element> The type of both Adapters' elements.
*/
public interface AdapterFilter<Element, Adapter extends ArrayAdapter<Element>> {
public interface AdapterFilter<Element, Adapter extends ArrayAdapter<Element>, FilterArg> {
/**
* Filters the specified adapter for matching elements.
* @param adapter An existing adapter with elements, some of which should be excluded by this filter.
* @return A List of all elements of the adapter that match the filter's conditions.
*/
List<Element> selectFrom(Adapter adapter);
List<Element> selectFrom(Adapter adapter, FilterArg argument);
}

View file

@ -18,9 +18,9 @@ import java.util.List;
* @param <Element> The shared item type of the two Adapters.
* @param <A> The type specification of the Adapter to be nested/linked.
*/
public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked<Element>> extends ArrayAdapter<Element> {
public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked<Element>, FilterArg> extends ArrayAdapter<Element> {
final A wrappedAdapter;
final AdapterFilter<Element, A> filter;
final AdapterFilter<Element, A, FilterArg> filter;
final List<Element> filteredList;
boolean isMutating = false;
@ -33,15 +33,17 @@ public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked
* @param <S> The type of the 'wrapped' source ArrayAdapter (possibly a subclass).
* @param <A> The type of the 'listener' NestedAdapter, complete with its generic type parameters.
*/
static class WrapperObserver<E, S extends ArrayAdapter<E> & ListBacked<E>, A extends NestedAdapter<E, S>> extends DataSetObserver {
static class WrapperObserver<E, S extends ArrayAdapter<E> & ListBacked<E>, A extends NestedAdapter<E, S, F>, F> extends DataSetObserver {
// By being static and using a WeakReference here, we avoid memory leaks that would otherwise
// prevent our owner from being GC'd.
private WeakReference<A> listenerRef;
private S source;
private final F filterArg;
WrapperObserver(A listener, S source) {
WrapperObserver(A listener, S source, F filterArg) {
this.listenerRef = new WeakReference<>(listener);
this.source = source;
this.filterArg = filterArg;
}
@Override
@ -57,7 +59,7 @@ public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked
}
listener.setNotifyOnChange(false); // Disable event notifications temporarily.
listener._internalClear();;
listener._internalAddAll(listener.filter.selectFrom(listener.wrappedAdapter));
listener._internalAddAll(listener.filter.selectFrom(listener.wrappedAdapter, this.filterArg));
listener.notifyDataSetChanged(); // Re-enables events and signals that we did change.
}
@ -73,26 +75,26 @@ public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked
}
}
private final WrapperObserver<Element, A, NestedAdapter<Element, A>> observer;
private final WrapperObserver<Element, A, NestedAdapter<Element, A, FilterArg>, FilterArg> observer;
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter) {
this(context, resource, adapter, new AdapterFilter<Element, A>() {
this(context, resource, adapter, new AdapterFilter<Element, A, FilterArg>() {
@Override
public List<Element> selectFrom(A adapter) {
public List<Element> selectFrom(A adapter, FilterArg dummy) {
return adapter.asList();
}
});
}, null);
}
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A> filter) {
this(context, resource, adapter, filter, filter.selectFrom(adapter));
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A, FilterArg> filter, FilterArg filterArg) {
this(context, resource, adapter, filter, filterArg, filter.selectFrom(adapter, filterArg));
}
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A> filter, List<Element> filteredList) {
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A, FilterArg> filter, FilterArg filterArg, List<Element> filteredList) {
super(context, resource, filteredList);
this.wrappedAdapter = adapter;
observer = new WrapperObserver<>(this, adapter);
observer = new WrapperObserver<>(this, adapter, filterArg);
this.wrappedAdapter.registerDataSetObserver(observer);
this.filter = filter;