diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java index 6796b8dc69..40ae76ee7e 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java @@ -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 implements OnClickListener { +final class KMKeyboardPickerAdapter extends NestedAdapter implements OnClickListener { private final static int KEYBOARD_LAYOUT_RESOURCE = R.layout.list_row_layout3; private Context context; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java index 1546a896b4..b18288060e 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java @@ -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 { + static private class FilteredLexicalModelAdapter extends NestedAdapter { 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() { + super(context, RESOURCE, lexicalModels, new AdapterFilter() { @Override - public List selectFrom(Dataset.LexicalModels adapter) { + public List selectFrom(Dataset.LexicalModels adapter, Void dummy) { List 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; } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java index 8f7f8b23b1..c153945675 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java @@ -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 hashMap = new HashMap(); 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); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java index 17491e5b34..ea07251daa 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java @@ -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 { - public class LanguageFilter> implements AdapterFilter { - private final String lgCode; - - LanguageFilter(String lgCode) { - this.lgCode = lgCode; - } + public abstract class LanguageFilter> implements AdapterFilter { + abstract Set getSetFrom(LanguageMetadata metadata); @Override - public List selectFrom(Adapter adapter) { + public List 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 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 keyboardFilter = new LanguageFilter() { + Set getSetFrom(LanguageMetadata metadata) { + return metadata.keyboards; + } + }; + + private final LanguageFilter lexicalModelFilter = new LanguageFilter() { + Set getSetFrom(LanguageMetadata metadata) { + return metadata.lexicalModels; + } + }; + // Implements common language-tracking functionality for each internally-managed master list. private class LanguageCodedAdapter extends ArrayAdapter implements ListBacked { private final List data; @@ -99,20 +102,7 @@ public class Dataset extends ArrayAdapter { @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 { super.addAll(collection); - for(Type kbd: collection) { - ensureLanguageDatasetExists(kbd); + HashSet 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 { 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 { } } + class LanguageMetadata { + public final String name; + public final String code; + public Set keyboards = new HashSet<>(); + public Set 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 languageIndexMap = new HashMap<>(); + // Tracks keyboards and model by language tag separately from the adapters; useful for + // optimizing Adapter functionality. + private final Map 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 { 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 nestedKbds = - new NestedAdapter<>(context, 0, keyboards, new LanguageFilter(languageCode)); - NestedAdapter nestedLexicals = - new NestedAdapter<>(context, 0, lexicalModels, new LanguageFilter(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 { keyboards.notifyDataSetChanged(); lexicalModels.notifyDataSetChanged(); } + + void _notifyDataSetChanged() { + super.notifyDataSetChanged(); + } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java index 4f83ac6ba2..0ed4e7f1bf 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java @@ -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(); + } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageDataset.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageDataset.java index 249c887e38..dcbdd8dc00 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageDataset.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageDataset.java @@ -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 keyboards; - public final NestedAdapter lexicalModels; +// public final NestedAdapter keyboards; +// public final NestedAdapter lexicalModels; public final String languageName; public final String languageCode; - public LanguageDataset(NestedAdapter keyboards, - NestedAdapter lexicalModels, + public LanguageDataset(/*NestedAdapter keyboards, + NestedAdapter lexicalModels,*/ String languageName, String languageCode) { - this.keyboards = keyboards; - this.lexicalModels = lexicalModels; +// this.keyboards = keyboards; +// this.lexicalModels = lexicalModels; this.languageName = languageName; this.languageCode = languageCode; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java index d06ed848b7..2af937aa20 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java @@ -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(); + } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/AdapterFilter.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/AdapterFilter.java index 21a4148eea..3cba3874e7 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/AdapterFilter.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/AdapterFilter.java @@ -8,11 +8,11 @@ import java.util.List; * Used by NestedAdapters to selectively filter which elements are selected from the original Adapter. * @param The type of both Adapters' elements. */ -public interface AdapterFilter> { +public interface AdapterFilter, 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 selectFrom(Adapter adapter); + List selectFrom(Adapter adapter, FilterArg argument); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/NestedAdapter.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/NestedAdapter.java index f1af60d6de..6f49cc154a 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/NestedAdapter.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/NestedAdapter.java @@ -18,9 +18,9 @@ import java.util.List; * @param The shared item type of the two Adapters. * @param The type specification of the Adapter to be nested/linked. */ -public class NestedAdapter & ListBacked> extends ArrayAdapter { +public class NestedAdapter & ListBacked, FilterArg> extends ArrayAdapter { final A wrappedAdapter; - final AdapterFilter filter; + final AdapterFilter filter; final List filteredList; boolean isMutating = false; @@ -33,15 +33,17 @@ public class NestedAdapter & ListBacked * @param The type of the 'wrapped' source ArrayAdapter (possibly a subclass). * @param The type of the 'listener' NestedAdapter, complete with its generic type parameters. */ - static class WrapperObserver & ListBacked, A extends NestedAdapter> extends DataSetObserver { + static class WrapperObserver & ListBacked, A extends NestedAdapter, 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 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 & 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 & ListBacked } } - private final WrapperObserver> observer; + private final WrapperObserver, FilterArg> observer; public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter) { - this(context, resource, adapter, new AdapterFilter() { + this(context, resource, adapter, new AdapterFilter() { @Override - public List selectFrom(A adapter) { + public List selectFrom(A adapter, FilterArg dummy) { return adapter.asList(); } - }); + }, null); } - public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter filter) { - this(context, resource, adapter, filter, filter.selectFrom(adapter)); + public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter filter, FilterArg filterArg) { + this(context, resource, adapter, filter, filterArg, filter.selectFrom(adapter, filterArg)); } - public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter filter, List filteredList) { + public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter filter, FilterArg filterArg, List 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;