diff --git a/OsmAnd/src/net/osmand/plus/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/OsmandSettings.java index 4019e16c60..7c22126cdd 100644 --- a/OsmAnd/src/net/osmand/plus/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/OsmandSettings.java @@ -42,11 +42,20 @@ import net.osmand.plus.voice.CommandPlayer; import net.osmand.render.RenderingRulesStorage; import net.osmand.util.Algorithms; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -61,6 +70,8 @@ import java.util.StringTokenizer; public class OsmandSettings { + public static final int VERSION = 1; + public interface OsmandPreference { T get(); @@ -83,6 +94,16 @@ public class OsmandSettings { boolean isSet(); boolean isSetForMode(ApplicationMode m); + + boolean writeToJson(JSONObject json, ApplicationMode appMode) throws JSONException; + + void readFromJson(JSONObject json, ApplicationMode appMode) throws JSONException; + + String asString(); + + String asStringModeValue(ApplicationMode m); + + T parseString(String s); } private abstract class PreferenceWithListener implements OsmandPreference { @@ -172,6 +193,10 @@ public class OsmandSettings { registeredPreferences.put(APPLICATION_MODE.getId(), APPLICATION_MODE); } + public Map> getRegisteredPreferences() { + return Collections.unmodifiableMap(registeredPreferences); + } + private static final String SETTING_CUSTOMIZED_ID = "settings_customized"; private void setCustomized() { @@ -387,8 +412,53 @@ public class OsmandSettings { public boolean setModeValue(ApplicationMode m, ApplicationMode obj) { throw new UnsupportedOperationException(); } + + @Override + public boolean writeToJson(JSONObject json, ApplicationMode appMode) throws JSONException { + return writeAppModeToJson(json, this); + } + + @Override + public void readFromJson(JSONObject json, ApplicationMode appMode) throws JSONException { + readAppModeFromJson(json, this); + } + + @Override + public String asString() { + return appModeToString(get()); + } + + @Override + public String asStringModeValue(ApplicationMode m) { + return appModeToString(m); + } + + @Override + public ApplicationMode parseString(String s) { + return appModeFromString(s); + } }; + private String appModeToString(ApplicationMode appMode) { + return appMode.getStringKey(); + } + + private ApplicationMode appModeFromString(String s) { + return ApplicationMode.valueOfStringKey(s, ApplicationMode.DEFAULT); + } + + private boolean writeAppModeToJson(JSONObject json, OsmandPreference appModePref) throws JSONException { + json.put(appModePref.getId(), appModePref.asString()); + return true; + } + + private void readAppModeFromJson(JSONObject json, OsmandPreference appModePref) throws JSONException { + String s = json.getString(appModePref.getId()); + if (s != null) { + appModePref.set(appModePref.parseString(s)); + } + } + public ApplicationMode getApplicationMode() { return APPLICATION_MODE.get(); } @@ -469,6 +539,10 @@ public class OsmandSettings { return this; } + public boolean isGlobal() { + return global; + } + public CommonPreference cache() { cache = true; return this; @@ -587,6 +661,53 @@ public class OsmandSettings { public boolean isSetForMode(ApplicationMode mode) { return settingsAPI.contains(getProfilePreferences(mode), getId()); } + + @Override + public boolean writeToJson(JSONObject json, ApplicationMode appMode) throws JSONException { + if (appMode != null) { + if (isSetForMode(appMode)) { + json.put(getId(), asStringModeValue(appMode)); + return true; + } + } else if (global) { + if (isSet()) { + json.put(getId(), asString()); + return true; + } + } + return false; + } + + @Override + public void readFromJson(JSONObject json, ApplicationMode appMode) throws JSONException { + if (isGlobal()) { + String globalValue = json.getString(getId()); + if (globalValue != null) { + set(parseString(globalValue)); + } + } else { + Object jsonModeValuesObj = json.get(getId()); + if (jsonModeValuesObj instanceof JSONObject) { + JSONObject jsonModeValues = (JSONObject) jsonModeValuesObj; + for (ApplicationMode m : ApplicationMode.allPossibleValues()) { + String modeValue = jsonModeValues.getString(m.getStringKey()); + if (modeValue != null) { + setModeValue(m, parseString(modeValue)); + } + } + } + } + } + + @Override + public String asString() { + return get().toString(); + } + + @Override + public String asStringModeValue(ApplicationMode m) { + return getModeValue(m).toString(); + } } public class BooleanPreference extends CommonPreference { @@ -605,6 +726,11 @@ public class OsmandSettings { protected boolean setValue(Object prefs, Boolean val) { return settingsAPI.edit(prefs).putBoolean(getId(), val).commit(); } + + @Override + public Boolean parseString(String s) { + return Boolean.parseBoolean(s); + } } private class BooleanAccessibilityPreference extends BooleanPreference { @@ -645,6 +771,10 @@ public class OsmandSettings { return settingsAPI.edit(prefs).putInt(getId(), val).commit(); } + @Override + public Integer parseString(String s) { + return Integer.parseInt(s); + } } private class LongPreference extends CommonPreference { @@ -664,6 +794,10 @@ public class OsmandSettings { return settingsAPI.edit(prefs).putLong(getId(), val).commit(); } + @Override + public Long parseString(String s) { + return Long.parseLong(s); + } } private class FloatPreference extends CommonPreference { @@ -683,6 +817,10 @@ public class OsmandSettings { return settingsAPI.edit(prefs).putFloat(getId(), val).commit(); } + @Override + public Float parseString(String s) { + return Float.parseFloat(s); + } } public class StringPreference extends CommonPreference { @@ -701,6 +839,10 @@ public class OsmandSettings { return settingsAPI.edit(prefs).putString(getId(), (val != null) ? val.trim() : val).commit(); } + @Override + public String parseString(String s) { + return s; + } } public class ListStringPreference extends StringPreference { @@ -752,8 +894,6 @@ public class OsmandSettings { } return false; } - - } public class EnumIntPreference> extends CommonPreference { @@ -784,6 +924,25 @@ public class OsmandSettings { return settingsAPI.edit(prefs).putInt(getId(), val.ordinal()).commit(); } + @Override + public String asString() { + return get().name(); + } + + @Override + public String asStringModeValue(ApplicationMode m) { + return getModeValue(m).name(); + } + + @Override + public E parseString(String s) { + for (E value : values) { + if (value.name().equals(s)) { + return value; + } + } + return null; + } } ///////////// PREFERENCES classes //////////////// @@ -959,6 +1118,31 @@ public class OsmandSettings { return valueSaved; } + + @Override + public boolean writeToJson(JSONObject json, ApplicationMode appMode) throws JSONException { + return writeAppModeToJson(json, this); + } + + @Override + public void readFromJson(JSONObject json, ApplicationMode appMode) throws JSONException { + readAppModeFromJson(json, this); + } + + @Override + public String asString() { + return appModeToString(get()); + } + + @Override + public String asStringModeValue(ApplicationMode m) { + return appModeToString(m); + } + + @Override + public ApplicationMode parseString(String s) { + return appModeFromString(s); + } }; public final OsmandPreference LAST_ROUTE_APPLICATION_MODE = new CommonPreference("last_route_application_mode_backup_string", ApplicationMode.DEFAULT) { @@ -976,6 +1160,31 @@ public class OsmandSettings { protected boolean setValue(Object prefs, ApplicationMode val) { return settingsAPI.edit(prefs).putString(getId(), val.getStringKey()).commit(); } + + @Override + public boolean writeToJson(JSONObject json, ApplicationMode appMode) throws JSONException { + return writeAppModeToJson(json, this); + } + + @Override + public void readFromJson(JSONObject json, ApplicationMode appMode) throws JSONException { + readAppModeFromJson(json, this); + } + + @Override + public String asString() { + return appModeToString(get()); + } + + @Override + public String asStringModeValue(ApplicationMode m) { + return appModeToString(m); + } + + @Override + public ApplicationMode parseString(String s) { + return appModeFromString(s); + } }; public final OsmandPreference FIRST_MAP_IS_DOWNLOADED = new BooleanPreference( diff --git a/OsmAnd/src/net/osmand/plus/SettingsHelper.java b/OsmAnd/src/net/osmand/plus/SettingsHelper.java new file mode 100644 index 0000000000..b3e0dceadf --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/SettingsHelper.java @@ -0,0 +1,533 @@ +package net.osmand.plus; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import net.osmand.PlatformUtil; +import net.osmand.plus.OsmandSettings.OsmandPreference; +import net.osmand.util.Algorithms; + +import org.apache.commons.logging.Log; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class SettingsHelper { + + private static final Log LOG = PlatformUtil.getLog(SettingsHelper.class); + private static final int BUFFER = 1024; + + public enum SettingsItemType { + GLOBAL, + PROFILE, + PLUGIN, + DATA, + FILE, + } + + public abstract static class SettingsItem { + + private SettingsItemType type; + + SettingsItem(@NonNull SettingsItemType type) { + this.type = type; + } + + public SettingsItemType getType() { + return type; + } + + public abstract String getName(); + + public abstract SettingsItemReader getReader(); + + public abstract SettingsItemWriter getWriter(); + } + + public abstract static class SettingsItemReader { + + private T item; + + public SettingsItemReader(@NonNull T item) { + this.item = item; + } + + public abstract void readFromStream(@NonNull InputStream inputStream) throws IOException, IllegalArgumentException; + } + + public abstract static class SettingsItemWriter { + + private T item; + + public SettingsItemWriter(T item) { + this.item = item; + } + + public T getItem() { + return item; + } + + public abstract boolean writeToStream(@NonNull OutputStream outputStream) throws IOException; + } + + public abstract static class OsmandSettingsItem extends SettingsItem { + + private OsmandSettings settings; + + protected OsmandSettingsItem(@NonNull SettingsItemType type, @NonNull OsmandSettings settings) { + super(type); + this.settings = settings; + } + + public OsmandSettings getSettings() { + return settings; + } + } + + public abstract static class OsmandSettingsItemReader extends SettingsItemReader { + + private OsmandSettings settings; + + public OsmandSettingsItemReader(@NonNull OsmandSettingsItem item, @NonNull OsmandSettings settings) { + super(item); + this.settings = settings; + } + + protected abstract void readPreferenceFromJson(@NonNull OsmandPreference preference, + @NonNull JSONObject json) throws JSONException; + + @Override + public void readFromStream(@NonNull InputStream inputStream) throws IOException, IllegalArgumentException { + StringBuilder buf = new StringBuilder(); + try { + BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + String str; + while ((str = in.readLine()) != null) { + buf.append(str); + } + } catch (IOException e) { + throw new IllegalArgumentException("Cannot read json body", e); + } + String jsonStr = buf.toString(); + if (Algorithms.isEmpty(jsonStr)) { + throw new IllegalArgumentException("Cannot find json body"); + } + JSONObject json; + try { + json = new JSONObject(jsonStr); + } catch (JSONException e) { + throw new IllegalArgumentException("Json parse error", e); + } + Map> prefs = settings.getRegisteredPreferences(); + Iterator iter = json.keys(); + while (iter.hasNext()) { + String key = iter.next(); + OsmandPreference p = prefs.get(key); + try { + readPreferenceFromJson(p, json); + } catch (JSONException e) { + LOG.error(null, e); + } + } + } + } + + public abstract static class OsmandSettingsItemWriter extends SettingsItemWriter { + + private OsmandSettings settings; + + public OsmandSettingsItemWriter(OsmandSettingsItem item, OsmandSettings settings) { + super(item); + this.settings = settings; + } + + protected abstract void writePreferenceToJson(@NonNull OsmandPreference preference, + @NonNull JSONObject json) throws JSONException; + + @Override + public boolean writeToStream(@NonNull OutputStream outputStream) throws IOException { + JSONObject json = new JSONObject(); + Map> prefs = settings.getRegisteredPreferences(); + for (OsmandPreference pref : prefs.values()) { + try { + writePreferenceToJson(pref, json); + } catch (JSONException e) { + LOG.error(null, e); + } + } + if (json.length() > 0) { + try { + String s = json.toString(2); + outputStream.write(s.getBytes("UTF-8")); + } catch (JSONException e) { + LOG.error(null, e); + } + return true; + } + return false; + } + } + + public static class GlobalSettingsItem extends OsmandSettingsItem { + + public GlobalSettingsItem(@NonNull OsmandSettings settings) { + super(SettingsItemType.GLOBAL, settings); + } + + @Override + public String getName() { + return "global"; + } + + @Override + public SettingsItemReader getReader() { + return new OsmandSettingsItemReader(this, getSettings()) { + @Override + protected void readPreferenceFromJson(@NonNull OsmandPreference preference, @NonNull JSONObject json) throws JSONException { + preference.readFromJson(json, null); + } + }; + } + + @Override + public SettingsItemWriter getWriter() { + return new OsmandSettingsItemWriter(this, getSettings()) { + @Override + protected void writePreferenceToJson(@NonNull OsmandPreference preference, @NonNull JSONObject json) throws JSONException { + preference.writeToJson(json, null); + } + }; + } + } + + public static class ProfileSettingsItem extends OsmandSettingsItem { + + private ApplicationMode appMode; + + public ProfileSettingsItem(@NonNull OsmandSettings settings, @NonNull ApplicationMode appMode) { + super(SettingsItemType.PROFILE, settings); + this.appMode = appMode; + } + + @Override + public String getName() { + return appMode.getStringKey(); + } + + @Override + public SettingsItemReader getReader() { + return new OsmandSettingsItemReader(this, getSettings()) { + @Override + protected void readPreferenceFromJson(@NonNull OsmandPreference preference, @NonNull JSONObject json) throws JSONException { + preference.readFromJson(json, appMode); + } + }; + } + + @Override + public SettingsItemWriter getWriter() { + return new OsmandSettingsItemWriter(this, getSettings()) { + @Override + protected void writePreferenceToJson(@NonNull OsmandPreference preference, @NonNull JSONObject json) throws JSONException { + preference.writeToJson(json, appMode); + } + }; + } + } + + public abstract static class StreamSettingsItemReader extends SettingsItemReader { + + public StreamSettingsItemReader(@NonNull StreamSettingsItem item) { + super(item); + } + + } + + public static class StreamSettingsItemWriter extends SettingsItemWriter { + + public StreamSettingsItemWriter(StreamSettingsItem item) { + super(item); + } + + @Override + public boolean writeToStream(@NonNull OutputStream outputStream) throws IOException { + boolean hasData = false; + InputStream is = getItem().inputStream; + if (is != null) { + byte[] data = new byte[BUFFER]; + int count; + while ((count = is.read(data, 0, BUFFER)) != -1) { + outputStream.write(data, 0, count); + if (!hasData) { + hasData = true; + } + } + Algorithms.closeStream(is); + } + return hasData; + } + } + + public abstract static class StreamSettingsItem extends SettingsItem { + + @Nullable + private InputStream inputStream; + private String name; + + public StreamSettingsItem(@NonNull SettingsItemType type, @NonNull String name) { + super(type); + this.name = name; + } + + public StreamSettingsItem(@NonNull SettingsItemType type, @NonNull InputStream inputStream, @NonNull String name) { + super(type); + this.inputStream = inputStream; + this.name = name; + } + + @Nullable + public InputStream getInputStream() { + return inputStream; + } + + protected void setInputStream(@Nullable InputStream inputStream) { + this.inputStream = inputStream; + } + + @Override + public String getName() { + return name; + } + + @Override + public SettingsItemWriter getWriter() { + return new StreamSettingsItemWriter(this); + } + } + + public static class DataSettingsItem extends StreamSettingsItem { + + @Nullable + private byte[] data; + + public DataSettingsItem(@NonNull String name) { + super(SettingsItemType.DATA, name); + } + + public DataSettingsItem(@NonNull byte[] data, @NonNull String name) { + super(SettingsItemType.DATA, name); + this.data = data; + } + + @Nullable + public byte[] getData() { + return data; + } + + @Override + public SettingsItemReader getReader() { + return new StreamSettingsItemReader(this) { + @Override + public void readFromStream(@NonNull InputStream inputStream) throws IOException, IllegalArgumentException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[BUFFER]; + while ((nRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + + buffer.flush(); + DataSettingsItem.this.data = buffer.toByteArray(); + } + }; + } + + @Override + public SettingsItemWriter getWriter() { + setInputStream(new ByteArrayInputStream(data)); + return super.getWriter(); + } + } + + public static class FileSettingsItem extends StreamSettingsItem { + + private File file; + + public FileSettingsItem(@NonNull OsmandApplication app, @NonNull File file) { + super(SettingsItemType.FILE, file.getPath().replace(app.getAppPath(null).getPath(), "")); + this.file = file; + } + + public File getFile() { + return file; + } + + @Override + public SettingsItemReader getReader() { + return new StreamSettingsItemReader(this) { + @Override + public void readFromStream(@NonNull InputStream inputStream) throws IOException, IllegalArgumentException { + OutputStream output = new FileOutputStream(file); + byte[] buffer = new byte[BUFFER]; + int count; + while ((count = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, count); + } + output.flush(); + } + }; + } + + @Override + public SettingsItemWriter getWriter() { + try { + setInputStream(new FileInputStream(file)); + } catch (FileNotFoundException e) { + LOG.error(null, e); + } + return super.getWriter(); + } + } + + private static class SettingsItemsFactory { + + private OsmandApplication app; + + SettingsItemsFactory(OsmandApplication app) { + this.app = app; + } + + @Nullable + public SettingsItem createItem(@NonNull SettingsItemType type, @NonNull String name) { + OsmandSettings settings = app.getSettings(); + switch (type) { + case GLOBAL: + return new GlobalSettingsItem(settings); + case PROFILE: + ApplicationMode appMode = ApplicationMode.valueOfStringKey(name, null); + return appMode != null ? new ProfileSettingsItem(settings, appMode) : null; + case PLUGIN: + return null; + case DATA: + return new DataSettingsItem(name); + case FILE: + return new FileSettingsItem(app, new File(app.getAppPath(null), name)); + } + return null; + } + } + + public static class SettingsExporter { + + private Map items; + private Map additionalParams; + + public SettingsExporter() { + items = new LinkedHashMap<>(); + additionalParams = new LinkedHashMap<>(); + } + + public void addSettingsItem(SettingsItem item) throws IllegalArgumentException { + if (items.containsKey(item.getName())) { + throw new IllegalArgumentException("Already has such item: " + item.getName()); + } + items.put(item.getName(), item); + } + + public void addAdditionalParam(String key, String value) { + additionalParams.put(key, value); + } + + public void exportSettings(File zipFile) throws JSONException, IOException { + JSONObject json = new JSONObject(); + json.put("osmand_settings_version", OsmandSettings.VERSION); + for (Map.Entry param : additionalParams.entrySet()) { + json.put(param.getKey(), param.getValue()); + } + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(zipFile), BUFFER); + ZipOutputStream zos = new ZipOutputStream(bos); + try { + for (SettingsItem item : items.values()) { + ZipEntry entry = new ZipEntry(item.getName()); + entry.setExtra(item.getType().name().getBytes()); + zos.putNextEntry(entry); + item.getWriter().writeToStream(zos); + zos.closeEntry(); + } + zos.flush(); + zos.finish(); + } finally { + Algorithms.closeStream(zos); + Algorithms.closeStream(bos); + } + } + } + + public static class SettingsImporter { + + private OsmandApplication app; + private List items; + + public SettingsImporter(@NonNull OsmandApplication app) { + this.app = app; + } + + public List getItems() { + return Collections.unmodifiableList(items); + } + + public void importSettings(File zipFile) throws IllegalArgumentException, IOException { + items = new ArrayList<>(); + ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile)); + InputStream ois = new BufferedInputStream(zis); + SettingsItemsFactory itemsFactory = new SettingsItemsFactory(app); + try { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String itemTypeStr = new String(entry.getExtra()); + if (!Algorithms.isEmpty(itemTypeStr)) { + try { + SettingsItemType type = SettingsItemType.valueOf(itemTypeStr); + SettingsItem item = itemsFactory.createItem(type, entry.getName()); + if (item != null) { + item.getReader().readFromStream(ois); + items.add(item); + } + } catch (IllegalArgumentException e) { + LOG.error("Wrong SettingsItemType: " + itemTypeStr, e); + } finally { + zis.closeEntry(); + } + } + } + } catch (IOException ex) { + LOG.error(ex); + } finally { + Algorithms.closeStream(ois); + Algorithms.closeStream(zis); + } + } + } +}