From c7ff8cf11739ba7f56acda5b4b38fabe32b0a364 Mon Sep 17 00:00:00 2001 From: opoto Date: Mon, 20 Jan 2020 01:29:18 +0100 Subject: [PATCH] In File and Dropbox synchronizers, add options to export to GPX, or TCX, or both. Ensure at least one option is selected. Remove usage of File's format field AUTH_CONFIG, use the ACCOUNT.FORMAT field present in all synchronizers. Migrate DB to copy format from AUTH_CONFIG to ACCOUNT.FORMAT, and delete it from AUTH_CONFIG. --- app/res/layout/filepermission.xml | 20 -- app/src/main/org/runnerup/db/DBHelper.java | 36 +++- .../runnerup/export/DropboxSynchronizer.java | 188 ++++++++++-------- .../org/runnerup/export/FileSynchronizer.java | 21 +- .../main/org/runnerup/export/SyncManager.java | 14 +- .../org/runnerup/export/Synchronizer.java | 4 +- .../org/runnerup/view/AccountActivity.java | 56 +++++- .../org/runnerup/view/DetailActivity.java | 1 + .../org/runnerup/workout/FileFormats.java | 106 ++++++++++ .../org/runnerup/workout/FileFormatsTest.java | 167 ++++++++++++++++ common/src/main/res/values-fr/strings.xml | 2 + common/src/main/res/values/strings.xml | 2 + 12 files changed, 471 insertions(+), 146 deletions(-) create mode 100644 app/src/main/org/runnerup/workout/FileFormats.java create mode 100644 app/test/java/org/runnerup/workout/FileFormatsTest.java diff --git a/app/res/layout/filepermission.xml b/app/res/layout/filepermission.xml index 039451d3c..9fbc316f0 100644 --- a/app/res/layout/filepermission.xml +++ b/app/res/layout/filepermission.xml @@ -55,24 +55,4 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/org/runnerup/db/DBHelper.java b/app/src/main/org/runnerup/db/DBHelper.java index 7ffbcad25..69bce9097 100644 --- a/app/src/main/org/runnerup/db/DBHelper.java +++ b/app/src/main/org/runnerup/db/DBHelper.java @@ -17,7 +17,6 @@ package org.runnerup.db; -import android.annotation.TargetApi; import android.support.v7.app.AlertDialog; import android.app.ProgressDialog; import android.content.ContentValues; @@ -27,9 +26,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.AsyncTask; -import android.os.Build; import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.db.entities.DBEntity; @@ -41,7 +41,6 @@ import org.runnerup.export.FunBeatSynchronizer; import org.runnerup.export.GarminSynchronizer; import org.runnerup.export.GoogleFitSynchronizer; -import org.runnerup.export.GooglePlusSynchronizer; import org.runnerup.export.JoggSESynchronizer; import org.runnerup.export.MapMyRunSynchronizer; import org.runnerup.export.NikePlusSynchronizer; @@ -53,7 +52,9 @@ import org.runnerup.export.RuntasticSynchronizer; import org.runnerup.export.StravaSynchronizer; import org.runnerup.util.FileUtil; +import org.runnerup.workout.FileFormats; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -133,7 +134,7 @@ public class DBHelper extends SQLiteOpenHelper implements + (DB.ACCOUNT.NAME + " text not null, ") + (DB.ACCOUNT.DESCRIPTION + " text, ") //DBVERSION update: remove + (DB.ACCOUNT.URL + " text, ") //DBVERSION update: remove - + (DB.ACCOUNT.FORMAT + " text not null, ") //DBVERSION update: remove + + (DB.ACCOUNT.FORMAT + " text not null, ") // tcx/gpx, for file uploads + (DB.ACCOUNT.FLAGS + " integer not null default " + DB.ACCOUNT.DEFAULT_FLAGS + ", ") //Mostly not used but dynamic changes could be stored here + (DB.ACCOUNT.ENABLED + " integer not null default 1,") //Account is not hidden/disabled + (DB.ACCOUNT.AUTH_METHOD + " text not null, ") //DBVERSION update: remove @@ -391,7 +392,7 @@ private static void echoDo(SQLiteDatabase arg0, String str) { } private void migrateFileSynchronizerInfo(SQLiteDatabase arg0) { - //Migrate storage of parameters, FORMAT is removed + // Migrate storage of parameters String from[] = { "_id", DB.ACCOUNT.FORMAT, DB.ACCOUNT.AUTH_CONFIG }; String args[] = { FileSynchronizer.NAME }; Cursor c = arg0.query(DB.ACCOUNT.TABLE, from, @@ -402,14 +403,31 @@ private void migrateFileSynchronizerInfo(SQLiteDatabase arg0) { if (c.moveToFirst()) { ContentValues tmp = DBHelper.get(c); //URL was stored in AUTH_CONFIG previously, FORMAT migrated too - String oldUrl = tmp.getAsString(DB.ACCOUNT.AUTH_CONFIG); + String oldAuthConfig = tmp.getAsString(DB.ACCOUNT.AUTH_CONFIG); //DBVERSION update, not needed in onUpgrade() - if (oldUrl.startsWith("/")) { - tmp.put(DB.ACCOUNT.URL, oldUrl); + if (oldAuthConfig.startsWith("/")) { + tmp.put(DB.ACCOUNT.URL, oldAuthConfig); String authConfig = FileSynchronizer.contentValuesToAuthConfig(tmp); tmp = new ContentValues(); tmp.put(DB.ACCOUNT.AUTH_CONFIG, authConfig); + tmp.put(DB.ACCOUNT.FORMAT, FileFormats.DEFAULT_FORMATS.toString()); arg0.update(DB.ACCOUNT.TABLE, tmp, DB.ACCOUNT.NAME + " = ?", args); + } else { + try { + // Check if AUTH_CONFIG contains deprecated FORMAT field + JSONObject authcfg = new JSONObject(oldAuthConfig); + String format = authcfg.optString(DB.ACCOUNT.FORMAT, null); + if (format != null) { + // Move deprecated FORMAT field in AUTH_CONFIG to ACCOUNT.FORMAT + authcfg.put(DB.ACCOUNT.FORMAT, null); + tmp = new ContentValues(); + tmp.put(DB.ACCOUNT.AUTH_CONFIG, authcfg.toString()); + tmp.put(DB.ACCOUNT.FORMAT, format); + arg0.update(DB.ACCOUNT.TABLE, tmp, DB.ACCOUNT.NAME + " = ?", args); + } + } catch (JSONException e) { + Log.w("DBHelper", "Failed to parse File auth config", e); + } } } c.close(); @@ -486,8 +504,8 @@ private static void insertAccount(SQLiteDatabase arg0, String name, int enabled, if (flags >= 0) { arg1.put(DB.ACCOUNT.FLAGS, flags); } + arg1.put(DB.ACCOUNT.FORMAT, FileFormats.DEFAULT_FORMATS.toString()); //DBVERSION update, must provide dummy data - arg1.put(DB.ACCOUNT.FORMAT, "tcx"); arg1.put(DB.ACCOUNT.AUTH_METHOD, "dummy"); //SQLite has no UPSERT command. Optimize for no change. diff --git a/app/src/main/org/runnerup/export/DropboxSynchronizer.java b/app/src/main/org/runnerup/export/DropboxSynchronizer.java index c5ae8f78b..c43c17a37 100644 --- a/app/src/main/org/runnerup/export/DropboxSynchronizer.java +++ b/app/src/main/org/runnerup/export/DropboxSynchronizer.java @@ -17,6 +17,7 @@ package org.runnerup.export; import android.app.Activity; import android.content.ContentValues; +import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -28,17 +29,17 @@ import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.common.util.Constants.DB; +import org.runnerup.db.PathSimplifier; +import org.runnerup.export.format.GPX; import org.runnerup.export.format.TCX; import org.runnerup.export.oauth2client.OAuth2Activity; import org.runnerup.export.oauth2client.OAuth2Server; import org.runnerup.export.util.SyncHelper; +import org.runnerup.workout.FileFormats; import org.runnerup.workout.Sport; import java.io.BufferedOutputStream; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StringWriter; import java.net.HttpURLConnection; @@ -59,11 +60,16 @@ public class DropboxSynchronizer extends DefaultSynchronizer implements OAuth2Se private long id = 0; private String access_token = null; + private FileFormats mFormat; + private PathSimplifier simplifier = null; - DropboxSynchronizer() { + DropboxSynchronizer(Context context) { if (ENABLED == 0) { Log.w(NAME, "No client id configured in this build"); } + this.simplifier = PathSimplifier.isEnabledForExportGpx(context) ? + new PathSimplifier(context, true) : + null; } @Override @@ -129,6 +135,7 @@ public void init(ContentValues config) { String authConfig = config.getAsString(DB.ACCOUNT.AUTH_CONFIG); if (authConfig != null) { try { + mFormat = new FileFormats(config.getAsString(DB.ACCOUNT.FORMAT)); JSONObject tmp = new JSONObject(authConfig); parseAuthData(tmp); } catch (Exception e) { @@ -216,6 +223,73 @@ private String getDesc(SQLiteDatabase db, final long mID) { return desc; } + // upload a single file + private Status uploadFile(SQLiteDatabase db, final long mID, Sport sport, + StringWriter writer, String fileExt) + throws IOException, JSONException { + + Status s; + + // Upload to default directory /Apps/RunnerUp + String file = String.format(Locale.getDefault(), "/RunnerUp_%s_%04d_%s.%s", + android.os.Build.MODEL.replaceAll("\\s","_"), mID, sport.TapiriikType(), + fileExt); + + HttpURLConnection conn = (HttpURLConnection) new URL(UPLOAD_URL).openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod(RequestMethod.POST.name()); + conn.addRequestProperty("Content-Type", "application/octet-stream"); + conn.setRequestProperty("Authorization", "Bearer " + access_token); + JSONObject parameters = new JSONObject(); + try { + parameters.put("path", file); + parameters.put("mode", "add"); + parameters.put("autorename", true); + } catch (JSONException e) { + e.printStackTrace(); + return Status.ERROR; + } + conn.addRequestProperty("Dropbox-API-Arg", parameters.toString()); + OutputStream out = new BufferedOutputStream(conn.getOutputStream()); + out.write(writer.getBuffer().toString().getBytes()); + out.flush(); + out.close(); + + int responseCode = conn.getResponseCode(); + String amsg = conn.getResponseMessage(); + Log.v(getName(), "code: " + responseCode + ", amsg: " + amsg+" "); + + JSONObject obj = SyncHelper.parse(conn, getName()); + + if (obj != null && responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) { + s = Status.OK; + s.activityId = mID; + if (obj.has("id")) { + // Note: duplicate will not set activity_id + s.externalId = noNullStr(obj.getString("id")); + if (s.externalId != null) { + s.externalIdStatus = ExternalIdStatus.OK; + } + } + return s; + } + String error = obj != null && obj.has("error") ? + noNullStr(obj.getString("error")) : + ""; + Log.e(getName(),"Error uploading, code: " + + responseCode + ", amsg: " + amsg + " " + error + ", json: " + (obj == null ? "" : obj)); + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + // token no longer valid + access_token = null; + s = Status.NEED_AUTH; + s.authMethod = AuthMethod.OAUTH2; + } else { + s = Status.ERROR; + } + + return s; + } + @Override public Status upload(SQLiteDatabase db, final long mID) { Status s = connect(); @@ -225,96 +299,50 @@ public Status upload(SQLiteDatabase db, final long mID) { Sport sport = Sport.RUNNING; try { - String[] columns = { - Constants.DB.ACTIVITY.SPORT - }; - Cursor c = null; - try { - c = db.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + mID, - null, null, null, null); - if (c.moveToFirst()) { - sport = Sport.valueOf(c.getInt(0)); - } - } finally { - if (c != null) { - c.close(); - } - } - // Upload to default directory /Apps/RunnerUp - String file = String.format(Locale.getDefault(), "/RunnerUp_%s_%04d_%s.tcx", - android.os.Build.MODEL.replaceAll("\\s","_"), mID, sport.TapiriikType()); - StringWriter writer = new StringWriter(); - TCX tcx = new TCX(db); - tcx.export(mID, writer); - - HttpURLConnection conn = (HttpURLConnection) new URL(UPLOAD_URL).openConnection(); - conn.setDoOutput(true); - conn.setRequestMethod(RequestMethod.POST.name()); - conn.addRequestProperty("Content-Type", "application/octet-stream"); - conn.setRequestProperty("Authorization", "Bearer " + access_token); - JSONObject parameters = new JSONObject(); + String[] columns = { Constants.DB.ACTIVITY.SPORT }; + Cursor c = null; try { - parameters.put("path", file); - parameters.put("mode", "add"); - parameters.put("autorename", true); - } catch (JSONException e) { - e.printStackTrace(); - return Status.ERROR; - } - conn.addRequestProperty("Dropbox-API-Arg", parameters.toString()); - OutputStream out = new BufferedOutputStream(conn.getOutputStream()); - out.write(writer.getBuffer().toString().getBytes()); - out.flush(); - out.close(); - - int responseCode = conn.getResponseCode(); - String amsg = conn.getResponseMessage(); - Log.v(getName(), "code: " + responseCode + ", amsg: " + amsg+" "); - - JSONObject obj = SyncHelper.parse(conn, getName()); - - if (obj != null && responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) { - s = Status.OK; - s.activityId = mID; - if (obj.has("id")) { - // Note: duplicate will not set activity_id - s.externalId = noNullStr(obj.getString("id")); - if (s.externalId != null) { - s.externalIdStatus = ExternalIdStatus.OK; - } + c = db.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + mID, + null, null, null, null); + if (c.moveToFirst()) { + sport = Sport.valueOf(c.getInt(0)); + } + } finally { + if (c != null) { + c.close(); } - return s; } - String error = obj != null && obj.has("error") ? - noNullStr(obj.getString("error")) : - ""; - Log.e(getName(),"Error uploading, code: " + - responseCode + ", amsg: " + amsg + " " + error + ", json: " + (obj == null ? "" : obj)); - if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - // token no longer valid - access_token = null; - s = Status.NEED_AUTH; - s.authMethod = AuthMethod.OAUTH2; + + StringWriter writer = new StringWriter(); + if (mFormat.contains(FileFormats.TCX)) { + TCX tcx = new TCX(db); + tcx.export(mID, writer); + s = uploadFile(db, mID, sport, writer, FileFormats.TCX.getValue()); + } + if (s == Status.OK && mFormat.contains(FileFormats.GPX)) { + GPX gpx = new GPX(db, true, true, simplifier); + gpx.export(mID, writer); + s = uploadFile(db, mID, sport, writer, FileFormats.GPX.getValue()); } - s = Status.ERROR; - return s; - } catch (IOException e) { - s = Status.ERROR; - s.ex = e; - } catch (JSONException e) { + } catch (Exception e) { + Log.e(getName(),"Error uploading, exception: ", e); s = Status.ERROR; s.ex = e; } - - s.ex.printStackTrace(); return s; } @Override public boolean checkSupport(Feature f) { - return f == Feature.UPLOAD; + switch (f) { + case UPLOAD: + case FILE_FORMAT: + return true; + default: + return false; + } } } diff --git a/app/src/main/org/runnerup/export/FileSynchronizer.java b/app/src/main/org/runnerup/export/FileSynchronizer.java index 32c6b53ee..753113209 100644 --- a/app/src/main/org/runnerup/export/FileSynchronizer.java +++ b/app/src/main/org/runnerup/export/FileSynchronizer.java @@ -33,6 +33,7 @@ import org.runnerup.db.PathSimplifier; import org.runnerup.export.format.GPX; import org.runnerup.export.format.TCX; +import org.runnerup.workout.FileFormats; import org.runnerup.workout.Sport; import java.io.BufferedOutputStream; @@ -50,7 +51,7 @@ public class FileSynchronizer extends DefaultSynchronizer { private long id = 0; private String mPath; - private String mFormat; + private FileFormats mFormat; private PathSimplifier simplifier = null; FileSynchronizer() {} @@ -86,8 +87,6 @@ public String getPublicUrl() { static public String contentValuesToAuthConfig(ContentValues config) { FileSynchronizer f = new FileSynchronizer(); f.mPath = config.getAsString(DB.ACCOUNT.URL); - f.mFormat = config.getAsString(DB.ACCOUNT.FORMAT); - return f.getAuthConfig(); } @@ -96,9 +95,9 @@ public void init(ContentValues config) { String authConfig = config.getAsString(DB.ACCOUNT.AUTH_CONFIG); if (authConfig != null) { try { + mFormat = new FileFormats(config.getAsString(DB.ACCOUNT.FORMAT)); JSONObject tmp = new JSONObject(authConfig); mPath = tmp.optString(DB.ACCOUNT.URL, null); - mFormat = tmp.optString(DB.ACCOUNT.FORMAT); } catch (JSONException e) { Log.w(getName(), "init: Dropping config due to failure to parse json from " + authConfig + ", " + e); } @@ -112,9 +111,8 @@ public String getAuthConfig() { if (isConfigured()) { try { tmp.put(DB.ACCOUNT.URL, mPath); - tmp.put(DB.ACCOUNT.FORMAT, mFormat); } catch (JSONException e) { - Log.w(getName(), "getAuthConfig: Failure to create json for " + mPath + ", " + mFormat + ", " + e); + Log.w(getName(), "getAuthConfig: Failure to create json for " + mPath + ", " + e); } } return tmp.toString(); @@ -122,7 +120,7 @@ public String getAuthConfig() { @Override public boolean isConfigured() { - return !TextUtils.isEmpty(mPath) && !TextUtils.isEmpty(mFormat); + return !TextUtils.isEmpty(mPath); } @Override @@ -177,17 +175,17 @@ public Status upload(SQLiteDatabase db, final long mID) { String fileBase = new File(mPath).getAbsolutePath() + File.separator + String.format(Locale.getDefault(), "RunnerUp_%04d_%s.", mID, sport.TapiriikType()); - if (mFormat.contains("tcx")) { + if (mFormat.contains(FileFormats.TCX)) { TCX tcx = new TCX(db); - File file = new File(fileBase + "tcx"); + File file = new File(fileBase + FileFormats.TCX.getValue()); OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); tcx.export(mID, new OutputStreamWriter(out)); s.externalId = Uri.fromFile(file).toString(); s.externalIdStatus = ExternalIdStatus.NONE; //Not working yet } - if (mFormat.contains("gpx")) { + if (mFormat.contains(FileFormats.GPX)) { GPX gpx = new GPX(db, true, true, simplifier); - File file = new File(fileBase + "gpx"); + File file = new File(fileBase + FileFormats.GPX.getValue()); OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); gpx.export(mID, new OutputStreamWriter(out)); } @@ -202,6 +200,7 @@ public Status upload(SQLiteDatabase db, final long mID) { public boolean checkSupport(Feature f) { switch (f) { case UPLOAD: + case FILE_FORMAT: return true; default: return false; diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index 5bad6ec02..e8eb0f808 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -216,7 +216,7 @@ public Synchronizer add(ContentValues config) { } else if (synchronizerName.contentEquals(RunalyzeSynchronizer.NAME)) { synchronizer = new RunalyzeSynchronizer(); } else if (synchronizerName.contentEquals(DropboxSynchronizer.NAME)) { - synchronizer = new DropboxSynchronizer(); + synchronizer = new DropboxSynchronizer(mContext); } else { Log.e(getClass().getName(), "synchronizer does not exist: " + synchronizerName);; } @@ -476,10 +476,6 @@ private void askFileUrl(final Synchronizer sync) { final TextView tv1 = (TextView) view.findViewById(R.id.fileuri); final TextView tvAuthNotice = (TextView) view.findViewById(R.id.textViewAuthNotice); - final CheckBox cbtcx = (CheckBox) view.findViewById(R.id.tcxformat); - final CheckBox cbgpx = (CheckBox) view.findViewById(R.id.gpxformat); - cbtcx.setChecked(true); - String path; if (Build.VERSION.SDK_INT >= 19) { //noinspection InlinedApi @@ -508,16 +504,8 @@ private void askFileUrl(final Synchronizer sync) { @Override public void onClick(DialogInterface dialog, int which) { //Set default values - String format = ""; - if (cbtcx.isChecked()) { - format = "tcx,"; - } - if (cbgpx.isChecked()) { - format += "gpx,"; - } ContentValues tmp = new ContentValues(); - tmp.put(DB.ACCOUNT.FORMAT, format); tmp.put(DB.ACCOUNT.URL, tv1.getText().toString()); ContentValues config = new ContentValues(); config.put("_id", sync.getId()); diff --git a/app/src/main/org/runnerup/export/Synchronizer.java b/app/src/main/org/runnerup/export/Synchronizer.java index 454208033..db5a8c432 100644 --- a/app/src/main/org/runnerup/export/Synchronizer.java +++ b/app/src/main/org/runnerup/export/Synchronizer.java @@ -70,8 +70,8 @@ enum Feature { LIVE, // live feed of activity SKIP_MAP, // skip map in upload ACTIVITY_LIST, //list recorded activities - GET_ACTIVITY //download recorded activity - + GET_ACTIVITY, //download recorded activity + FILE_FORMAT // upload as file in different possible formats } /** diff --git a/app/src/main/org/runnerup/view/AccountActivity.java b/app/src/main/org/runnerup/view/AccountActivity.java index 8f32a6880..9628b42d1 100644 --- a/app/src/main/org/runnerup/view/AccountActivity.java +++ b/app/src/main/org/runnerup/view/AccountActivity.java @@ -46,6 +46,7 @@ import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; +import android.widget.Toast; import org.runnerup.R; import org.runnerup.common.util.Constants; @@ -57,6 +58,7 @@ import org.runnerup.export.Synchronizer.Status; import org.runnerup.util.Bitfield; import org.runnerup.widget.WidgetUtil; +import org.runnerup.workout.FileFormats; import java.util.ArrayList; @@ -67,6 +69,7 @@ public class AccountActivity extends AppCompatActivity implements Constants { private final ArrayList mCursors = new ArrayList<>(); private long flags; + private FileFormats format; private SyncManager syncManager = null; private EditText mRunnerUpLiveApiAddress = null; @@ -132,7 +135,7 @@ private void fillData() { // Fields from the database (projection) // Must include the _id column for the adapter to work String[] from = new String[]{ - "_id", DB.ACCOUNT.NAME, DB.ACCOUNT.FLAGS, DB.ACCOUNT.AUTH_CONFIG + "_id", DB.ACCOUNT.NAME, DB.ACCOUNT.FLAGS, DB.ACCOUNT.FORMAT, DB.ACCOUNT.AUTH_CONFIG }; String args[] = { @@ -147,6 +150,7 @@ private void fillData() { ContentValues tmp = DBHelper.get(c); synchronizer = syncManager.add(tmp); flags = tmp.getAsLong(DB.ACCOUNT.FLAGS); + format = new FileFormats(tmp.getAsString(DB.ACCOUNT.FORMAT)); if (synchronizer == null) { return; } @@ -204,6 +208,18 @@ private void fillData() { btn.setVisibility(View.GONE); } + if (synchronizer.checkSupport(Synchronizer.Feature.FILE_FORMAT)) { + // Add file format checkboxes + addRow(getResources().getString(R.string.File_format), null); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + CheckBox cb = new CheckBox(this); + cb.setChecked(format.contains(f)); + cb.setTag(f); + cb.setOnCheckedChangeListener(sendCBChecked); + addRow(f.getName(), cb); + } + } + if (synchronizer.checkSupport(Synchronizer.Feature.FEED)) { CheckBox cb = new CheckBox(this); cb.setTag(DB.ACCOUNT.FLAG_FEED); @@ -325,17 +341,35 @@ public void onClick(View v) { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { ContentValues tmp = new ContentValues(); - int flag = (Integer) buttonView.getTag(); - switch (flag) { - case DB.ACCOUNT.FLAG_UPLOAD: - case DB.ACCOUNT.FLAG_FEED: - case DB.ACCOUNT.FLAG_LIVE: - flags = Bitfield.set(flags, flag, isChecked); - break; - case DB.ACCOUNT.FLAG_SKIP_MAP: - flags = Bitfield.set(flags, flag, !isChecked); + Object flag = buttonView.getTag(); + if (flag instanceof FileFormats.Format) { + if (isChecked) { + format.add((FileFormats.Format) flag); + } else { + format.remove((FileFormats.Format) flag); + // At least one format needed + if (TextUtils.isEmpty(format.toString())) { + // Recheck unchecked format + format.add((FileFormats.Format) flag); + buttonView.setChecked(true); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.File_need_one_format), + Toast.LENGTH_SHORT).show(); + } + } + tmp.put(DB.ACCOUNT.FORMAT, format.toString()); + } else { + switch ((int) flag) { + case DB.ACCOUNT.FLAG_UPLOAD: + case DB.ACCOUNT.FLAG_FEED: + case DB.ACCOUNT.FLAG_LIVE: + flags = Bitfield.set(flags, (Integer) flag, isChecked); + break; + case DB.ACCOUNT.FLAG_SKIP_MAP: + flags = Bitfield.set(flags, (Integer) flag, !isChecked); + } + tmp.put(DB.ACCOUNT.FLAGS, flags); } - tmp.put(DB.ACCOUNT.FLAGS, flags); String args[] = { mSynchronizerName }; diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index 4c7ccd517..f53e38843 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -414,6 +414,7 @@ private void requery() { + (" acc." + DB.ACCOUNT.NAME + ", ") + (" acc." + DB.ACCOUNT.FLAGS + ", ") + (" acc." + DB.ACCOUNT.AUTH_CONFIG + ", ") + + (" acc." + DB.ACCOUNT.FORMAT + ", ") + (" rep._id as repid, ") + (" rep." + DB.EXPORT.ACCOUNT + ", ") + (" rep." + DB.EXPORT.ACTIVITY + ", ") diff --git a/app/src/main/org/runnerup/workout/FileFormats.java b/app/src/main/org/runnerup/workout/FileFormats.java new file mode 100644 index 000000000..0d06087c4 --- /dev/null +++ b/app/src/main/org/runnerup/workout/FileFormats.java @@ -0,0 +1,106 @@ +package org.runnerup.workout; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class FileFormats { + + private final boolean readonly; + private String formats; + + public static class Format { + + final private String name; + final private String value; + + Format(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + public String getValue() { + return value; + } + } + + public final static Format GPX = new Format("GPX", "gpx"); + public final static Format TCX = new Format("TCX", "tcx"); + public final static List ALL_FORMATS; + public final static FileFormats DEFAULT_FORMATS; + + static { + List formatList = Arrays.asList(TCX, GPX); + ALL_FORMATS = Collections.unmodifiableList(formatList); + DEFAULT_FORMATS = new FileFormats(FileFormats.TCX.getValue(), true); + } + + public FileFormats() { + this(null, false); + } + + public FileFormats(String formats) { + this(formats, false); + } + + private FileFormats(String formats, boolean readonly) { + this.readonly = readonly; + this.formats = formats == null ? "" : formats; + } + + public boolean contains(Format format) { + if (format == null) { + throw new IllegalArgumentException(); + } + // search for the format type between 2 word boundaries, anywhere in the string + return formats.matches(".*\\b" + format.getValue() + "\\b.*"); + } + + public boolean remove(Format format) { + if (format == null) { + throw new IllegalArgumentException(); + } + if (readonly) { + throw new UnsupportedOperationException(); + } + if (contains(format)) { + formats = formats.replaceAll(",?" + format.getValue() + "\\b", ""); + // cleanup commas + formats = formats.replaceAll(",$", ""); + formats = formats.replaceAll("^,", ""); + return true; + } else { + return false; + } + } + + public boolean add(Format format) { + if (format == null) { + throw new IllegalArgumentException(); + } + if (readonly) { + throw new UnsupportedOperationException(); + } + if (formats.length() == 0) { + formats = format.getValue(); + return true; + } else { + if (contains(format)) { + return false; + } else { + formats += "," + format.getValue(); + // cleanup commas + formats = formats.replaceAll(",,", ","); + return true; + } + } + } + + @Override + public String toString() { + return formats; + } +} diff --git a/app/test/java/org/runnerup/workout/FileFormatsTest.java b/app/test/java/org/runnerup/workout/FileFormatsTest.java new file mode 100644 index 000000000..5615a0d9b --- /dev/null +++ b/app/test/java/org/runnerup/workout/FileFormatsTest.java @@ -0,0 +1,167 @@ +package org.runnerup.workout; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class FileFormatsTest { + + @Test + public void nullConstructor() { + FileFormats formats = new FileFormats(); + assertNotNull(formats.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void nullContains() { + FileFormats formats = new FileFormats(); + formats.contains(null); + } + + @Test(expected = IllegalArgumentException.class) + public void nullAdd() { + FileFormats formats = new FileFormats(); + formats.add(null); + } + + @Test(expected = IllegalArgumentException.class) + public void nullRemove() { + FileFormats formats = new FileFormats(); + formats.remove(null); + } + + @Test + public void defaultNotEmpty() { + FileFormats formats = FileFormats.DEFAULT_FORMATS; + boolean notEmpty = false; + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + notEmpty = notEmpty || formats.contains(f); + } + assertTrue(notEmpty); + } + + @Test(expected = UnsupportedOperationException.class) + public void defaultNoRemove() { + FileFormats formats = FileFormats.DEFAULT_FORMATS; + formats.remove(FileFormats.TCX); + } + + @Test(expected = UnsupportedOperationException.class) + public void defaultNoAdd() { + FileFormats formats = FileFormats.DEFAULT_FORMATS; + formats.add(FileFormats.GPX); + } + + + @Test + public void addOnce() { + FileFormats formats; + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + formats = new FileFormats(); + formats.add(f); + assertEquals(formats.toString(), f.getValue()); + } + } + + @Test + public void addAll() { + FileFormats formats = new FileFormats(); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertFalse(formats.contains(f)); + } + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + formats.add(f); + } + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.contains(f)); + } + } + + @Test + public void addTwice() { + FileFormats formats = new FileFormats(); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.add(f)); + } + String v1 = formats.toString(); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertFalse(formats.add(f)); + } + String v2 = formats.toString(); + assertEquals(v1, v2); + } + + @Test + public void removeOnce() { + FileFormats formats = new FileFormats(); + String v1 = formats.toString(); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.add(f)); + } + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.remove(f)); + } + String v2 = formats.toString(); + assertEquals(v1, v2); + } + + @Test + public void removeTwice() { + FileFormats formats = new FileFormats(); + String v1 = formats.toString(); + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.add(f)); + } + for (FileFormats.Format f: FileFormats.ALL_FORMATS) { + assertTrue(formats.remove(f)); + assertFalse(formats.remove(f)); + } + String v2 = formats.toString(); + assertEquals(v1, v2); + } + + @Test + public void legacyCompat() { + FileFormats formats; + // one format + formats = new FileFormats("gpx,"); + assertTrue(formats.contains(FileFormats.GPX)); + assertTrue(formats.add(FileFormats.TCX)); + assertTrue(formats.remove(FileFormats.GPX)); + assertFalse(formats.contains(FileFormats.GPX)); + assertTrue(formats.contains(FileFormats.TCX)); + + // both formats + formats = new FileFormats("gpx,tcx,"); + String v1 = formats.toString(); + assertTrue(formats.contains(FileFormats.TCX)); + assertFalse(formats.add(FileFormats.TCX)); + String v2 = formats.toString(); + assertEquals(v1, v2); + assertTrue(formats.remove(FileFormats.TCX)); + assertFalse(formats.contains(FileFormats.TCX)); + assertEquals(formats.toString(), FileFormats.GPX.getValue()); + } + + @Test + public void futureProof() { + // playing with longer list + FileFormats formats; + formats = new FileFormats("gpx,a,b,c,d,"); + assertTrue(formats.contains(FileFormats.GPX)); + assertTrue(formats.add(FileFormats.TCX)); + assertTrue(formats.remove(FileFormats.GPX)); + assertFalse(formats.contains(FileFormats.GPX)); + assertTrue(formats.contains(FileFormats.TCX)); + assertTrue(formats.add(FileFormats.GPX)); + assertTrue(formats.contains(FileFormats.GPX)); + assertTrue(formats.remove(FileFormats.TCX)); + assertFalse(formats.contains(FileFormats.TCX)); + assertTrue(formats.remove(FileFormats.GPX)); + assertFalse(formats.contains(FileFormats.GPX)); + assertEquals(formats.toString(), ("a,b,c,d")); + } +} diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml index 7d3ec8204..62143be5d 100644 --- a/common/src/main/res/values-fr/strings.xml +++ b/common/src/main/res/values-fr/strings.xml @@ -350,4 +350,6 @@ Algorithme Choisissez entre rapidité et haute qualité de la simplification de la trace Simplifier la trace + Fichier(s) au format: + Sélectionnez au moins un format ! diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 20f3b0109..f0497d74b 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -351,4 +351,6 @@ Algorithm Choose fast or high quality path simplification Simplify path + Upload file(s) as: + At least one format needed!