From 01c95079db466b7715e30d742cf5a83d8b753462 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson <6248932+gerhardol@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:31:13 +0100 Subject: [PATCH 01/31] fix: logging level cleanup (#1259) Decrease logging level for many entries. Many normal events were using Log.e() --- .../runnerup/content/ActivityProvider.java | 14 +++++----- .../main/org/runnerup/db/ActivityCleaner.java | 2 +- app/src/main/org/runnerup/db/DBHelper.java | 10 +++---- .../runnerup/export/DefaultSynchronizer.java | 4 +-- .../org/runnerup/export/FileSynchronizer.java | 4 +-- .../export/RunKeeperSynchronizer.java | 2 +- .../export/RunningAHEADSynchronizer.java | 4 +-- .../main/org/runnerup/export/SyncManager.java | 12 ++++---- .../export/oauth2client/OAuth2Activity.java | 2 +- .../main/org/runnerup/tracker/Tracker.java | 8 +++--- .../component/TrackerComponentCollection.java | 10 +++---- .../tracker/component/TrackerElevation.java | 2 +- app/src/main/org/runnerup/util/Formatter.java | 2 +- .../main/org/runnerup/util/GraphWrapper.java | 28 +++++++++---------- app/src/main/org/runnerup/util/HRZones.java | 8 +++--- .../org/runnerup/view/DetailActivity.java | 2 +- .../org/runnerup/view/HRZonesActivity.java | 9 +++--- .../main/org/runnerup/view/HRZonesBar.java | 2 +- .../main/org/runnerup/view/MainLayout.java | 12 ++++---- .../org/runnerup/view/ManualActivity.java | 8 +++--- .../main/org/runnerup/view/RunActivity.java | 2 +- .../main/org/runnerup/view/StartFragment.java | 14 +++++----- .../runnerup/workout/EndOfLapSuppression.java | 2 +- .../org/runnerup/workout/TargetTrigger.java | 7 ++--- .../main/org/runnerup/workout/Trigger.java | 2 +- .../org/runnerup/workout/WorkoutBuilder.java | 2 +- .../runnerup/workout/WorkoutSerializer.java | 4 +-- .../workout/feedback/RUTextToSpeech.java | 10 +++---- .../tracker/component/TrackerWear.java | 12 ++++---- hrdevice/src/org/runnerup/hr/Bt20Base.java | 4 ++- hrdevice/src/org/runnerup/hr/BtHRBase.java | 4 ++- hrdevice/src/org/runnerup/hr/HRManager.java | 2 +- .../runnerup/hr/RetryingHRProviderProxy.java | 4 ++- .../org/runnerup/service/ListenerService.java | 18 ++++++------ .../org/runnerup/service/StateService.java | 14 ++++++---- .../java/org/runnerup/view/MainActivity.java | 9 +++--- 36 files changed, 133 insertions(+), 122 deletions(-) diff --git a/app/src/main/org/runnerup/content/ActivityProvider.java b/app/src/main/org/runnerup/content/ActivityProvider.java index 159d1d960..d7fbad76b 100644 --- a/app/src/main/org/runnerup/content/ActivityProvider.java +++ b/app/src/main/org/runnerup/content/ActivityProvider.java @@ -97,7 +97,7 @@ private Pair openCacheFile(String name) { @SuppressWarnings("ConstantConditions") final File file = new File(path.getAbsolutePath() + File.separator + name); final OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); - Log.e(getClass().getName(), i + ": putting cache file in: " + file.getAbsolutePath()); + Log.d(getClass().getName(), i + ": putting cache file in: " + file.getAbsolutePath()); //noinspection Convert2Diamond return new Pair(file, out); } catch (IOException | NullPointerException ignored) { @@ -112,7 +112,7 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { final int res = uriMatcher.match(uri); - Log.e(getClass().getName(), "match(" + uri + "): " + res); + Log.d(getClass().getName(), "match(" + uri + "): " + res); switch (res) { case GPX: case TCX: @@ -122,11 +122,11 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) final String parcelFile = "activity." + list.get(list.size() - 3); final Pair out = openCacheFile(parcelFile); if (out == null) { - Log.e(getClass().getName(), "Failed to open cacheFile(" + parcelFile + ")"); + Log.i(getClass().getName(), "Failed to open cacheFile(" + parcelFile + ")"); return null; } - Log.e( + Log.d( getClass().getName(), "activity: " + activityId + ", file: " + out.first.getAbsolutePath()); SQLiteDatabase mDB = DBHelper.getReadableDatabase(getContext()); @@ -139,7 +139,7 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) var options = ExportOptions.builder(); TCX tcx = new TCX(mDB, options.build(), simplifier); tcx.export(activityId, new OutputStreamWriter(out.second)); - Log.e(getClass().getName(), "export tcx"); + Log.d(getClass().getName(), "export tcx"); break; } case GPX:{ @@ -154,13 +154,13 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) options.accuracyExtensions = extraData; GPX gpx = new GPX(mDB, options.build(), simplifier); gpx.export(activityId, new OutputStreamWriter(out.second)); - Log.e(getClass().getName(), "export gpx"); + Log.d(getClass().getName(), "export gpx"); break; } } out.second.flush(); out.second.close(); - Log.e(getClass().getName(), "wrote " + out.first.length() + " bytes..."); + Log.d(getClass().getName(), "wrote " + out.first.length() + " bytes..."); } catch (Exception e) { e.printStackTrace(); } diff --git a/app/src/main/org/runnerup/db/ActivityCleaner.java b/app/src/main/org/runnerup/db/ActivityCleaner.java index 62d3b04b0..38e0d37b6 100644 --- a/app/src/main/org/runnerup/db/ActivityCleaner.java +++ b/app/src/main/org/runnerup/db/ActivityCleaner.java @@ -230,7 +230,7 @@ public static void trim(SQLiteDatabase db, long activityId) { for (long lap : laps) { int res = trimLap(db, activityId, lap); - Log.e("ActivityCleaner", "lap " + lap + " removed " + res + " locations"); + Log.i("ActivityCleaner", "lap " + lap + " removed " + res + " locations"); } } diff --git a/app/src/main/org/runnerup/db/DBHelper.java b/app/src/main/org/runnerup/db/DBHelper.java index d529f4df8..df480de72 100644 --- a/app/src/main/org/runnerup/db/DBHelper.java +++ b/app/src/main/org/runnerup/db/DBHelper.java @@ -260,7 +260,7 @@ public void onCreate(SQLiteDatabase arg0) { @Override public void onUpgrade(SQLiteDatabase arg0, int oldVersion, int newVersion) { - Log.e( + Log.i( getClass().getName(), "onUpgrade: oldVersion: " + oldVersion + ", newVersion: " + newVersion); @@ -429,7 +429,7 @@ private void onCreateUpgrade(SQLiteDatabase arg0, int oldVersion, int newVersion } private static void echoDo(SQLiteDatabase arg0, String str) { - Log.e("DBHelper", "execSQL(" + str + ")"); + Log.d("DBHelper", "execSQL(" + str + ")"); arg0.execSQL(str); } @@ -567,12 +567,12 @@ private static void insertAccount(SQLiteDatabase arg0, String name, int enabled, arg1.remove(DB.ACCOUNT.FORMAT); arg1.remove(DB.ACCOUNT.AUTH_METHOD); arg0.update(DB.ACCOUNT.TABLE, arg1, DB.ACCOUNT.NAME + " = ?", arr); - Log.v("DBhelper", "update: " + arg1); + Log.d("DBhelper", "update: " + arg1); } } public static void deleteAccount(SQLiteDatabase db, long id) { - Log.e("DBHelper", "deleting account: " + id); + Log.v("DBHelper", "deleting account: " + id); String[] args = {Long.toString(id)}; db.delete(DB.EXPORT.TABLE, DB.EXPORT.ACCOUNT + " = ?", args); db.delete(DB.ACCOUNT.TABLE, "_id = ?", args); @@ -601,7 +601,7 @@ public static ContentValues[] toArray(Cursor c) { } public static void deleteActivity(SQLiteDatabase db, long id) { - Log.e("DBHelper", "deleting activity: " + id); + Log.v("DBHelper", "deleting activity: " + id); String[] args = {Long.toString(id)}; db.delete(DB.EXPORT.TABLE, DB.EXPORT.ACTIVITY + " = ?", args); db.delete(DB.LOCATION.TABLE, DB.LOCATION.ACTIVITY + " = ?", args); diff --git a/app/src/main/org/runnerup/export/DefaultSynchronizer.java b/app/src/main/org/runnerup/export/DefaultSynchronizer.java index 734306ce6..2ec40ca9a 100644 --- a/app/src/main/org/runnerup/export/DefaultSynchronizer.java +++ b/app/src/main/org/runnerup/export/DefaultSynchronizer.java @@ -98,7 +98,7 @@ public void init(ContentValues config) { @NonNull @Override public Intent getAuthIntent(AppCompatActivity a) { - Log.e(getName(), "getAuthIntent: getAuthIntent must be implemented for OAUTH2"); + Log.i(getName(), "getAuthIntent: getAuthIntent must be implemented for OAUTH2"); return new Intent(); } @@ -158,7 +158,7 @@ public final Status download(SQLiteDatabase db, SyncActivityItem item) { } ActivityEntity download(SyncActivityItem item) { - Log.e(Constants.LOG, "No download method implemented for the synchronizer " + getName()); + Log.i(Constants.LOG, "No download method implemented for the synchronizer " + getName()); return null; } diff --git a/app/src/main/org/runnerup/export/FileSynchronizer.java b/app/src/main/org/runnerup/export/FileSynchronizer.java index b1cd3f322..91ef1a0b0 100644 --- a/app/src/main/org/runnerup/export/FileSynchronizer.java +++ b/app/src/main/org/runnerup/export/FileSynchronizer.java @@ -258,7 +258,7 @@ private OutputStream getOutputStream(String fileName, String mimeType) throws IO final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Uri uri = resolver.insert(contentUri, contentValues); if (uri == null) { - Log.w(getName(), "No uri: " + contentUri + " " + fileName); + Log.i(getName(), "No uri: " + contentUri + " " + fileName); return null; } return resolver.openOutputStream(uri); @@ -266,7 +266,7 @@ private OutputStream getOutputStream(String fileName, String mimeType) throws IO String path = new File(mPath).getAbsolutePath() + File.separator + fileName; if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - Log.w(getName(), "No permission to write to: " + path); + Log.i(getName(), "No permission to write to: " + path); return null; } File file = new File(path); diff --git a/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java b/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java index 6be182239..bff3afc69 100644 --- a/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java +++ b/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java @@ -411,7 +411,7 @@ public Status upload(SQLiteDatabase db, final long mID) { Exception ex; try { URL newurl = new URL(REST_URL + fitnessActivitiesUrl); - // Log.e(Constants.LOG, "url: " + newurl.toString()); + // Log.d(Constants.LOG, "url: " + newurl.toString()); conn = (HttpURLConnection) newurl.openConnection(); conn.setDoOutput(true); conn.setRequestMethod(RequestMethod.POST.name()); diff --git a/app/src/main/org/runnerup/export/RunningAHEADSynchronizer.java b/app/src/main/org/runnerup/export/RunningAHEADSynchronizer.java index 5e6e0b193..31ff66a22 100644 --- a/app/src/main/org/runnerup/export/RunningAHEADSynchronizer.java +++ b/app/src/main/org/runnerup/export/RunningAHEADSynchronizer.java @@ -232,7 +232,7 @@ public Status upload(SQLiteDatabase db, final long mID) { out.close(); int responseCode = conn.getResponseCode(); String amsg = conn.getResponseMessage(); - Log.e(getName(), "code: " + responseCode + ", amsg: " + amsg); + Log.d(getName(), "code: " + responseCode + ", amsg: " + amsg); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); JSONObject obj = SyncHelper.parse(in); @@ -252,7 +252,7 @@ public Status upload(SQLiteDatabase db, final long mID) { } } if (!found) { - Log.e(getName(), "Unhandled response from RunningAHEADSynchronizer: " + obj); + Log.i(getName(), "Unhandled response from RunningAHEADSynchronizer: " + obj); } if (responseCode == HttpURLConnection.HTTP_OK && found) { conn.disconnect(); diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index f669b6f26..2e7ffd033 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -160,7 +160,7 @@ public long load(String synchronizerName) { @SuppressWarnings("null") public Synchronizer add(ContentValues config) { if (config == null) { - Log.e(getClass().getName(), "Add null!"); + Log.w(getClass().getName(), "Add null!"); if (BuildConfig.DEBUG) { throw new AssertionError(); } @@ -169,7 +169,7 @@ public Synchronizer add(ContentValues config) { String synchronizerName = config.getAsString(DB.ACCOUNT.NAME); if (synchronizerName == null) { - Log.e(getClass().getName(), "name not found!"); + Log.w(getClass().getName(), "name not found!"); return null; } if (synchronizers.containsKey(synchronizerName)) { @@ -196,7 +196,7 @@ public Synchronizer add(ContentValues config) { } else if (synchronizerName.contentEquals(EndurainSynchronizer.NAME)) { synchronizer = new EndurainSynchronizer(simplifier); } else { - Log.e(getClass().getName(), "synchronizer does not exist: " + synchronizerName); + Log.w(getClass().getName(), "synchronizer does not exist: " + synchronizerName); } if (synchronizer != null) { @@ -209,7 +209,7 @@ public Synchronizer add(ContentValues config) { synchronizers.put(synchronizerName, synchronizer); synchronizersById.put(synchronizer.getId(), synchronizer); } else { - Log.e(getClass().getName(), "Synchronizer not found for " + synchronizerName); + Log.w(getClass().getName(), "Synchronizer not found for " + synchronizerName); try { long synchronizerId = Long.parseLong(config.getAsString(DB.PRIMARY_KEY)); DBHelper.deleteAccount(mDB, synchronizerId); @@ -900,14 +900,14 @@ protected Synchronizer.Status doInBackground(String... params0) { synchronizer.downloadWorkout(w, ref.workoutKey); if (w != f) { if (!compareFiles(w, f)) { - Log.e(getClass().getName(), "overwriting " + f.getPath() + " with " + w.getPath()); + Log.w(getClass().getName(), "overwriting " + f.getPath() + " with " + w.getPath()); // TODO dialog //noinspection ResultOfMethodCallIgnored f.delete(); //noinspection ResultOfMethodCallIgnored w.renameTo(f); } else { - Log.e(getClass().getName(), "file identical...deleting temporary " + w.getPath()); + Log.i(getClass().getName(), "file identical...deleting temporary " + w.getPath()); //noinspection ResultOfMethodCallIgnored w.delete(); } diff --git a/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java b/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java index 8c60ab3d6..14ef4a0a7 100644 --- a/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java +++ b/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java @@ -172,7 +172,7 @@ public void onPageFinished(WebView view, String url) { } if (e != null) { - Log.e(getClass().getName(), "e: " + e); + Log.d(getClass().getName(), "e: " + e); Intent res = new Intent().putExtra("error", e); OAuth2Activity.this.setResult(AppCompatActivity.RESULT_CANCELED, res); OAuth2Activity.this.finish(); diff --git a/app/src/main/org/runnerup/tracker/Tracker.java b/app/src/main/org/runnerup/tracker/Tracker.java index 4163f54cb..f5cf7d138 100644 --- a/app/src/main/org/runnerup/tracker/Tracker.java +++ b/app/src/main/org/runnerup/tracker/Tracker.java @@ -206,7 +206,7 @@ public void run(TrackerComponent component, TrackerComponent.ResultCode resultCo state.set(TrackerState.INITIALIZED); } - Log.e(getClass().getName(), "state.set(" + getState() + ")"); + Log.d(getClass().getName(), "state.set(" + getState() + ")"); handleNextState(); } }; @@ -251,14 +251,14 @@ private void handleNextState() { } public void connect() { - Log.e(getClass().getName(), "Tracker.connect() - state: " + state.get()); + Log.d(getClass().getName(), "Tracker.connect() - state: " + state.get()); switch (state.get()) { case INIT: setup(); case INITIALIZING: case CLEANUP: nextState = TrackerState.CONNECTED; - Log.e(getClass().getName(), " => nextState: " + nextState); + Log.d(getClass().getName(), " => nextState: " + nextState); return; case INITIALIZED: break; @@ -321,7 +321,7 @@ private long createActivity(int sport) { tmp.put(DB.LOCATION.LAP, 0); // always start with lap 0 mDBWriter = new PersistentGpsLoggerListener(mDB, DB.LOCATION.TABLE, tmp, logGpxAccuracy); } catch (IllegalStateException ex) { - Log.e(getClass().getName(), "Query failed:", ex); + Log.i(getClass().getName(), "Query failed:", ex); } return mActivityId; } diff --git a/app/src/main/org/runnerup/tracker/component/TrackerComponentCollection.java b/app/src/main/org/runnerup/tracker/component/TrackerComponentCollection.java index 8587a02cd..61e1e3f82 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerComponentCollection.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerComponentCollection.java @@ -211,23 +211,23 @@ private ResultCode forEach( handler.post( () -> { synchronized (components) { - Log.e( + Log.d( getName(), component1.getName() + " " + msg + " => " + resultCode); if (!pending.containsKey(key)) return; TrackerComponent check = pending.remove(key); if (BuildConfig.DEBUG && check != component1) { - Log.e(getName(), component1.getName() + " != " + check.getName()); + Log.w(getName(), component1.getName() + " != " + check.getName()); throw new AssertionError(); } components.put(key, new Pair<>(component1, resultCode)); if (pending.isEmpty()) { - Log.e(getName(), " => runCallback()"); + Log.d(getName(), " => runCallback()"); callback.run(TrackerComponentCollection.this, getResult(components)); } } }), context); - Log.e(getName(), component.getName() + " " + msg + " => " + res); + Log.d(getName(), component.getName() + " " + msg + " => " + res); if (res != ResultCode.RESULT_PENDING) { components.put(key, new Pair<>(component, res)); } else { @@ -237,7 +237,7 @@ private ResultCode forEach( } if (!pending.isEmpty()) return ResultCode.RESULT_PENDING; else { - Log.e(getName(), " => return directly"); + Log.d(getName(), " => return directly"); return getResult(components); } } diff --git a/app/src/main/org/runnerup/tracker/component/TrackerElevation.java b/app/src/main/org/runnerup/tracker/component/TrackerElevation.java index 191231254..51b3e7a5c 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerElevation.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerElevation.java @@ -65,7 +65,7 @@ GeoidAdjust GetAltitudeAdjust(Context context) { Geoid.init(context.getAssets().open("egm96-delta.dat")); return new GeoidAdjust(); } catch (IOException e) { - Log.e("TrackerElevation", "Altitude correction " + e); + Log.i("TrackerElevation", "Altitude correction " + e); } return null; } diff --git a/app/src/main/org/runnerup/util/Formatter.java b/app/src/main/org/runnerup/util/Formatter.java index 51f715433..a4c78e712 100644 --- a/app/src/main/org/runnerup/util/Formatter.java +++ b/app/src/main/org/runnerup/util/Formatter.java @@ -198,7 +198,7 @@ public static boolean getUseMetric(Resources res, SharedPreferences prefs, Edito private static boolean guessDefaultUnit(Resources res, Editor editor) { String countryCode = Locale.getDefault().getCountry(); - Log.e("Formatter", "guessDefaultUnit: countryCode: " + countryCode); + Log.i("Formatter", "guessDefaultUnit: countryCode: " + countryCode); if (countryCode.equals("")) return true; // km; String key = res.getString(R.string.pref_unit); if ("US".contentEquals(countryCode) || "GB".contentEquals(countryCode)) { diff --git a/app/src/main/org/runnerup/util/GraphWrapper.java b/app/src/main/org/runnerup/util/GraphWrapper.java index c163f6270..429e013c8 100644 --- a/app/src/main/org/runnerup/util/GraphWrapper.java +++ b/app/src/main/org/runnerup/util/GraphWrapper.java @@ -455,7 +455,7 @@ void KolmogorovZurbenko(int n, int len) { public void complete(final GraphView graphView) { avg_velocity /= velocityList.size(); - Log.e(getClass().getName(), "graph: " + velocityList.size() + " points"); + Log.d(getClass().getName(), "graph: " + velocityList.size() + " points"); boolean smoothData = PreferenceManager.getDefaultSharedPreferences(graphView.getContext()) @@ -465,7 +465,7 @@ public void complete(final GraphView graphView) { .getResources() .getString(R.string.pref_pace_graph_smoothing), true); - if (velocityList.size() > 0 && smoothData) { + if (!velocityList.isEmpty() && smoothData) { GraphFilter f = new GraphFilter(velocityList); final String defaultFilterList = graphView.getContext().getResources().getString(R.string.mm31kz513sg5); @@ -478,35 +478,35 @@ public void complete(final GraphView graphView) { .getString(R.string.pref_pace_graph_smoothing_filters), defaultFilterList); final String[] filters = filterList.split(";"); - System.err.print("Applying filters(" + filters.length + ", >" + filterList + "<):"); + StringBuilder s = new StringBuilder("Applying filters(" + filters.length + ", >" + filterList + "<):"); for (String filter : filters) { int[] args = getArgs(filter); if (filter.startsWith("mm")) { if (args.length == 1) { f.movingMedian(args[0]); - System.err.print(" mm(" + args[0] + ")"); + s.append(" mm(").append(args[0]).append(")"); } } else if (filter.startsWith("ma")) { if (args.length == 1) { f.movingAvergage(args[0]); - System.err.print(" ma(" + args[0] + ")"); + s.append(" ma(").append(args[0]).append(")"); } } else if (filter.startsWith("kz")) { if (args.length == 2) { f.KolmogorovZurbenko(args[0], args[1]); - System.err.print(" kz(" + args[0] + "," + args[1] + ")"); + s.append(" kz(").append(args[0]).append(",").append(args[1]).append(")"); } } else if (filter.startsWith("sg")) { if (args.length == 1 && args[0] == 5) { f.SavitzkyGolay5(); - System.err.print(" sg(5)"); + s.append(" sg(5)"); } else if (args.length == 1 && args[0] == 7) { f.SavitzkyGolay7(); - System.err.print(" sg(7)"); + s.append(" sg(7)"); } } } - Log.e(getClass().getName(), ""); + Log.d(getClass().getName(), s.toString()); f.complete(); } LineGraphSeries graphViewData = @@ -547,16 +547,16 @@ public void complete(final GraphView graphView) { }); if (showHRZhist) { - System.err.print("HR Zones:"); + StringBuilder s = new StringBuilder("HR Zones:"); double sum = 0; for (double aHrzHist : hrzHist) { sum += aHrzHist; } for (int i = 0; i < hrzHist.length; i++) { hrzHist[i] = hrzHist[i] / sum; - System.err.print(" " + hrzHist[i]); + s.append(" ").append(hrzHist[i]); } - Log.e(getClass().getName(), "\n"); + Log.d(getClass().getName(), s.toString()); hrzonesBar.pushHrzData(hrzHist); } } @@ -662,8 +662,8 @@ protected GraphProducer doInBackground(LoadParam... params) { graphData.clearSmooth(xAxis.getX(tot_distance, tot_time)); ll.close(); - Log.e(getClass().getName(), "Finished loading " + cnt + " points" - + " => " + graphData.velocityList.size() + " points"); + // Log.e(getClass().getName(), "Finished loading " + cnt + " points" + // + " => " + graphData.velocityList.size() + " points"); return graphData; } diff --git a/app/src/main/org/runnerup/util/HRZones.java b/app/src/main/org/runnerup/util/HRZones.java index ac58ed568..c8d0a686f 100644 --- a/app/src/main/org/runnerup/util/HRZones.java +++ b/app/src/main/org/runnerup/util/HRZones.java @@ -50,11 +50,11 @@ public void reload() { zones = null; } if (zones != null) { - System.err.print("loaded: (" + str + ")"); + StringBuilder s = new StringBuilder("loaded: (" + str + ")"); for (int zone : zones) { - System.err.print(" " + zone); + s.append(" ").append(zone); } - Log.e(getClass().getName(), ""); + Log.d(getClass().getName(), s.toString()); } } @@ -80,7 +80,7 @@ public double getZone(double value) { double lo = (z == 0) ? 0 : zones[z - 1]; double hi = zones[z]; double add = (value - lo) / (hi - lo); - Log.e( + Log.d( getClass().getName(), "value: " + value + ", z: " + z + ", lo: " + lo + ", hi: " + hi + ", add: " + add); return z + add; diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index 6061e33ab..fc260237a 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -318,7 +318,7 @@ public WindowInsetsCompat onApplyWindowInsets( LinearLayout hrzonesBarLayout = findViewById(R.id.hrzonesBarLayout); boolean use_distance_as_x = !Sport.isWithoutGps(sport.getValueInt()); graphWrapper = new GraphWrapper(this, graphTabLayout, hrzonesBarLayout, - formatter, mDB, mID, use_distance_as_x); + formatter, mDB, mID, use_distance_as_x); if (this.mode == MODE_SAVE) { resumeButton.setOnClickListener(resumeButtonClick); diff --git a/app/src/main/org/runnerup/view/HRZonesActivity.java b/app/src/main/org/runnerup/view/HRZonesActivity.java index 8e18f4cc7..7e7097154 100644 --- a/app/src/main/org/runnerup/view/HRZonesActivity.java +++ b/app/src/main/org/runnerup/view/HRZonesActivity.java @@ -244,7 +244,7 @@ private void load() { EditText hi = zones.get(2 * zone + 1); lo.setText(String.format(Locale.getDefault(), "%d", values.first)); hi.setText(String.format(Locale.getDefault(), "%d", values.second)); - Log.e( + Log.i( getClass().getName(), "loaded " + (zone + 1) + " " + values.first + "-" + values.second); } @@ -291,13 +291,14 @@ private void recomputeZones() { private void saveHR() { try { Vector vals = new Vector<>(); - System.err.print("saving: "); + StringBuilder s = new StringBuilder("saving:"); for (int i = 0; i < zones.size(); i += 2) { vals.add(Integer.valueOf(zones.get(i).getText().toString())); - System.err.print(" " + vals.lastElement()); + s.append(" ").append(vals.lastElement()); } vals.add(Integer.valueOf(zones.lastElement().getText().toString())); - Log.e(getClass().getName(), " " + vals.lastElement()); + s.append(" ").append(vals.lastElement()); + Log.d(getClass().getName(), s.toString()); hrZones.save(vals); } catch (Exception ex) { } diff --git a/app/src/main/org/runnerup/view/HRZonesBar.java b/app/src/main/org/runnerup/view/HRZonesBar.java index e26dce8bd..1d2f404dd 100644 --- a/app/src/main/org/runnerup/view/HRZonesBar.java +++ b/app/src/main/org/runnerup/view/HRZonesBar.java @@ -79,7 +79,7 @@ public void onDraw(Canvas canvas) { float totalWidth = getWidth(); if (totalWidth <= 0 || calculatedBarHeight < 10) { - Log.e(getClass().getName(), "Not enough space to display the heart-rate zone bar"); + Log.i(getClass().getName(), "Not enough space to display the heart-rate zone bar"); activity.findViewById(R.id.hrzonesBarLayout).setVisibility(View.GONE); return; } diff --git a/app/src/main/org/runnerup/view/MainLayout.java b/app/src/main/org/runnerup/view/MainLayout.java index 34c71d692..708dccdc6 100644 --- a/app/src/main/org/runnerup/view/MainLayout.java +++ b/app/src/main/org/runnerup/view/MainLayout.java @@ -112,7 +112,7 @@ public void onCreate(Bundle savedInstanceState) { // clear basicTargetType between application startup/shutdown pref.edit().remove(getString(R.string.pref_basic_target_type)).apply(); - Log.e( + Log.i( getClass().getName(), "app-version: " + versionCode + ", upgradeState: " + upgradeState + ", km: " + km); @@ -295,7 +295,7 @@ private void handleBundled(AssetManager mgr, String srcBase, String dstBase) { // Normal, src is directory for first call } - Log.v(getClass().getName(), "Found: " + src + ", " + dst + ", isFile: " + isFile); + Log.d(getClass().getName(), "Found: " + src + ", " + dst + ", isFile: " + isFile); if (!isFile) { // The request is hierarchical, source is still on a directory level @@ -303,7 +303,7 @@ private void handleBundled(AssetManager mgr, String srcBase, String dstBase) { //noinspection ResultOfMethodCallIgnored dstDir.mkdir(); if (!dstDir.isDirectory()) { - Log.w( + Log.i( getClass().getName(), "Failed to copy " + src + " as \"" + dstBase + "\" is not a directory!"); continue; @@ -313,7 +313,7 @@ private void handleBundled(AssetManager mgr, String srcBase, String dstBase) { // Source is a file, ready to copy File dstFile = new File(dst); if (dstFile.isDirectory() || dstFile.isFile()) { - Log.v( + Log.d( getClass().getName(), "Skip: " + dst @@ -328,13 +328,13 @@ private void handleBundled(AssetManager mgr, String srcBase, String dstBase) { String key = "install_bundled_" + add; SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); if (pref.contains(key)) { - Log.v(getClass().getName(), "Skip already existing pref: " + key); + Log.d(getClass().getName(), "Skip already existing pref: " + key); continue; } pref.edit().putBoolean(key, true).apply(); - Log.v(getClass().getName(), "Copying: " + dst); + Log.d(getClass().getName(), "Copying: " + dst); InputStream input = null; try { input = mgr.open(src); diff --git a/app/src/main/org/runnerup/view/ManualActivity.java b/app/src/main/org/runnerup/view/ManualActivity.java index eb65a4d01..1d7a707bb 100644 --- a/app/src/main/org/runnerup/view/ManualActivity.java +++ b/app/src/main/org/runnerup/view/ManualActivity.java @@ -110,18 +110,18 @@ public void onActivityResult( super.onActivityResult(requestCode, resultCode, data); if (data != null) { if (data.getStringExtra("url") != null) - Log.e( + Log.d( getClass().getName(), "data.getStringExtra(\"url\") => " + data.getStringExtra("url")); if (data.getStringExtra("ex") != null) - Log.e(getClass().getName(), "data.getStringExtra(\"ex\") => " + data.getStringExtra("ex")); + Log.d(getClass().getName(), "data.getStringExtra(\"ex\") => " + data.getStringExtra("ex")); if (data.getStringExtra("obj") != null) - Log.e( + Log.d( getClass().getName(), "data.getStringExtra(\"obj\") => " + data.getStringExtra("obj")); } } void setManualPace(String distance, String duration) { - Log.e(getClass().getName(), "distance: >" + distance + "< duration: >" + duration + "<"); + Log.d(getClass().getName(), "distance: >" + distance + "< duration: >" + duration + "<"); double dist = SafeParse.parseDouble(distance, 0); // convert to meters long seconds = SafeParse.parseSeconds(duration, 0); if (seconds == 0) { diff --git a/app/src/main/org/runnerup/view/RunActivity.java b/app/src/main/org/runnerup/view/RunActivity.java index f6700f654..65cfb579c 100644 --- a/app/src/main/org/runnerup/view/RunActivity.java +++ b/app/src/main/org/runnerup/view/RunActivity.java @@ -198,7 +198,7 @@ public void handleOnBackPressed() { @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); - Log.e(getClass().getName(), "onConfigurationChange => do NOTHING!!"); + Log.d(getClass().getName(), "onConfigurationChange => do NOTHING!!"); } @Override diff --git a/app/src/main/org/runnerup/view/StartFragment.java b/app/src/main/org/runnerup/view/StartFragment.java index b7c24cb93..cea3702ac 100644 --- a/app/src/main/org/runnerup/view/StartFragment.java +++ b/app/src/main/org/runnerup/view/StartFragment.java @@ -462,7 +462,7 @@ public void onPause() { if (mTracker != null && ((mTracker.getState() == TrackerState.INITIALIZED) || (mTracker.getState() == TrackerState.INITIALIZING))) { - Log.e(getClass().getName(), "mTracker.reset()"); + Log.i(getClass().getName(), "mTracker.reset()"); mTracker.reset(); } } @@ -533,7 +533,7 @@ private void onGpsTrackerBound() { if (!missingEssentialPermission && getAutoStartGps()) { startGps(); } else { - Log.e(getClass().getName(), "onGpsTrackerBound state: " + mTracker.getState()); + Log.d(getClass().getName(), "onGpsTrackerBound state: " + mTracker.getState()); switch (mTracker.getState()) { case INIT: case CLEANUP: @@ -561,7 +561,7 @@ public boolean getAutoStartGps() { } private void startGps() { - Log.v(getClass().getName(), "StartFragment.startGps()"); + Log.d(getClass().getName(), "StartFragment.startGps()"); if (!sportWithoutGps) { if (!mGpsStatus.isEnabled()) { startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); @@ -580,7 +580,7 @@ private void startGps() { } public void stopGps() { - Log.e(getClass().getName(), "StartFragment.stopGps() skipStop: " + this.runActivityPending); + Log.d(getClass().getName(), "StartFragment.stopGps() skipStop: " + this.runActivityPending); if (runActivityPending) { return; } @@ -1280,12 +1280,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (data != null) { if (data.getStringExtra("url") != null) - Log.e( + Log.d( getClass().getName(), "data.getStringExtra(\"url\") => " + data.getStringExtra("url")); if (data.getStringExtra("ex") != null) - Log.e(getClass().getName(), "data.getStringExtra(\"ex\") => " + data.getStringExtra("ex")); + Log.d(getClass().getName(), "data.getStringExtra(\"ex\") => " + data.getStringExtra("ex")); if (data.getStringExtra("obj") != null) - Log.e( + Log.d( getClass().getName(), "data.getStringExtra(\"obj\") => " + data.getStringExtra("obj")); } if (requestCode == START_ACTIVITY) { diff --git a/app/src/main/org/runnerup/workout/EndOfLapSuppression.java b/app/src/main/org/runnerup/workout/EndOfLapSuppression.java index 5634a56c0..7ce88f85b 100644 --- a/app/src/main/org/runnerup/workout/EndOfLapSuppression.java +++ b/app/src/main/org/runnerup/workout/EndOfLapSuppression.java @@ -100,7 +100,7 @@ private boolean suppressInterval(Trigger trigger, Workout w) { double distance = w.getDistance(Scope.LAP); if ((distance - lapDuration) == lapDistanceLimit) { - Log.e( + Log.i( getClass().getName(), "suppressing trigger! distance: " + distance + ", lapDistance: " + lapDuration); return true; diff --git a/app/src/main/org/runnerup/workout/TargetTrigger.java b/app/src/main/org/runnerup/workout/TargetTrigger.java index eec8899e5..fae5b0518 100644 --- a/app/src/main/org/runnerup/workout/TargetTrigger.java +++ b/app/src/main/org/runnerup/workout/TargetTrigger.java @@ -111,16 +111,15 @@ public boolean onTick(Workout w) { for (int i = 0; i < elapsed_seconds; i++) { addObservation(val_now); } - // Log.e(getName(), "val_now: " + val_now + " elapsed: " + - // elapsed_seconds); + // Log.d(getName(), "val_now: " + val_now + " elapsed: " + elapsed_seconds); if (graceCount > 0) { // only emit coaching ever so often - // Log.e(getName(), "graceCount: " + graceCount); + // Log.d(getName(), "graceCount: " + graceCount); graceCount -= elapsed_seconds; } else { double avg = getValue(); double cmp = range.compare(avg); - // Log.e(getName(), " => avg: " + avg + " => cmp: " + cmp); + // Log.d(getName(), " => avg: " + avg + " => cmp: " + cmp); if (cmp == 0) { graceCount = minGraceCount; return false; diff --git a/app/src/main/org/runnerup/workout/Trigger.java b/app/src/main/org/runnerup/workout/Trigger.java index 1c28fbce4..ea36450a2 100644 --- a/app/src/main/org/runnerup/workout/Trigger.java +++ b/app/src/main/org/runnerup/workout/Trigger.java @@ -50,7 +50,7 @@ public void onEnd(Workout s) { void fire(Workout w) { for (TriggerSuppression s : triggerSuppression) { if (s.suppress(this, w)) { - Log.e(getClass().getName(), "trigger: " + this + "suppressed by: " + s); + Log.v(getClass().getName(), "trigger: " + this + "suppressed by: " + s); return; } } diff --git a/app/src/main/org/runnerup/workout/WorkoutBuilder.java b/app/src/main/org/runnerup/workout/WorkoutBuilder.java index 3eec43d3f..502bdb941 100644 --- a/app/src/main/org/runnerup/workout/WorkoutBuilder.java +++ b/app/src/main/org/runnerup/workout/WorkoutBuilder.java @@ -409,7 +409,7 @@ private static Trigger hasEndOfLapTrigger(List triggers) { private static void checkDuplicateTriggers(Step step) { if (hasEndOfLapTrigger(step.triggers) != null) { - Log.e("WorkoutBuilder", "hasEndOfLapTrigger()"); + Log.d("WorkoutBuilder", "hasEndOfLapTrigger()"); /* * The end of lap trigger can be a duplicate of a distance based interval trigger * 1) in a step with distance duration, that is a multiple of the interval-distance diff --git a/app/src/main/org/runnerup/workout/WorkoutSerializer.java b/app/src/main/org/runnerup/workout/WorkoutSerializer.java index 72d91fdc4..b57f2d54c 100644 --- a/app/src/main/org/runnerup/workout/WorkoutSerializer.java +++ b/app/src/main/org/runnerup/workout/WorkoutSerializer.java @@ -439,7 +439,7 @@ public static Workout readFile(Context ctx, String name) throws FileNotFoundException, JSONException { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); File fin = getFile(ctx, name); - Log.e("WorkoutSerializer", "reading " + fin.getPath()); + Log.d("WorkoutSerializer", "reading " + fin.getPath()); Workout w = readJSON(new FileReader(fin)); w.sport = @@ -452,7 +452,7 @@ public static Workout readFile(Context ctx, String name) public static void writeFile(Context ctx, String name, Workout workout) throws IOException, JSONException { File fout = getFile(ctx, name); - Log.e("WorkoutSerializer", "writing " + fout.getPath()); + Log.v("WorkoutSerializer", "writing " + fout.getPath()); writeJSON(new FileWriter(fout), workout); } diff --git a/app/src/main/org/runnerup/workout/feedback/RUTextToSpeech.java b/app/src/main/org/runnerup/workout/feedback/RUTextToSpeech.java index c40e6cba5..bc2701856 100644 --- a/app/src/main/org/runnerup/workout/feedback/RUTextToSpeech.java +++ b/app/src/main/org/runnerup/workout/feedback/RUTextToSpeech.java @@ -70,11 +70,11 @@ public RUTextToSpeech(TextToSpeech tts, boolean mute_, Context context) { case TextToSpeech.LANG_COUNTRY_AVAILABLE: case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: res = tts.setLanguage(locale); - Log.e(getClass().getName(), "setLanguage(" + locale.getDisplayLanguage() + ") => " + res); + Log.d(getClass().getName(), "setLanguage(" + locale.getDisplayLanguage() + ") => " + res); break; case TextToSpeech.LANG_MISSING_DATA: case TextToSpeech.LANG_NOT_SUPPORTED: - Log.e( + Log.v( getClass().getName(), "setLanguage(" + locale.getDisplayLanguage() + ") => MISSING: " + res); break; @@ -109,14 +109,14 @@ int speak(String text, UtterancePrio prio, boolean flush, HashMap { if (dataItem != null) { wearNode = dataItem.getUri().getHost(); - Log.e(getName(), "getDataItem => wearNode:" + wearNode); + Log.d(getName(), "getDataItem => wearNode:" + wearNode); } }); } @@ -231,12 +231,12 @@ public void onBind(HashMap bindValues) { case WORKOUT_TYPE.INTERVAL: case WORKOUT_TYPE.ADVANCED: initIntervalScreens(); - Log.e("TrackerWear::onBind()", "initIntervalScreens()"); + Log.d("TrackerWear::onBind()", "initIntervalScreens()"); break; default: case WORKOUT_TYPE.BASIC: initBasicScreens(); - Log.e("TrackerWear::onBind()", "initBasicScreens()"); + Log.d("TrackerWear::onBind()", "initBasicScreens()"); break; } } @@ -249,7 +249,7 @@ public void onStart() { } private void setTrackerState(TrackerState val) { - Log.e(getName(), "setTrackerState(" + val + ")"); + Log.d(getName(), "setTrackerState(" + val + ")"); Bundle b = new Bundle(); b.putInt(Wear.TrackerState.STATE, val.getValue()); setData(Wear.Path.TRACKER_STATE, b); @@ -433,7 +433,7 @@ public boolean isConnected() { @Override public void onMessageReceived(final MessageEvent messageEvent) { - Log.e(getName(), "onMessageReceived: " + messageEvent); + Log.d(getName(), "onMessageReceived: " + messageEvent); // note: skip state checking, do that in receiver instead if (Wear.Path.MSG_CMD_WORKOUT_PAUSE.contentEquals(messageEvent.getPath())) { sendLocalBroadcast(Intents.PAUSE_WORKOUT); @@ -497,7 +497,7 @@ private void clearData(boolean self) { @Override public void onDataChanged(final DataEventBuffer dataEvents) { for (DataEvent ev : dataEvents) { - Log.e(getName(), "onDataChanged: " + ev.getDataItem().getUri()); + Log.d(getName(), "onDataChanged: " + ev.getDataItem().getUri()); String path = ev.getDataItem().getUri().getPath(); if (Constants.Wear.Path.WEAR_NODE_ID.contentEquals(path)) { setWearNode(ev); diff --git a/hrdevice/src/org/runnerup/hr/Bt20Base.java b/hrdevice/src/org/runnerup/hr/Bt20Base.java index 49c937512..e74fbb717 100644 --- a/hrdevice/src/org/runnerup/hr/Bt20Base.java +++ b/hrdevice/src/org/runnerup/hr/Bt20Base.java @@ -28,6 +28,8 @@ import android.os.Build; import android.os.Handler; import android.os.SystemClock; +import android.util.Log; + import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import java.io.IOException; @@ -64,7 +66,7 @@ public static boolean startEnableIntentImpl(AppCompatActivity activity, int requ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { - System.err.println("No BLUETOOTH_CONNECT permission in startEnableIntentImpl"); + Log.d(Bt20Base.class.getName(), "No BLUETOOTH_CONNECT permission in startEnableIntentImpl"); return false; } activity.startActivityForResult( diff --git a/hrdevice/src/org/runnerup/hr/BtHRBase.java b/hrdevice/src/org/runnerup/hr/BtHRBase.java index ef8ea600c..19fe5b52a 100644 --- a/hrdevice/src/org/runnerup/hr/BtHRBase.java +++ b/hrdevice/src/org/runnerup/hr/BtHRBase.java @@ -18,6 +18,8 @@ import android.os.Handler; import android.os.Looper; +import android.util.Log; + import java.util.UUID; abstract class BtHRBase implements HRProvider { @@ -44,6 +46,6 @@ void log(final String msg) { if (hrClient != null) hrClient.log(BtHRBase.this, msg); }); } - } else System.err.println(msg); + } else Log.d(getClass().getName(), msg); } } diff --git a/hrdevice/src/org/runnerup/hr/HRManager.java b/hrdevice/src/org/runnerup/hr/HRManager.java index 57d0633a5..97836f638 100644 --- a/hrdevice/src/org/runnerup/hr/HRManager.java +++ b/hrdevice/src/org/runnerup/hr/HRManager.java @@ -83,7 +83,7 @@ public static HRProvider getHRProvider(Context ctx, String src) { } private static HRProvider getHRProviderImpl(Context ctx, String src) { - System.err.println("getHRProvider(" + src + ")"); + Log.d(HRManager.class.getName(), "getHRProvider(" + src + ")"); if (src.contentEquals(AndroidBLEHRProvider.NAME)) { if (!AndroidBLEHRProvider.checkLibrary(ctx)) return null; return new AndroidBLEHRProvider(ctx); diff --git a/hrdevice/src/org/runnerup/hr/RetryingHRProviderProxy.java b/hrdevice/src/org/runnerup/hr/RetryingHRProviderProxy.java index 93a4a9ea4..a876bad6b 100644 --- a/hrdevice/src/org/runnerup/hr/RetryingHRProviderProxy.java +++ b/hrdevice/src/org/runnerup/hr/RetryingHRProviderProxy.java @@ -2,6 +2,8 @@ import android.os.Handler; import android.os.Looper; +import android.util.Log; + import androidx.appcompat.app.AppCompatActivity; /** @@ -326,7 +328,7 @@ private void log(final String msg) { + requestedState + ", " + msg; - System.err.println(res); + Log.d(getClass().getName(), res); if (client != null) { if (Looper.myLooper() == Looper.getMainLooper()) { client.log(this, msg); diff --git a/wear/src/main/java/org/runnerup/service/ListenerService.java b/wear/src/main/java/org/runnerup/service/ListenerService.java index c9afe46a3..e83ebcb19 100644 --- a/wear/src/main/java/org/runnerup/service/ListenerService.java +++ b/wear/src/main/java/org/runnerup/service/ListenerService.java @@ -24,6 +24,8 @@ import android.content.Context; import android.content.Intent; import android.os.Build; +import android.util.Log; + import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.wear.ongoing.OngoingActivity; @@ -54,7 +56,7 @@ public class ListenerService extends WearableListenerService { @Override public void onCreate() { super.onCreate(); - System.err.println("ListenerService.onCreate()"); + Log.d(getClass().getName(), "ListenerService.onCreate()"); mGoogleApiClient = new WearableClient(getApplicationContext()); mGoogleApiClient.readData(Constants.Wear.Path.WEAR_APP, dataItem -> { mainActivityRunning = (dataItem != null); @@ -81,7 +83,7 @@ public void onCreate() { @Override public void onDestroy() { super.onDestroy(); - System.err.println("ListenerService.onDestroy()"); + Log.d(getClass().getName(), "ListenerService.onDestroy()"); if (mGoogleApiClient != null) { mGoogleApiClient = null; } @@ -89,14 +91,14 @@ public void onDestroy() { @Override public int onStartCommand(Intent intent, int flags, int startId) { - System.err.println("ListenerService.onStart()"); + Log.d(getClass().getName(), "ListenerService.onStart()"); return super.onStartCommand(intent, flags, startId); } @Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent ev : dataEvents) { - System.err.println("onDataChanged: " + ev.getDataItem().getUri()); + Log.d(getClass().getName(), "onDataChanged: " + ev.getDataItem().getUri()); var type = ev.getType(); String path = ev.getDataItem().getUri().getPath(); if (!(type == DataEvent.TYPE_DELETED || type == DataEvent.TYPE_CHANGED)) { @@ -129,19 +131,19 @@ public void onDataChanged(DataEventBuffer dataEvents) { @Override public void onPeerConnected(Node peer) { if (BuildConfig.DEBUG) { - System.err.println("ListenerService.onPeerConnected: " + peer.getId()); + Log.d(getClass().getName(), "ListenerService.onPeerConnected: " + peer.getId()); } } @Override public void onPeerDisconnected(Node peer) { if (BuildConfig.DEBUG) { - System.err.println("ListenerService.onPeerDisconnected: " + peer.getId()); + Log.d(getClass().getName(), "ListenerService.onPeerDisconnected: " + peer.getId()); } } private void maybeShowNotification() { - System.err.println("mainActivityRunning=" + mainActivityRunning + + Log.d(getClass().getName(), "mainActivityRunning=" + mainActivityRunning + ", phoneApp=" + phoneApp + " ,phoneRunning=" + phoneRunning + " ,trackerState=" + trackerState); @@ -160,7 +162,7 @@ private void maybeShowNotification() { } if (mainActivityRunning == null || phoneRunning == null || trackerState == null) { - System.err.println("wait for read"); + Log.d(getClass().getName(), "wait for read"); return; } showNotification(); diff --git a/wear/src/main/java/org/runnerup/service/StateService.java b/wear/src/main/java/org/runnerup/service/StateService.java index d8fba28ad..ee54d52e0 100644 --- a/wear/src/main/java/org/runnerup/service/StateService.java +++ b/wear/src/main/java/org/runnerup/service/StateService.java @@ -21,6 +21,8 @@ import android.content.Intent; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; @@ -89,7 +91,7 @@ public void onConnectionFailed(@NonNull ConnectionResult result) {} mGoogleApiClient.connect(); this.headers.registerChangeListener(this); - System.err.println("StateService.onCreate()"); + Log.d(getClass().getName(), "StateService.onCreate()"); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") @@ -101,7 +103,7 @@ private void readData() { mDataClient.readData(Constants.Wear.Path.PHONE_NODE_ID, dataItem -> { if (dataItem != null ) { phoneNode = dataItem.getUri().getHost(); - System.err.println("getDataItem => phoneNode:" + phoneNode); + Log.d(getClass().getName(), "getDataItem => phoneNode:" + phoneNode); } }); mDataClient.readData(Constants.Wear.Path.TRACKER_STATE, dataItem -> { @@ -133,7 +135,7 @@ private void clearData() { @Override public void onDestroy() { - System.err.println("StateService.onDestroy()"); + Log.d(getClass().getName(), "StateService.onDestroy()"); trackerState.clearListeners(); if (mGoogleApiClient != null) { if (mGoogleApiClient.isConnected()) { @@ -191,14 +193,14 @@ public void onMessageReceived(MessageEvent messageEvent) { data = DataMap.fromByteArray(messageEvent.getData()).toBundle(); data.putLong(UPDATE_TIME, System.currentTimeMillis()); } else { - System.err.println("onMessageReceived: " + messageEvent); + Log.d(getClass().getName(), "onMessageReceived: " + messageEvent); } } @Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent ev : dataEvents) { - System.err.println("onDataChanged: " + ev.getDataItem().getUri()); + Log.d(getClass().getName(), "onDataChanged: " + ev.getDataItem().getUri()); String path = ev.getDataItem().getUri().getPath(); if (Constants.Wear.Path.PHONE_NODE_ID.contentEquals(path)) { setPhoneNode(ev); @@ -223,7 +225,7 @@ private void setHeaders(DataEvent ev) { if (ev.getType() == DataEvent.TYPE_CHANGED) { Bundle b = DataMapItem.fromDataItem(ev.getDataItem()).getDataMap().toBundle(); b.putLong(UPDATE_TIME, System.currentTimeMillis()); - System.err.println("setHeaders(): b=" + b); + Log.d(getClass().getName(), "setHeaders(): b=" + b); headers.set(b); } else { headers.set(null); diff --git a/wear/src/main/java/org/runnerup/view/MainActivity.java b/wear/src/main/java/org/runnerup/view/MainActivity.java index 65ee0a678..942777608 100644 --- a/wear/src/main/java/org/runnerup/view/MainActivity.java +++ b/wear/src/main/java/org/runnerup/view/MainActivity.java @@ -33,6 +33,7 @@ import android.support.wearable.view.DotsPageIndicator; import android.support.wearable.view.FragmentGridPagerAdapter; import android.support.wearable.view.GridViewPager; +import android.util.Log; import android.widget.LinearLayout; import com.google.android.gms.wearable.DataClient; import com.google.android.gms.wearable.PutDataRequest; @@ -221,12 +222,12 @@ private void update(TrackerState newValue) { private int getRowsForScreen(int col) { Bundle b = headers.get(); if (b == null) { - System.err.println("getRowsForScreen(): headers == null"); + Log.d(getClass().getName(), "getRowsForScreen(): headers == null"); return 1; } ArrayList screens = b.getIntegerArrayList(Wear.RunInfo.SCREENS); if (screens == null) { - System.err.println("getRowsForScreen(): screens == null"); + Log.d(getClass().getName(), "getRowsForScreen(): screens == null"); return 1; } if (col > screens.size()) return 1; @@ -236,12 +237,12 @@ private int getRowsForScreen(int col) { private int getScreensCount() { Bundle b = headers.get(); if (b == null) { - System.err.println("getScreensCount(): headers == null"); + Log.d(getClass().getName(), "getScreensCount(): headers == null"); return 1; } ArrayList screens = b.getIntegerArrayList(Wear.RunInfo.SCREENS); if (screens == null) { - System.err.println("getScreensCount(): screens == null"); + Log.d(getClass().getName(), "getScreensCount(): screens == null"); return 1; } return screens.size(); From a1e639f54b8f534cc415d3506ee2773e1d645c1e Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Mon, 22 Sep 2025 23:44:43 +0200 Subject: [PATCH 02/31] fix: common test compile --- .../org/runnerup/common/util/ValueModelTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/src/test/java/org/runnerup/common/util/ValueModelTest.java b/common/src/test/java/org/runnerup/common/util/ValueModelTest.java index 361fde67d..6006a3d3c 100644 --- a/common/src/test/java/org/runnerup/common/util/ValueModelTest.java +++ b/common/src/test/java/org/runnerup/common/util/ValueModelTest.java @@ -4,8 +4,8 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -71,7 +71,7 @@ public void shouldNotCallListenerIfValueIsNull() { sut.set(null); - verify(listener, never()).onValueChanged(eq(sut), anyObject(), anyObject()); + verify(listener, never()).onValueChanged(eq(sut), any(), any()); } @Test @@ -82,7 +82,7 @@ public void shouldNotCallListenerIfListenerIsRemoved() { sut.unregisterChangeListener(listener); sut.set(newValue); - verify(listener, never()).onValueChanged(eq(sut), anyObject(), anyObject()); + verify(listener, never()).onValueChanged(eq(sut), any(), any()); } @Test @@ -112,9 +112,9 @@ public void shouldNotCallListenersIfClearIsCalled() { sut.set(newValue); - verify(listener1, never()).onValueChanged(eq(sut), anyObject(), anyObject()); - verify(listener2, never()).onValueChanged(eq(sut), anyObject(), anyObject()); - verify(listener3, never()).onValueChanged(eq(sut), anyObject(), anyObject()); + verify(listener1, never()).onValueChanged(eq(sut), any(), any()); + verify(listener2, never()).onValueChanged(eq(sut), any(), any()); + verify(listener3, never()).onValueChanged(eq(sut), any(), any()); } @Test From dcff77821aabcf951c6793f14bd6dab93e88465b Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Sat, 11 Oct 2025 22:59:10 +0200 Subject: [PATCH 03/31] fix: agp 8.13.2 buildtools 36.1.0 Update a few dependencies Fix variable assignment in build.gradle lint Baseline update --- app/build.gradle | 60 +-- app/lint-baseline.xml | 17 +- build.gradle | 6 +- common/build.gradle | 38 +- common/lint-baseline.xml | 13 +- .../org/runnerup/wear/WearableClient.java | 43 +- hrdevice/build.gradle | 30 +- hrdevice/lint-baseline.xml | 388 +----------------- wear/build.gradle | 50 +-- wear/lint-baseline.xml | 2 +- .../org/runnerup/service/ListenerService.java | 12 +- 11 files changed, 159 insertions(+), 500 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 57999fc0e..316bc6695 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,11 +6,11 @@ def getGitHash = providers.exec { android { buildToolsVersion = rootProject.ext.buildToolsVersion - namespace 'org.runnerup' + namespace = 'org.runnerup' compileOptions { - sourceCompatibility JavaVersion.toVersion("17") - targetCompatibility JavaVersion.toVersion("17") + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") } sourceSets { @@ -44,13 +44,13 @@ android { flavorDimensions = [ "all" ] productFlavors { latest { - dimension "all" + dimension = "all" // multidexing support, min play support - minSdk rootProject.ext.minSdk - compileSdk rootProject.ext.compileSdk - targetSdk rootProject.ext.targetSdk - versionName rootProject.ext.versionName - versionCode rootProject.ext.latestBaseVersionCode + rootProject.ext.versionCode + minSdk = rootProject.ext.minSdk + compileSdk = rootProject.ext.compileSdk + targetSdk = rootProject.ext.targetSdk + versionName = rootProject.ext.versionName + versionCode = rootProject.ext.latestBaseVersionCode + rootProject.ext.versionCode } } @@ -60,7 +60,7 @@ android { // enable rootProject.ext.allowNonFree && gradle.startParameter.taskNames.contains("assembleLatestRelease") // relevant archs only - these are the only available anyway for newer NDK include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk true + universalApk = true } } @@ -86,22 +86,22 @@ android { versionNameSuffix = "-${getGitHash}" } release { - debuggable false - applicationIdSuffix "" + debuggable = false + applicationIdSuffix = "" - minifyEnabled rootProject.ext.allowNonFree - shrinkResources rootProject.ext.allowNonFree + minifyEnabled = rootProject.ext.allowNonFree + shrinkResources = rootProject.ext.allowNonFree proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard.txt' - signingConfig signingConfigs.release + signingConfig = signingConfigs.release } } lint { - baseline file('lint-baseline.xml') - checkReleaseBuilds true - lintConfig file('lint.xml') - showAll true - //textOutput 'stdout' - textReport true + baseline = file('lint-baseline.xml') + checkReleaseBuilds = true + lintConfig = file('lint.xml') + showAll = true + //textOutput = 'stdout' + textReport = true } bundle { language { @@ -110,21 +110,21 @@ android { } } buildFeatures { - aidl true - buildConfig true + aidl = true + buildConfig = true } androidResources { - generateLocaleConfig true + generateLocaleConfig = true } } repositories { google() mavenCentral() //MapBox GraphView - maven { url "https://oss.sonatype.org/content/groups/public/" } //pebblekit + maven { url = "https://oss.sonatype.org/content/groups/public/" } //pebblekit if (rootProject.ext.useMapBox) { maven { - url 'https://api.mapbox.com/downloads/v2/releases/maven' + url = 'https://api.mapbox.com/downloads/v2/releases/maven' authentication { basic(BasicAuthentication) } @@ -144,10 +144,10 @@ repositories { // Duplicate class kotlin.collections.jdk8 (from MapBox?) dependencies { constraints { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0") { because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") } - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0") { because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") } } @@ -163,7 +163,7 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.2.1" - latestImplementation "com.google.android.material:material:1.12.0" + latestImplementation "com.google.android.material:material:1.13.0" if (rootProject.ext.enableWear) { // Build Wear separately, do not include in phone apk // latestWearApp project(':wear') @@ -172,7 +172,7 @@ dependencies { latestImplementation "com.google.android.gms:play-services-wearable:${rootProject.ext.googlePlayServicesWearableVersion}" } - implementation "com.squareup.okhttp3:okhttp:5.1.0" + implementation "com.squareup.okhttp3:okhttp:5.3.2" latestImplementation 'com.getpebble:pebblekit:4.0.1' if (rootProject.ext.allowNonFree) { // MapBox uses telemetry, without Play there may be exceptions from mapbox (OK to ignore) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index ce9768f92..5cb214c6d 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -1,5 +1,16 @@ - + + + + + + file="$GRADLE_USER_HOME/caches/8.13/transforms/42de1f8347fab8a0bff82f7b8295106e/transformed/jetified-mapbox-android-sdk-gl-core-5.2.2/jni/arm64-v8a/libmapbox-gl.so"/> + file="$GRADLE_USER_HOME/caches/8.13/transforms/42de1f8347fab8a0bff82f7b8295106e/transformed/jetified-mapbox-android-sdk-gl-core-5.2.2/jni/arm64-v8a/libmapbox-gl.so"/> - + + + + + consumer) { - mDataClient.getDataItems(new Uri.Builder() - .scheme(WEAR_URI_SCHEME) - .path(path) - .build()) - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(Task task) { - if (task.isSuccessful()) { - DataItemBuffer dataItems = task.getResult(); - if (dataItems.getCount() == 0) { - consumer.accept(null); - } else { - for (DataItem dataItem : dataItems) { - consumer.accept(dataItem); + mDataClient + .getDataItems(new Uri.Builder().scheme(WEAR_URI_SCHEME).path(path).build()) + .addOnCompleteListener( + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + DataItemBuffer dataItems = task.getResult(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (dataItems.getCount() == 0) { + consumer.accept(null); + } else { + for (DataItem dataItem : dataItems) { + consumer.accept(dataItem); + } + } } + dataItems.release(); + } else { + System.out.println("task.getException(): " + task.getException()); } - dataItems.release(); - } else { - System.out.println("task.getException(): " + task.getException()); } - } - }); + }); } public Task putData(String path) { diff --git a/hrdevice/build.gradle b/hrdevice/build.gradle index a51e58d3b..73a0c5749 100644 --- a/hrdevice/build.gradle +++ b/hrdevice/build.gradle @@ -4,13 +4,13 @@ group = "org.runnerup.hr" version = "1.0" android { - namespace 'org.runnerup.hr' - compileSdk rootProject.ext.compileSdk - buildToolsVersion rootProject.ext.buildToolsVersion + namespace = 'org.runnerup.hr' + compileSdk = rootProject.ext.compileSdk + buildToolsVersion = rootProject.ext.buildToolsVersion compileOptions { - sourceCompatibility JavaVersion.toVersion("17") - targetCompatibility JavaVersion.toVersion("17") + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") } sourceSets { @@ -25,16 +25,16 @@ android { } } lint { - baseline file('lint-baseline.xml') - checkReleaseBuilds true - //lintConfig file('lint.xml') - showAll true - //textOutput 'stdout' - textReport true + baseline = file('lint-baseline.xml') + checkReleaseBuilds = true + //lintConfig = file('lint.xml') + showAll = true + //textOutput = 'stdout' + textReport = true } defaultConfig { - minSdk rootProject.ext.minSdk - targetSdk rootProject.ext.targetSdk + minSdk = rootProject.ext.minSdk + targetSdk = rootProject.ext.targetSdk if (rootProject.ext.antPlusLibName) { buildConfigField 'Boolean', 'ANTPLUS_ENABLED', "true" @@ -44,8 +44,8 @@ android { } } buildFeatures { - aidl true - buildConfig true + aidl = true + buildConfig = true } } diff --git a/hrdevice/lint-baseline.xml b/hrdevice/lint-baseline.xml index 73f790af7..375963cc0 100644 --- a/hrdevice/lint-baseline.xml +++ b/hrdevice/lint-baseline.xml @@ -1,389 +1,15 @@ - + + id="UnusedAttribute" + message="Attribute `usesPermissionFlags` is only used in API level 31 and higher (current min is 21)" + errorLine1=" android:usesPermissionFlags="neverForLocation" android:minSdkVersion="31"/>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/wear/build.gradle b/wear/build.gradle index ce1dc77da..e6fcced94 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -1,21 +1,21 @@ apply plugin: 'com.android.application' android { - namespace 'org.runnerup' - compileSdk rootProject.ext.compileSdk + namespace = 'org.runnerup' + compileSdk = rootProject.ext.compileSdk buildToolsVersion = rootProject.ext.buildToolsVersion compileOptions { - sourceCompatibility JavaVersion.toVersion("17") - targetCompatibility JavaVersion.toVersion("17") + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") } defaultConfig { - minSdk 25 - targetSdk rootProject.ext.targetSdk - versionName rootProject.ext.versionName - versionCode rootProject.ext.latestBaseVersionCode + rootProject.ext.versionCode + 1 - applicationId rootProject.ext.applicationId + minSdk = 25 + targetSdk = rootProject.ext.targetSdk + versionName = rootProject.ext.versionName + versionCode = rootProject.ext.latestBaseVersionCode + rootProject.ext.versionCode + 1 + applicationId = rootProject.ext.applicationId } signingConfigs { @@ -26,27 +26,27 @@ android { buildTypes { release { - debuggable false - minifyEnabled true - shrinkResources true + debuggable = false + minifyEnabled = true + shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.txt' - signingConfig signingConfigs.release + signingConfig = signingConfigs.release } debug { - applicationIdSuffix ".debug" + applicationIdSuffix = ".debug" } } lint { - baseline file('lint-baseline.xml') - checkReleaseBuilds true - lintConfig file('lint.xml') - showAll true - //textOutput 'stdout' - textReport true + baseline = file('lint-baseline.xml') + checkReleaseBuilds = true + lintConfig = file('lint.xml') + showAll = true + //textOutput = 'stdout' + textReport = true } - namespace 'org.runnerup' + namespace = 'org.runnerup' buildFeatures { - buildConfig true + buildConfig = true } } @@ -58,10 +58,10 @@ repositories { // Duplicate class kotlin.collections.jdk8 (from MapBox?) dependencies { constraints { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0") { because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") } - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0") { because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") } } @@ -76,7 +76,7 @@ dependencies { implementation "com.google.android.gms:play-services-wearable:${rootProject.ext.googlePlayServicesWearableVersion}" implementation "androidx.wear:wear:1.3.0" - implementation "androidx.wear:wear-ongoing:1.0.0" + implementation "androidx.wear:wear-ongoing:1.1.0" // Includes LocusIdCompat and new Notification categories for Ongoing Activity. implementation "androidx.core:core:1.16.0" } diff --git a/wear/lint-baseline.xml b/wear/lint-baseline.xml index 0e17fd917..1d8692c05 100644 --- a/wear/lint-baseline.xml +++ b/wear/lint-baseline.xml @@ -1,4 +1,4 @@ - + diff --git a/wear/src/main/java/org/runnerup/service/ListenerService.java b/wear/src/main/java/org/runnerup/service/ListenerService.java index e83ebcb19..0a0bfb20f 100644 --- a/wear/src/main/java/org/runnerup/service/ListenerService.java +++ b/wear/src/main/java/org/runnerup/service/ListenerService.java @@ -264,9 +264,15 @@ private void updateNotification() { if (ongoingActivity == null) { return; } - ongoingActivity.update(this, new Status.Builder() - .addPart("Status", - new TextPart(getStatusString())).build()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + return; + } + + ongoingActivity.update( + this, new Status.Builder().addPart("Status", new TextPart(getStatusString())).build()); } private void dismissNotification() { From f1fa1eada83a60a200b4967a60c6928ab6415a0d Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 16 Dec 2025 21:35:30 +0100 Subject: [PATCH 04/31] feat: Android 16 / SDK 36.1 (#1298) --- build.gradle | 4 ++-- wear/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index ec15b3162..7ce75434f 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ project.ext { //Common settings for most builds //Note that Android Studio does not know about the 'ext' module and will warn buildToolsVersion = '36.1.0' //Update Travis manually - compileSdk = 35 //Update Travis manually - targetSdk = 35 + compileSdk = 36.1 //Update Travis manually + targetSdk = 36.1 minSdk = 21 appcompat_version = "1.7.1" diff --git a/wear/build.gradle b/wear/build.gradle index e6fcced94..5ecc61229 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -78,7 +78,7 @@ dependencies { implementation "androidx.wear:wear:1.3.0" implementation "androidx.wear:wear-ongoing:1.1.0" // Includes LocusIdCompat and new Notification categories for Ongoing Activity. - implementation "androidx.core:core:1.16.0" + implementation "androidx.core:core:1.17.0" } def props = new Properties() From 32a81d82a2eab4d87e446b3c1276e23fda9e0e9e Mon Sep 17 00:00:00 2001 From: Gerhard Olsson <6248932+gerhardol@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:40:19 +0100 Subject: [PATCH 05/31] feat: build with GitHub Action (#1303) --- .github/workflows/actions.yml | 118 ++++++++++++++++++ .travis.yml | 35 ------ app/build.gradle | 66 +++------- app/lint-baseline.xml | 31 +---- app/src/main/org/runnerup/db/DBHelper.java | 6 +- .../runnerup/export/DropboxSynchronizer.java | 4 +- .../runnerup/export/RunalyzeSynchronizer.java | 7 +- .../org/runnerup/view/DetailActivity.java | 12 +- .../org/runnerup/view/SettingsFragment.java | 2 +- build.gradle | 51 +++++++- common/build.gradle | 4 + common/lint-baseline.xml | 7 -- common/lint.xml | 3 + gradle.properties | 6 +- hrdevice/build.gradle | 4 + hrdevice/lint-baseline.xml | 2 +- wear/build.gradle | 4 + wear/lint-baseline.xml | 2 +- 18 files changed, 222 insertions(+), 142 deletions(-) create mode 100644 .github/workflows/actions.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml new file mode 100644 index 000000000..9faa23d7c --- /dev/null +++ b/.github/workflows/actions.yml @@ -0,0 +1,118 @@ +name: Android CI + +on: + push: + branches: + - master + - test/** + pull_request: + types: [opened, synchronize, reopened] + +env: + ANDROID_API: 36.1 + ANDROID_BUILD_TOOLS: 36.1.0 + ADB_INSTALL_TIMEOUT: 5 + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + + - name: Cache Gradle Build Cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches/build-cache-* + key: ${{ runner.os }}-gradle-build-cache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-gradle-build-cache- + + - name: Restore cache for Git LFS Objects + id: lfs-cache + uses: actions/cache@v4 + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.git/lfs/objects/**') }} + restore-keys: ${{ runner.os }}-lfs- + + - name: Git LFS Pull + run: git lfs pull + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK components + run: | + sdkmanager "tools" + sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" + sdkmanager "platforms;android-${ANDROID_API}" + sdkmanager "extras;android;m2repository" + sdkmanager "extras;google;m2repository" + sdkmanager "extras;google;google_play_services" + + - name: Prepare builddir + run: | + echo "${{ secrets.MAPBOX }}" > $GITHUB_WORKSPACE/mapbox.properties + echo "${{ secrets.DROPBOX }}" > $GITHUB_WORKSPACE/dropbox.properties + echo "${{ secrets.RUNALYZE }}" > $GITHUB_WORKSPACE/runalyze.properties + chmod +x gradlew + + - name: Build bundle + run: ./gradlew :app:bundleLatestRelease :wear:bundleRelease + + - name: Upload build logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-bundle + # possibly include: app/build/outputs/bundle/latestRelease/ wear/build/outputs/bundle/release/ + path: | + **/build/outputs/logs/ + + - name: Test + run: ./gradlew testBuildTypesUnitTest :app:testLatestDebugUnitTest testDebugUnitTest + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: | + **/build/reports/tests/ + + - name: Lint latest release + run: ./gradlew :app:lintLatestRelease :wear:lintRelease :hrdevice:lintRelease :common:lintRelease + + - name: Upload lint logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: lint-logs + path: | + **/build/reports/lint-results-release.html + **/build/reports/lint-results-latestRelease.html + + - name: Build F-Droid + run: | + rm $GITHUB_WORKSPACE/mapbox.properties + # For special build steps, see https://gitlab.com/fdroid/fdroiddata.git metadata/org.runnerup.free.yml + rm -rf wear ANT-Android-SDKs $GITHUB_WORKSPACE/dropbox.properties $GITHUB_WORKSPACE/runalyze.properties + sed -i -e '/play-services/d' -e '/mapboxsdk/d' -e '/api.mapbox.com/d' app/build.gradle + sed -i -e '/wearable/d' common/build.gradle + ./gradlew clean :app:assembleLatestRelease + + - name: Upload F-Droid logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: fdroid-logs + path: | + build/reports/problems/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9fa069636..000000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -env: - global: - - ANDROID_API=35 - - ANDROID_BUILD_TOOLS=36.0.0 - - ADB_INSTALL_TIMEOUT=5 -language: android -jdk: -- oraclejdk8 -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ -before_cache: -- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -- rm -f $HOME/.gradle/caches/*/classAnalysis/cache.properties.lock -- rm -f $HOME/.gradle/caches/*/jarSnapshots/cache.properties.lock -- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -android: - components: - - tools #latest for "builtin" sdk tools (24.4.1 in Android-25) - #To update SDK Tools to latest, another update is required - #- tools #latest, 26.1.1 as of 2018-10-07 - #- platform-tools #latest, 28.0.1 as of 2018-10-07 - - build-tools-$ANDROID_BUILD_TOOLS - - android-$ANDROID_API - - extra-android-m2repository - - extra-google-m2repository - - extra-google-google_play_services -notifications: - email: false -script: -- ./gradlew wear:lintRelease -- ./gradlew app:lintLatestRelease -- ./gradlew app:assembleLatestRelease -- ./gradlew app:test diff --git a/app/build.gradle b/app/build.gradle index 316bc6695..5144b72a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { } if (rootProject.ext.noMap) { java.srcDirs += ['src/nomap'] - } else if (rootProject.ext.useMapBox) { + } else if (rootProject.ext.mapboxEnabled) { java.srcDirs += ['src/mapbox'] } else { java.srcDirs += ['src/osmdroid'] @@ -102,6 +102,10 @@ android { showAll = true //textOutput = 'stdout' textReport = true + // Treat all warnings as errors + warningsAsErrors = true + // Halt the build if any errors are found + abortOnError = true } bundle { language { @@ -122,7 +126,7 @@ repositories { google() mavenCentral() //MapBox GraphView maven { url = "https://oss.sonatype.org/content/groups/public/" } //pebblekit - if (rootProject.ext.useMapBox) { + if (rootProject.ext.mapboxEnabled) { maven { url = 'https://api.mapbox.com/downloads/v2/releases/maven' authentication { @@ -178,7 +182,7 @@ dependencies { // MapBox uses telemetry, without Play there may be exceptions from mapbox (OK to ignore) latestImplementation "com.google.android.gms:play-services-location:${rootProject.ext.googlePlayServicesVersion}" } - if (rootProject.ext.useMapBox) { + if (rootProject.ext.mapboxEnabled) { //noinspection GradleDependency latestImplementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.6.2' latestImplementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9:0.9.0' @@ -202,6 +206,7 @@ allprojects { } def props = new Properties() +// For locally signed builds only, Google Play signs the bundle if (rootProject.file("release.properties").exists()) { props.load(new FileInputStream(rootProject.file("release.properties"))) @@ -215,54 +220,17 @@ if (rootProject.file("release.properties").exists()) { } android.applicationVariants.configureEach { - // Note: As a minimum extra security at least obfuscate the strings with Proguard - if (rootProject.ext.noMap) { - buildConfigField 'boolean', 'USING_OSMDROID', "false" - buildConfigField 'int', 'MAPBOX_ENABLED', "0" - buildConfigField 'String', 'MAPBOX_ACCESS_TOKEN', '""' - } else if (rootProject.ext.useMapBox) { - // https://www.mapbox.com/account/ - props.load(new FileInputStream(rootProject.file("mapbox.properties"))) - buildConfigField 'int', 'MAPBOX_ENABLED', "1" - buildConfigField 'String', 'MAPBOX_ACCESS_TOKEN', props.mapboxAccessToken + buildConfigField 'boolean', 'MAPBOX_ENABLED', "${rootProject.ext.mapboxEnabled}" + buildConfigField 'String', 'MAPBOX_ACCESS_TOKEN', "\"${rootProject.ext.mapboxAccessToken}\"" + buildConfigField 'boolean', 'OSMDROID_ENABLED', "${rootProject.ext.osmdroidEnabled}" - buildConfigField 'boolean', 'USING_OSMDROID', "false" - } else { - buildConfigField 'int', 'MAPBOX_ENABLED', "0" - buildConfigField 'String', 'MAPBOX_ACCESS_TOKEN', '""' - buildConfigField 'boolean', 'USING_OSMDROID', "true" - } + buildConfigField 'boolean', 'RUNALYZE_ENABLED', "${rootProject.ext.runalyzeEnabled}" + buildConfigField 'String', 'RUNALYZE_ID', "\"${rootProject.ext.runalyzeId}\"" + buildConfigField 'String', 'RUNALYZE_SECRET', "\"${rootProject.ext.runalyzeSecret}\"" - if (rootProject.file("runalyze.properties").exists()) { - // Contact Runalyze team at https://forum.runalyze.com/ - props.load(new FileInputStream(rootProject.file("runalyze.properties"))) - buildConfigField 'int', 'RUNALYZE_ENABLED', "1" - buildConfigField 'String', 'RUNALYZE_ID', props.CLIENT_ID - buildConfigField 'String', 'RUNALYZE_SECRET', props.CLIENT_SECRET - } else { - // Demo, connect to testing.runalyze.com - buildConfigField 'int', 'RUNALYZE_ENABLED', "0" - buildConfigField 'String', 'RUNALYZE_ID', '"8_2jx5jt9r39ic40ooc80c8c0884okgk0owsowg808c4csg8ko8g"' - buildConfigField 'String', 'RUNALYZE_SECRET', '"1v7d6nwe1v9c8skok44g0gc8cc04cc0wwwo8swwgckoogwsww4"' - } - - if (rootProject.file("dropbox.properties").exists()) { - // Create an app at https://www.dropbox.com/developers/apps/ - // Dropbox API, App folder -> create app - // Enable additional users, Redirect URI:http://localhost:8080/runnerup/dropbox, Disallow implicit grant - // Set branding icon - // Create dropbox.properties with two lines: - // CLIENT_ID="replace_dropbox_id" - // CLIENT_SECRET="replace_dropbox_secret" - props.load(new FileInputStream(rootProject.file("dropbox.properties"))) - buildConfigField 'int', 'DROPBOX_ENABLED', "1" - buildConfigField 'String', 'DROPBOX_ID', props.CLIENT_ID - buildConfigField 'String', 'DROPBOX_SECRET', props.CLIENT_SECRET - } else { - buildConfigField 'int', 'DROPBOX_ENABLED', "0" - buildConfigField 'String', 'DROPBOX_ID', "null" - buildConfigField 'String', 'DROPBOX_SECRET', "null" - } + buildConfigField 'boolean', 'DROPBOX_ENABLED', "${rootProject.ext.dropboxEnabled}" + buildConfigField 'String', 'DROPBOX_ID', "\"${rootProject.ext.dropboxId}\"" + buildConfigField 'String', 'DROPBOX_SECRET', "\"${rootProject.ext.dropboxSecret}\"" } //Based on an example from https://developer.android.com/studio/build/configure-apk-splits.html diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 5cb214c6d..9cb0b5202 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -1,16 +1,5 @@ - - - - - + @@ -63,24 +52,10 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - 0 ? "https://runalyze.com" : "https://testing.runalyze.com"; + BuildConfig.RUNALYZE_ENABLED ? "https://runalyze.com" : "https://testing.runalyze.com"; private static final String PUBLIC_URL = BASE_URL; private static final String UPLOAD_URL = BASE_URL + "/api/v1/activities/uploads"; @@ -67,7 +68,7 @@ public class RunalyzeSynchronizer extends DefaultSynchronizer implements OAuth2S private final PathSimplifier simplifier; RunalyzeSynchronizer(PathSimplifier simplifier) { - if (ENABLED == 0) { + if (!ENABLED) { Log.w(NAME, "No client id configured in this build"); } this.simplifier = simplifier; diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index fc260237a..27e69666a 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -17,7 +17,6 @@ package org.runnerup.view; -import static org.runnerup.BuildConfig.USING_OSMDROID; import static org.runnerup.content.ActivityProvider.GPX_MIME; import static org.runnerup.content.ActivityProvider.TCX_MIME; @@ -136,7 +135,7 @@ public class DetailActivity extends AppCompatActivity implements Constants { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (USING_OSMDROID || BuildConfig.MAPBOX_ENABLED > 0) { + if (BuildConfig.OSMDROID_ENABLED || BuildConfig.MAPBOX_ENABLED) { // MapBox or Osmdroid, set mapWrapper. MapWrapper.start(this); } @@ -210,7 +209,7 @@ public int preSetValue(int newValue) throws IllegalArgumentException { }); notes = findViewById(R.id.notes_text); - if (USING_OSMDROID || BuildConfig.MAPBOX_ENABLED > 0) { + if (BuildConfig.OSMDROID_ENABLED || BuildConfig.MAPBOX_ENABLED) { Object mapView = findViewById(R.id.mapview); mapWrapper = new MapWrapper(this, mDB, mID, formatter, mapView); mapWrapper.onCreate(savedInstanceState); @@ -235,7 +234,7 @@ public int preSetValue(int newValue) throws IllegalArgumentException { tabSpec.setContent(R.id.tab_lap); th.addTab(tabSpec); - if (USING_OSMDROID || BuildConfig.MAPBOX_ENABLED > 0) { + if (BuildConfig.OSMDROID_ENABLED || BuildConfig.MAPBOX_ENABLED) { tabSpec = th.newTabSpec("map"); tabSpec.setIndicator( WidgetUtil.createHoloTabIndicator(this, getString(org.runnerup.common.R.string.Map))); @@ -317,8 +316,9 @@ public WindowInsetsCompat onApplyWindowInsets( LinearLayout graphTabLayout = findViewById(R.id.tab_graph); LinearLayout hrzonesBarLayout = findViewById(R.id.hrzonesBarLayout); boolean use_distance_as_x = !Sport.isWithoutGps(sport.getValueInt()); - graphWrapper = new GraphWrapper(this, graphTabLayout, hrzonesBarLayout, - formatter, mDB, mID, use_distance_as_x); + // variable not needed + new GraphWrapper(this, graphTabLayout, hrzonesBarLayout, + formatter, mDB, mID, use_distance_as_x); if (this.mode == MODE_SAVE) { resumeButton.setOnClickListener(resumeButtonClick); diff --git a/app/src/main/org/runnerup/view/SettingsFragment.java b/app/src/main/org/runnerup/view/SettingsFragment.java index dfc5caff7..8c36d2d62 100644 --- a/app/src/main/org/runnerup/view/SettingsFragment.java +++ b/app/src/main/org/runnerup/view/SettingsFragment.java @@ -14,7 +14,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.settings, rootKey); - if (BuildConfig.MAPBOX_ENABLED == 0) { + if (!BuildConfig.MAPBOX_ENABLED) { Preference pref = findPreference("map_preferencescreen"); pref.setEnabled(false); } diff --git a/build.gradle b/build.gradle index 7ce75434f..a7b0fd8c1 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,8 @@ repositories { project.ext { //Common settings for most builds //Note that Android Studio does not know about the 'ext' module and will warn - buildToolsVersion = '36.1.0' //Update Travis manually - compileSdk = 36.1 //Update Travis manually + buildToolsVersion = '36.1.0' // also .github/workflows/actions.yml + compileSdk = 36.1 // also .github/workflows/actions.yml targetSdk = 36.1 minSdk = 21 @@ -39,11 +39,52 @@ project.ext { // F-Droid builds only allow free software (wear dir deleted at builds) allowNonFree = !project.hasProperty('org.runnerup.free') && rootProject.file("wear").exists() enableWear = !project.hasProperty('org.runnerup.wear.disable') && rootProject.file("wear").exists() - - // Use or not use mapbox. - useMapBox = allowNonFree && rootProject.file("mapbox.properties").exists() noMap = project.hasProperty('org.runnerup.nomap') + // get tokens, also required to set buildDirs + + mapboxAccessToken = "" + def props = new Properties() + if (!noMap && allowNonFree && rootProject.file("mapbox.properties").exists()) { + // https://www.mapbox.com/account/ + props.load(new FileInputStream(rootProject.file("mapbox.properties"))) + mapboxAccessToken = props.getProperty("mapboxAccessToken") ?: "" + } + mapboxEnabled = !mapboxAccessToken.isEmpty() + osmdroidEnabled = !noMap && !mapboxEnabled + + runalyzeId = "" + if (rootProject.file("runalyze.properties").exists()) { + // Contact Runalyze team at https://forum.runalyze.com/ + props.load(new FileInputStream(rootProject.file("runalyze.properties"))) + runalyzeEnabled = true + runalyzeId = props.getProperty("CLIENT_ID") ?: "" + runalyzeSecret = props.getProperty("CLIENT_SECRET") ?: "" + } + if (runalyzeId.isEmpty() || runalyzeSecret.isEmpty()) { + // Demo, with runalyzeEnabled set to false connect to testing.runalyze.com + runalyzeEnabled = false + runalyzeId = "8_2jx5jt9r39ic40ooc80c8c0884okgk0owsowg808c4csg8ko8g" + runalyzeSecret = "1v7d6nwe1v9c8skok44g0gc8cc04cc0wwwo8swwgckoogwsww4" + } + + dropboxEnabled = false + dropboxId = "" + dropboxSecret = "" + if (rootProject.file("dropbox.properties").exists()) { + // Create an app at https://www.dropbox.com/developers/apps/ + // Dropbox API, App folder -> create app + // Enable additional users, Redirect URI:http://localhost:8080/runnerup/dropbox, Disallow implicit grant + // Set branding icon + // Create dropbox.properties with two lines: + // CLIENT_ID="replace_dropbox_id" + // CLIENT_SECRET="replace_dropbox_secret" + props.load(new FileInputStream(rootProject.file("dropbox.properties"))) + dropboxEnabled = true + dropboxId = props.getProperty("CLIENT_ID") ?: "" + dropboxSecret = props.getProperty("CLIENT_SECRET") ?: "" + } + // Note: AntPlus may have to be downloaded explicitly due to licensing // Therefore, the .aar file may not be redistributed in the RU repo antPlusLibPath = "$rootDir/ANT-Android-SDKs/ANT+_Android_SDK/API" diff --git a/common/build.gradle b/common/build.gradle index 2f2462d41..7488ee5ff 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -43,6 +43,10 @@ android { lintConfig = file('lint.xml') //textOutput = 'stdout' textReport = true + // Treat all warnings as errors + warningsAsErrors = true + // Halt the build if any errors are found + abortOnError = true } namespace = 'org.runnerup.common' buildFeatures { diff --git a/common/lint-baseline.xml b/common/lint-baseline.xml index 51f09c8eb..b689d67e0 100644 --- a/common/lint-baseline.xml +++ b/common/lint-baseline.xml @@ -12,11 +12,4 @@ column="17"/> - - - - diff --git a/common/lint.xml b/common/lint.xml index c4b210183..d7f33b42b 100644 --- a/common/lint.xml +++ b/common/lint.xml @@ -5,4 +5,7 @@ + + + diff --git a/gradle.properties b/gradle.properties index 0a91919ef..cdeb25fcc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ # R8 is not displaying warnings for configuration issues, proguard may be required # Build with ./gradlew app:minifyLatestReleaseWithProguard -#android.enableR8=false android.enableJetifier=true android.useAndroidX=true org.gradle.caching = true @@ -10,8 +9,11 @@ org.gradle.vfs.watch = true org.gradle.unsafe.configuration-cache=true android.nonTransitiveRClass=true android.nonFinalResIds=true +org.gradle.jvmargs=-Xmx4096m +-XX:MaxMetaspaceSize=1024m +-Dfile.encoding=UTF-8 + # org.runnerup.free=true -# org.gradle.jvmargs=-Xmx4096M # org.runnerup.hr.disableAntPlus=true # org.runnerup.wear.disable=true diff --git a/hrdevice/build.gradle b/hrdevice/build.gradle index 73a0c5749..877d1c0d1 100644 --- a/hrdevice/build.gradle +++ b/hrdevice/build.gradle @@ -31,6 +31,10 @@ android { showAll = true //textOutput = 'stdout' textReport = true + // Treat all warnings as errors + warningsAsErrors = true + // Halt the build if any errors are found + abortOnError = true } defaultConfig { minSdk = rootProject.ext.minSdk diff --git a/hrdevice/lint-baseline.xml b/hrdevice/lint-baseline.xml index 375963cc0..f3c60b6dc 100644 --- a/hrdevice/lint-baseline.xml +++ b/hrdevice/lint-baseline.xml @@ -1,5 +1,5 @@ - + - + From 495e9db8aaf06469eec5204d954abb9a81791c8f Mon Sep 17 00:00:00 2001 From: Gerhard Olsson <6248932+gerhardol@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:25:13 +0100 Subject: [PATCH 06/31] fix: special lint config if no mapbox key (#1307) --- .github/workflows/actions.yml | 1 + app/build.gradle | 6 ++- app/lint-osmdroid.xml | 48 +++++++++++++++++++ app/lint.xml | 1 + .../tracker/component/TrackerGPS.java | 13 +++-- 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 app/lint-osmdroid.xml diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 9faa23d7c..8065d0c82 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -60,6 +60,7 @@ jobs: - name: Prepare builddir run: | + # secrets are only available for collabrators, other will build with OsmDroid (but a few more options than F-Droid still) echo "${{ secrets.MAPBOX }}" > $GITHUB_WORKSPACE/mapbox.properties echo "${{ secrets.DROPBOX }}" > $GITHUB_WORKSPACE/dropbox.properties echo "${{ secrets.RUNALYZE }}" > $GITHUB_WORKSPACE/runalyze.properties diff --git a/app/build.gradle b/app/build.gradle index 5144b72a6..401c321e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,11 @@ android { lint { baseline = file('lint-baseline.xml') checkReleaseBuilds = true - lintConfig = file('lint.xml') + if (mapboxEnabled) { + lintConfig = file('lint.xml') + } else { + lintConfig = file('lint-osmdroid.xml') + } showAll = true //textOutput = 'stdout' textReport = true diff --git a/app/lint-osmdroid.xml b/app/lint-osmdroid.xml new file mode 100644 index 000000000..2c50ab3b6 --- /dev/null +++ b/app/lint-osmdroid.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/lint.xml b/app/lint.xml index 4f935c9b0..743715ebc 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -69,4 +69,5 @@ + diff --git a/app/src/main/org/runnerup/tracker/component/TrackerGPS.java b/app/src/main/org/runnerup/tracker/component/TrackerGPS.java index 870b7f555..ca04ded20 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerGPS.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerGPS.java @@ -28,6 +28,8 @@ import android.location.LocationManager; import android.os.Handler; import android.text.TextUtils; + +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import org.runnerup.R; @@ -116,11 +118,16 @@ private Integer parseAndFixInteger( return Integer.parseInt(s); } - static Location getLastKnownLocation(LocationManager lm) { + static Location getLastKnownLocation(LocationManager lm, Context context) { String[] list = {GPS_PROVIDER, NETWORK_PROVIDER, PASSIVE_PROVIDER}; Location lastLocation = null; for (String s : list) { - Location tmp = lm.getLastKnownLocation(s); + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // User has deactivated location permission during the workout. + continue; + } + Location tmp = lm.getLastKnownLocation(s); if (tmp == null) { continue; } @@ -154,7 +161,7 @@ public ResultCode onConnecting(final Callback callback, Context context) { var lm = locationManager; SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); frequency_ms = parseAndFixInteger(preferences, R.string.pref_pollInterval, "1000", context); - mLastLocation = getLastKnownLocation(lm); + mLastLocation = getLastKnownLocation(lm, context); if (!mWithoutGps) { Integer frequency_meters = parseAndFixInteger(preferences, R.string.pref_pollDistance, "0", context); From 522b95a7ca204dfe6ea27158693097738070ec26 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 30 Dec 2025 10:46:46 +0100 Subject: [PATCH 07/31] fix: check mGpsStatus in tick not set for non GPS activities when switching activities A few cleanups --- .../tracker/component/TrackerGPS.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/org/runnerup/tracker/component/TrackerGPS.java b/app/src/main/org/runnerup/tracker/component/TrackerGPS.java index ca04ded20..b4263c196 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerGPS.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerGPS.java @@ -110,22 +110,22 @@ private Integer parseAndFixInteger( String s = preferences.getString(context.getString(resId), def); if (TextUtils.isEmpty(s)) { // Update the settings - SharedPreferences.Editor prefedit = preferences.edit(); - prefedit.putString(context.getString(resId), def); - prefedit.apply(); + SharedPreferences.Editor prefs = preferences.edit(); + prefs.putString(context.getString(resId), def); + prefs.apply(); s = def; } return Integer.parseInt(s); } - static Location getLastKnownLocation(LocationManager lm, Context context) { + private static Location getLastKnownLocation(LocationManager lm, Context context) { String[] list = {GPS_PROVIDER, NETWORK_PROVIDER, PASSIVE_PROVIDER}; Location lastLocation = null; for (String s : list) { - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // User has deactivated location permission during the workout. - continue; + if (s.equals(GPS_PROVIDER) + && (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { + continue; } Location tmp = lm.getLastKnownLocation(s); if (tmp == null) { @@ -151,7 +151,7 @@ static Location getLastKnownLocation(LocationManager lm, Context context) { @Override public ResultCode onConnecting(final Callback callback, Context context) { - if (mWithoutGps == false && + if (!mWithoutGps && ContextCompat.checkSelfPermission(this.tracker, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { mWithoutGps = true; @@ -253,7 +253,7 @@ public void run() { @Override public void onTick() { - if (mWithoutGps == false) { + if (!mWithoutGps) { if (mGpsStatus == null) { return; } @@ -269,8 +269,10 @@ public void onTick() { Callback tmp = mConnectCallback; mConnectCallback = null; - mGpsStatus.stop(this); - // note: Don't reset mGpsStatus, it's used for isConnected() + if (mGpsStatus != null) { + mGpsStatus.stop(this); + // note: Don't reset mGpsStatus, it's used for isConnected() + } tmp.run(this, ResultCode.RESULT_OK); } From 61e250a1f7e7639df29315bad0c449a3e3d9b417 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 30 Dec 2025 11:09:25 +0100 Subject: [PATCH 08/31] fix: workout started after stop only started after a pause. * each step was started multiple times * validateSeconds did not handle not parsable data --- .../org/runnerup/workout/TargetTrigger.java | 1 + .../main/org/runnerup/workout/Workout.java | 9 +++++++- .../org/runnerup/workout/WorkoutBuilder.java | 23 ++++++++----------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/main/org/runnerup/workout/TargetTrigger.java b/app/src/main/org/runnerup/workout/TargetTrigger.java index fae5b0518..9738fca5f 100644 --- a/app/src/main/org/runnerup/workout/TargetTrigger.java +++ b/app/src/main/org/runnerup/workout/TargetTrigger.java @@ -248,6 +248,7 @@ public void onRepeat(int current, int limit) {} @Override public void onStart(Scope what, Workout s) { if (this.scope == what) { + paused = false; reset(); for (Feedback f : triggerAction) { f.onStart(s); diff --git a/app/src/main/org/runnerup/workout/Workout.java b/app/src/main/org/runnerup/workout/Workout.java index ecc5fda3c..c0b949048 100644 --- a/app/src/main/org/runnerup/workout/Workout.java +++ b/app/src/main/org/runnerup/workout/Workout.java @@ -139,6 +139,12 @@ public void onEnd(Workout w) { @Override public void onRepeat(int current, int limit) {} + /** + * Start the ACTIVITY, the tracker starts this once. The activity starts the steps for STEP/LAP. + * + * @param s always ACTIVITY + * @param w current workout + */ public void onStart(Scope s, Workout w) { if (BuildConfig.DEBUG && w != this) { throw new AssertionError(); @@ -151,10 +157,11 @@ public void onStart(Scope s, Workout w) { } currentStepNo = 0; - if (steps.size() > 0) { + if (!steps.isEmpty()) { setCurrentStep(steps.get(currentStepNo)); } + // start STEP and LAP if (currentStep != null) { currentStep.onStart(Scope.ACTIVITY, this); currentStep.onStart(Scope.STEP, this); diff --git a/app/src/main/org/runnerup/workout/WorkoutBuilder.java b/app/src/main/org/runnerup/workout/WorkoutBuilder.java index 502bdb941..24f24429d 100644 --- a/app/src/main/org/runnerup/workout/WorkoutBuilder.java +++ b/app/src/main/org/runnerup/workout/WorkoutBuilder.java @@ -185,17 +185,14 @@ public static Workout createDefaultIntervalWorkout(Resources res, SharedPreferen } repeat.steps.add(step); - Step rest = null; - switch (intervalRestType) { - case 0: // Time - rest = Step.createRestStep(Dimension.TIME, intervalRestTime, convertRestToRecovery); - break; - case 1: // Distance - rest = - Step.createRestStep(Dimension.DISTANCE, intervalRestDistance, convertRestToRecovery); - break; - } - repeat.steps.add(rest); + Step rest = switch (intervalRestType) { + case 0 -> // Time + Step.createRestStep(Dimension.TIME, intervalRestTime, convertRestToRecovery); + case 1 -> // Distance + Step.createRestStep(Dimension.DISTANCE, intervalRestDistance, convertRestToRecovery); + default -> null; + }; + repeat.steps.add(rest); } w.steps.add(repeat); @@ -214,7 +211,7 @@ public static boolean validateSeconds(String newValue) { // TODO move this somewhere long seconds = SafeParse.parseSeconds(newValue, -1); long seconds2 = SafeParse.parseSeconds(DateUtils.formatElapsedTime(seconds), -1); - return seconds == seconds2; + return seconds >= 0 && seconds == seconds2; } public static SharedPreferences getAudioCuePreferences( @@ -552,7 +549,7 @@ private static void createAudioCountdown(Step step) { } // Remove all values in list close to the step - while (list.size() > 0 && step.getDurationValue() < list.get(0) * 1.1d) { + while (!list.isEmpty() && step.getDurationValue() < list.get(0) * 1.1d) { list.remove(0); } list.add(0, step.getDurationValue()); From 0bc3a1744df0c51d474d07329a6025a956d0ff11 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Sat, 3 Jan 2026 00:13:09 +0100 Subject: [PATCH 09/31] fix: various error handling Incorrect error handling reported in Play Console --- .../runnerup/view/ManageWorkoutsActivity.java | 5 ++-- .../org/runnerup/service/StateService.java | 4 ++- .../org/runnerup/view/RunInfoFragment.java | 29 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java b/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java index 8455d34f2..d511e4638 100644 --- a/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java +++ b/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java @@ -162,12 +162,11 @@ private String getFilename(Uri data) { } else if (ContentResolver.SCHEME_CONTENT.contentEquals(data.getScheme())) { String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; Cursor c = getContentResolver().query(data, projection, null, null, null); - if (c != null) { - c.moveToFirst(); + if (c != null && c.moveToFirst()) { final int fileNameColumnId = c.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); if (fileNameColumnId >= 0) name = c.getString(fileNameColumnId); - c.close(); } + c.close(); } return name; } diff --git a/wear/src/main/java/org/runnerup/service/StateService.java b/wear/src/main/java/org/runnerup/service/StateService.java index ee54d52e0..0d414707f 100644 --- a/wear/src/main/java/org/runnerup/service/StateService.java +++ b/wear/src/main/java/org/runnerup/service/StateService.java @@ -41,6 +41,8 @@ import org.runnerup.view.MainActivity; import org.runnerup.wear.WearableClient; +import java.util.Objects; + public class StateService extends Service implements MessageApi.MessageListener, DataApi.DataListener, ValueModel.ChangeListener { @@ -213,7 +215,7 @@ public void onDataChanged(DataEventBuffer dataEvents) { } private void setPhoneNode(DataEvent ev) { - if (ev.getType() == DataEvent.TYPE_CHANGED) { + if (ev.getType() == DataEvent.TYPE_CHANGED && Objects.requireNonNull(ev.getDataItem().getData()).length > 0) { phoneNode = new String(ev.getDataItem().getData()); } else if (ev.getType() == DataEvent.TYPE_DELETED) { phoneNode = null; diff --git a/wear/src/main/java/org/runnerup/view/RunInfoFragment.java b/wear/src/main/java/org/runnerup/view/RunInfoFragment.java index d630e67a7..c309df51b 100644 --- a/wear/src/main/java/org/runnerup/view/RunInfoFragment.java +++ b/wear/src/main/java/org/runnerup/view/RunInfoFragment.java @@ -88,21 +88,20 @@ public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { int[] ids = null; - int card = 0; - switch (rowsOnScreen) { - case 3: - ids = card3ids; - card = R.layout.card3; - break; - case 2: - ids = card2ids; - card = R.layout.card2; - break; - case 1: - ids = card1ids; - card = R.layout.card1; - break; - } + int card = switch (rowsOnScreen) { + case 2 -> { + ids = card2ids; + yield R.layout.card2; + } + case 1 -> { + ids = card1ids; + yield R.layout.card1; + } + default -> { + ids = card3ids; + yield R.layout.card3; + } + }; View view = inflater.inflate(card, container, false); for (int i = 0; i < rowsOnScreen; i++) { textViews.add( From 42ee716f783b76277bdb80648baa46c2b7ee08f7 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 2 Jan 2026 18:55:50 +0100 Subject: [PATCH 10/31] fix: EdgeToEdge enable EdgeToEdge is implemented previously, this enables a Play Console suggestion. --- app/src/main/org/runnerup/view/MainLayout.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/org/runnerup/view/MainLayout.java b/app/src/main/org/runnerup/view/MainLayout.java index 708dccdc6..4d64e9b9d 100644 --- a/app/src/main/org/runnerup/view/MainLayout.java +++ b/app/src/main/org/runnerup/view/MainLayout.java @@ -39,6 +39,8 @@ import android.view.View.OnClickListener; import android.webkit.WebView; import android.widget.Toast; + +import androidx.activity.EdgeToEdge; import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -76,6 +78,7 @@ private enum UpgradeState { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setContentView(R.layout.main); From 1eea61e0ff2644e2d17246f6da10a73871db4337 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Thu, 1 Jan 2026 12:36:05 +0100 Subject: [PATCH 11/31] fix: use Executor for background tasks Mostly using Gemini, some manual fixes and updates to comments etc. --- app/src/main/org/runnerup/db/DBHelper.java | 62 +-- .../export/RunKeeperSynchronizer.java | 109 ++--- .../export/oauth2client/OAuth2Activity.java | 152 +++---- .../mapbox/org/runnerup/util/MapWrapper.java | 393 +++++++++--------- .../org/runnerup/util/MapWrapper.java | 178 ++++---- 5 files changed, 448 insertions(+), 446 deletions(-) diff --git a/app/src/main/org/runnerup/db/DBHelper.java b/app/src/main/org/runnerup/db/DBHelper.java index 819e7c99b..d76ff6829 100644 --- a/app/src/main/org/runnerup/db/DBHelper.java +++ b/app/src/main/org/runnerup/db/DBHelper.java @@ -24,13 +24,16 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import androidx.appcompat.app.AlertDialog; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.json.JSONException; import org.json.JSONObject; import org.runnerup.common.util.Constants; @@ -626,35 +629,40 @@ public static void purgeDeletedActivities( } c.close(); - if (list.size() > 0) { - new AsyncTask() { - - @Override - protected void onPreExecute() { - dialog.setMax(list.size()); - super.onPreExecute(); - } - - @Override - protected Void doInBackground(Long... args) { - for (Long id : list) { - deleteActivity(db, id); - dialog.incrementProgressBy(1); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - db.close(); - mDBHelper.close(); - if (onComplete != null) onComplete.run(); - } - }.execute((long) 2); + if (!list.isEmpty()) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Handler handler = new Handler(Looper.getMainLooper()); + + // Pre-execution step (runs on the current/main thread) + dialog.setMax(list.size()); + + executor.execute( + () -> { + int currentProgress = 0; + for (Long id : list) { + deleteActivity(db, id); + currentProgress++; + final int progressToShow = currentProgress; + handler.post(() -> dialog.setProgress(progressToShow)); + } + + // Post-execution step (runs on the main thread) + handler.post( + () -> { + db.close(); + mDBHelper.close(); + if (onComplete != null) { + onComplete.run(); + } + }); + }); } else { + // No activities to delete, just clean up db.close(); mDBHelper.close(); - if (onComplete != null) onComplete.run(); + if (onComplete != null) { + onComplete.run(); + } } } diff --git a/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java b/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java index bff3afc69..7e2637404 100644 --- a/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java +++ b/app/src/main/org/runnerup/export/RunKeeperSynchronizer.java @@ -17,14 +17,12 @@ package org.runnerup.export; -import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; import androidx.annotation.ColorRes; @@ -47,6 +45,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.json.JSONArray; import org.json.JSONException; @@ -91,6 +91,7 @@ public class RunKeeperSynchronizer extends DefaultSynchronizer private String fitnessActivitiesUrl = null; private String userName = null; private final PathSimplifier simplifier; + private final Executor executor = Executors.newSingleThreadExecutor(); public static final Map runkeeper2sportMap = new HashMap<>(); public static final Map sport2runkeeperMap = new HashMap<>(); @@ -207,6 +208,7 @@ public void init(ContentValues config) { try { JSONObject tmp = new JSONObject(authConfig); access_token = tmp.optString("access_token", null); + userName = tmp.optString("username", null); } catch (Exception e) { Log.e(Constants.LOG, e.getMessage()); } @@ -224,6 +226,9 @@ public String getAuthConfig() { JSONObject tmp = new JSONObject(); try { tmp.put("access_token", access_token); + if (userName != null) { + tmp.put("username", userName); + } } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); } @@ -257,6 +262,7 @@ public Status getAuthResult(int resultCode, Intent data) { @Override public void reset() { access_token = null; + userName = null; } @NonNull @@ -268,6 +274,7 @@ public Status connect() { return s; } + // Already connected and initialized. if (fitnessActivitiesUrl != null) { return Synchronizer.Status.OK; } @@ -313,6 +320,8 @@ public Status connect() { if (uri != null) { fitnessActivitiesUrl = uri; + // After a successful connection, fetch the user profile info in the background. + fetchUserProfileAsync(); return Synchronizer.Status.OK; } s = Synchronizer.Status.ERROR; @@ -320,6 +329,40 @@ public Status connect() { return s; } + /** + * Fetches the user's profile information, including the username, on a background thread. This is + * a non-blocking "fire-and-forget" call. + */ + private void fetchUserProfileAsync() { + // If we already have the username, no need to fetch it again. + if (userName != null) { + return; + } + + executor.execute( + () -> { + try { + URL newurl = new URL(REST_URL + "/profile"); + HttpURLConnection conn = (HttpURLConnection) newurl.openConnection(); + conn.setRequestProperty("Authorization", "Bearer " + access_token); + conn.addRequestProperty("Content-Type", "application/vnd.com.runkeeper.Profile+json"); + + InputStream in = new BufferedInputStream(conn.getInputStream()); + JSONObject obj = SyncHelper.parse(in); + conn.disconnect(); + + String uri = obj.getString("profile"); + // The username is the last part of the profile URI. + String fetchedUserName = uri.substring(uri.lastIndexOf("/") + 1); + if (!TextUtils.isEmpty(fetchedUserName)) { + this.userName = fetchedUserName; + } + } catch (Exception e) { + Log.w(getName(), "Failed to fetch user profile in background: " + e.getMessage()); + } + }); + } + @NonNull public Status listActivities(List list) { Status s = connect(); @@ -455,63 +498,25 @@ public Status upload(SQLiteDatabase db, final long mID) { return s; } - @SuppressLint("StaticFieldLeak") @Override public String getActivityUrl(String extId) { - // username is part of the "web" URL but is not directly accessible in the API - // the numeric userID is in the "User" info (see connect()), but that is not accepted in URLs - // The userName could be retrieved from getAuthResult() too and saved in auth_config (but - // retries will not be handled) - if (userName == null) { - // try to get the information (cannot run in UI thread, use timeout) - try { - userName = - new AsyncTask() { - - @Override - protected String doInBackground(Void... args) { - try { - URL newurl = new URL(REST_URL + "/profile"); - HttpURLConnection conn = (HttpURLConnection) newurl.openConnection(); - conn.setRequestProperty("Authorization", "Bearer " + access_token); - conn.addRequestProperty( - "Content-Type", "application/vnd.com.runkeeper.Profile+json"); - - InputStream in = new BufferedInputStream(conn.getInputStream()); - JSONObject obj = SyncHelper.parse(in); - conn.disconnect(); - - String uri = obj.getString("profile"); - return uri.substring(uri.lastIndexOf("/") + 1); - } catch (Exception e) { - } - return null; - } - }.execute().get(5, TimeUnit.SECONDS); - } catch (Exception e) { - } - } - String url; if (userName == null || extId == null) { - url = null; - } else { - // Do not bother with fitnessActivitiesUrl - url = PUBLIC_URL + "/user/" + userName + extId.replace("/fitnessActivities/", "/activity/"); + // If userName is not yet available, we can't construct the URL. + // It might be fetched in the background, so this could work on a subsequent attempt. + Log.w(getName(), "Cannot get activity URL because userName is not available."); + return null; } - return url; + + // Do not bother with fitnessActivitiesUrl + return PUBLIC_URL + "/user/" + userName + extId.replace("/fitnessActivities/", "/activity/"); } @Override public boolean checkSupport(Synchronizer.Feature f) { - switch (f) { - case UPLOAD: - case ACTIVITY_LIST: - case GET_ACTIVITY: - return true; - default: - break; - } - return false; + return switch (f) { + case UPLOAD, ACTIVITY_LIST, GET_ACTIVITY -> true; + default -> false; + }; } @Override @@ -541,7 +546,7 @@ public ActivityEntity download(SyncActivityItem item) { Log.e(Constants.LOG, e.getMessage()); return activity; } - return activity; + return activity; } private double getLapLength() { diff --git a/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java b/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java index 14ef4a0a7..43ea8ea10 100644 --- a/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java +++ b/app/src/main/org/runnerup/export/oauth2client/OAuth2Activity.java @@ -22,8 +22,9 @@ import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.view.Window; @@ -38,6 +39,8 @@ import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.runnerup.common.util.Constants.DB; import org.runnerup.export.Synchronizer; import org.runnerup.export.util.FormValues; @@ -61,15 +64,13 @@ public interface OAuth2ServerCredentials { String AUTH_EXTRA = "auth_extra"; String TOKEN_URL = "token_url"; String REDIRECT_URI = "redirect_uri"; - String REVOKE_URL = "revoke_url"; - - String AUTH_TOKEN = "auth_token"; } private boolean mFinished = false; private String mRedirectUri = null; private ProgressDialog mSpinner = null; private Bundle mArgs = null; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); private void setSavedPassword(WebView wv, boolean val) { wv.getSettings().setSavePassword(false); @@ -147,8 +148,6 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { if (!isFinishing()) mSpinner.show(); } - // TODO: Fix "WrongThread" - @SuppressLint({"StaticFieldLeak", "WrongThread"}) @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); @@ -157,7 +156,7 @@ public void onPageFinished(WebView view, String url) { // this stupid crash! if (mSpinner != null && mSpinner.isShowing()) mSpinner.dismiss(); } catch (Exception ex) { - + // Ignore exception on dismiss } if (url.startsWith(mRedirectUri)) { @@ -186,72 +185,7 @@ public void onPageFinished(WebView view, String url) { mFinished = true; } - Bundle b = mArgs; - String code = u.getQueryParameter("code"); - final String token_url = b.getString(OAuth2ServerCredentials.TOKEN_URL); - final FormValues fv = new FormValues(); - fv.put("client_id", b.getString(OAuth2ServerCredentials.CLIENT_ID)); - fv.put("client_secret", b.getString(OAuth2ServerCredentials.CLIENT_SECRET)); - fv.put("grant_type", "authorization_code"); - fv.put("redirect_uri", b.getString(OAuth2ServerCredentials.REDIRECT_URI)); - fv.put("code", code); - - final Intent res = new Intent().putExtra("url", token_url); - - new AsyncTask() { - @Override - protected Integer doInBackground(String... params) { - int resultCode = AppCompatActivity.RESULT_CANCELED; - HttpURLConnection conn = null; - - try { - URL newUrl = new URL(token_url); - conn = (HttpURLConnection) newUrl.openConnection(); - conn.setDoOutput(true); - conn.setRequestMethod(Synchronizer.RequestMethod.POST.name()); - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - SyncHelper.postData(conn, fv); - StringBuilder obj = new StringBuilder(); - int responseCode = conn.getResponseCode(); - String amsg = conn.getResponseMessage(); - - try { - BufferedReader in = - new BufferedReader(new InputStreamReader(conn.getInputStream())); - char[] buf = new char[1024]; - int len; - while ((len = in.read(buf)) != -1) { - obj.append(buf, 0, len); - } - - res.putExtra(DB.ACCOUNT.AUTH_CONFIG, obj.toString()); - if (responseCode >= HttpURLConnection.HTTP_OK - && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) { - resultCode = AppCompatActivity.RESULT_OK; - } - } catch (IOException e) { - InputStream inS = conn.getErrorStream(); - String msg = inS == null ? "" : SyncHelper.readInputStream(inS); - Log.w("oath2", "Error stream: " + responseCode + " " + amsg + "; " + msg); - } - } catch (Exception ex) { - ex.printStackTrace(System.err); - res.putExtra("ex", ex.toString()); - } finally { - if (conn != null) { - conn.disconnect(); - } - } - - return resultCode; - } - - @Override - protected void onPostExecute(Integer resultCode) { - setResult(resultCode, res); - finish(); - } - }.execute(); + exchangeCodeForToken(u); } } @@ -270,8 +204,80 @@ public void onReceivedError( ViewUtil.Insets(wv, true); } + private void exchangeCodeForToken(Uri uri) { + Bundle b = mArgs; + String code = uri.getQueryParameter("code"); + final String token_url = b.getString(OAuth2ServerCredentials.TOKEN_URL); + final FormValues fv = new FormValues(); + fv.put("client_id", b.getString(OAuth2ServerCredentials.CLIENT_ID)); + fv.put("client_secret", b.getString(OAuth2ServerCredentials.CLIENT_SECRET)); + fv.put("grant_type", "authorization_code"); + fv.put("redirect_uri", b.getString(OAuth2ServerCredentials.REDIRECT_URI)); + fv.put("code", code); + + final Intent res = new Intent().putExtra("url", token_url); + final Handler handler = new Handler(Looper.getMainLooper()); + + executor.execute( + () -> { + int resultCode = AppCompatActivity.RESULT_CANCELED; + HttpURLConnection conn = null; + + try { + URL newUrl = new URL(token_url); + conn = (HttpURLConnection) newUrl.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod(Synchronizer.RequestMethod.POST.name()); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + SyncHelper.postData(conn, fv); + StringBuilder obj = new StringBuilder(); + int responseCode = conn.getResponseCode(); + String amsg = conn.getResponseMessage(); + + try { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + char[] buf = new char[1024]; + int len; + while ((len = in.read(buf)) != -1) { + obj.append(buf, 0, len); + } + + res.putExtra(DB.ACCOUNT.AUTH_CONFIG, obj.toString()); + if (responseCode >= HttpURLConnection.HTTP_OK + && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) { + resultCode = AppCompatActivity.RESULT_OK; + } + } catch (IOException e) { + InputStream inS = conn.getErrorStream(); + String msg = inS == null ? "" : SyncHelper.readInputStream(inS); + Log.w("oath2", "Error stream: " + responseCode + " " + amsg + "; " + msg); + } + } catch (Exception ex) { + ex.printStackTrace(System.err); + res.putExtra("ex", ex.toString()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + + final int finalResultCode = resultCode; + handler.post( + () -> { + setResult(finalResultCode, res); + finish(); + }); + }); + } + @Override public void onDestroy() { + if (executor != null) { + executor.shutdown(); + } + if (mSpinner != null && mSpinner.isShowing()) { + mSpinner.dismiss(); + } super.onDestroy(); } diff --git a/app/src/mapbox/org/runnerup/util/MapWrapper.java b/app/src/mapbox/org/runnerup/util/MapWrapper.java index 1f4040c26..0d9e07f8e 100644 --- a/app/src/mapbox/org/runnerup/util/MapWrapper.java +++ b/app/src/mapbox/org/runnerup/util/MapWrapper.java @@ -19,13 +19,11 @@ import static org.runnerup.util.Formatter.Format.TXT_SHORT; -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; import android.graphics.Color; -import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.ViewTreeObserver; @@ -49,8 +47,9 @@ import com.mapbox.pluginscalebar.ScaleBarOptions; import com.mapbox.pluginscalebar.ScaleBarPlugin; import java.util.ArrayList; -import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.runnerup.BuildConfig; import org.runnerup.R; import org.runnerup.common.util.Constants; @@ -67,8 +66,14 @@ public class MapWrapper implements Constants { private final Context context; private final Formatter formatter; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + public MapWrapper( - Context context, SQLiteDatabase mDB, long mID, Formatter formatter, Object mapView) { + Context context, + SQLiteDatabase mDB, + long mID, + Formatter formatter, + java.lang.Object mapView) { this.context = context; this.mDB = mDB; this.mID = mID; @@ -97,8 +102,8 @@ public void onCreate(Bundle savedInstanceState) { mapStyle -> { lineManager = new LineManager(mapView, mapboxMap, mapStyle); symbolManager = new SymbolManager(mapView, mapboxMap, mapStyle); - // Map is set up and the style has loaded - new LoadRoute().execute(new LoadParam(context, mDB, mID, mapboxMap)); + // Map is set up and the style has loaded, now load the route data + loadRouteAsync(mapboxMap); }); }); } @@ -128,6 +133,8 @@ public void onLowMemory() { } public void onDestroy() { + executor.shutdown(); + if (lineManager != null) { lineManager.onDestroy(); } @@ -137,213 +144,207 @@ public void onDestroy() { mapView.onDestroy(); } - class Route { - Route(Context context, MapboxMap map) { - this.context = context; - this.map = map; - } - - final List path = new ArrayList<>(10); - final ArrayList markers = new ArrayList<>(10); - final Context context; - final MapboxMap map; + /** Class to hold the processed route data from the background task. */ + static class Route { + final java.util.List path = new ArrayList<>(10); + final java.util.ArrayList markers = new ArrayList<>(10); } - private class LoadParam { - LoadParam(Context context, SQLiteDatabase mDB, long mID, MapboxMap map) { - this.context = context; - this.mDB = mDB; - this.mID = mID; - this.map = map; - } - - final Context context; - final SQLiteDatabase mDB; - final long mID; - final MapboxMap map; + /** + * Kicks off the process of loading route data on a background thread and then updating the UI. + * + * @param mapboxMap The map instance to update once data is loaded. + */ + private void loadRouteAsync(final MapboxMap mapboxMap) { + executor.execute( + () -> { + // DB queries, data processing on the background thread + final Route route = loadRouteDataInBackground(); + + // update the UI on the main thread + mapView.post(() -> onRouteLoaded(route, mapboxMap)); + }); } - @SuppressLint("StaticFieldLeak") - private class LoadRoute extends AsyncTask { - @Override - protected Route doInBackground(LoadParam... params) { - - Route route = new Route(params[0].context, params[0].map); - LocationEntity.LocationList ll = - new LocationEntity.LocationList<>(params[0].mDB, params[0].mID); - int lastLap = 0; - for (LocationEntity loc : ll) { - LatLng point = new LatLng(loc.getLatitude(), loc.getLongitude()); - route.path.add(point); - - Integer type; - // Start/end markers are not set in db, special handling - if (route.markers.isEmpty()) { - type = DB.LOCATION.TYPE_START; - } else { - type = loc.getType(); - } + /** + * This method runs on a background thread. It queries the database and processes the location + * data into a format suitable for the map, without touching any UI components. + * + * @return A {@link Route} object containing the processed path and markers. + */ + private Route loadRouteDataInBackground() { + Route route = new Route(); + LocationEntity.LocationList ll = new LocationEntity.LocationList<>(mDB, mID); + int lastLap = 0; + for (LocationEntity loc : ll) { + LatLng point = new LatLng(loc.getLatitude(), loc.getLongitude()); + route.path.add(point); + + java.lang.Integer type; + // Start/end markers are not set in db, special handling + if (route.markers.isEmpty()) { + type = DB.LOCATION.TYPE_START; + } else { + type = loc.getType(); + } + + java.lang.String iconImage; + if (type != DB.LOCATION.TYPE_START && lastLap != loc.getLap()) { + lastLap = loc.getLap(); + iconImage = "lap"; + } else if (type == DB.LOCATION.TYPE_START + || type == DB.LOCATION.TYPE_END + || type == DB.LOCATION.TYPE_PAUSE + || type == DB.LOCATION.TYPE_RESUME) { + iconImage = type.toString(); + } else { + iconImage = null; + } - String iconImage; - if (type != DB.LOCATION.TYPE_START && lastLap != loc.getLap()) { - lastLap = loc.getLap(); - iconImage = "lap"; - } else if (type == DB.LOCATION.TYPE_START - || type == DB.LOCATION.TYPE_END - || type == DB.LOCATION.TYPE_PAUSE - || type == DB.LOCATION.TYPE_RESUME) { - iconImage = type.toString(); + if (iconImage != null) { + // TBD Implement Info popup with the info instead, using the annotaion plugin (currently + // no examples) + java.lang.String info; + if (type == DB.LOCATION.TYPE_START) { + info = null; } else { - iconImage = null; + info = + (iconImage.equals("lap") ? "#" + loc.getLap() + "\n" : "") + + formatter.formatDistance(TXT_SHORT, loc.getDistance().longValue()) + + "\n" + + formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)); } - if (iconImage != null) { - // TBD Implement Info popup with the info instead, using the annotaion plugin (currently - // no examples) - String info; - if (type == DB.LOCATION.TYPE_START) { - info = null; - } else { - info = - (iconImage.equals("lap") ? "#" + loc.getLap() + "\n" : "") - + formatter.formatDistance(TXT_SHORT, loc.getDistance().longValue()) - + "\n" - + formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)); - } - - SymbolOptions m = - new SymbolOptions() - .withLatLng(point) - .withIconImage(iconImage) - .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) - .withTextField(info) - .withTextAnchor(Property.TEXT_ANCHOR_TOP); - route.markers.add(m); - } + SymbolOptions m = + new SymbolOptions() + .withLatLng(point) + .withIconImage(iconImage) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + .withTextField(info) + .withTextAnchor(Property.TEXT_ANCHOR_TOP); + route.markers.add(m); } - ll.close(); - - // Track is normally ended with a pause, not always followed by an end - // Ignore the pause - if (route.markers.size() >= 2) { - SymbolOptions m = route.markers.get(route.markers.size() - 2); - SymbolOptions me = route.markers.get(route.markers.size() - 1); - if (m.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString()) - && me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_END).toString())) { - route.markers.remove(route.markers.size() - 2); - } + } + ll.close(); + + // Track is normally ended with a pause, not always followed by an end + // Ignore the pause + if (route.markers.size() >= 2) { + SymbolOptions m = route.markers.get(route.markers.size() - 2); + SymbolOptions me = route.markers.get(route.markers.size() - 1); + if (m.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString()) + && me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_END).toString())) { + route.markers.remove(route.markers.size() - 2); } + } - if (route.markers.size() >= 1) { - SymbolOptions me = route.markers.get(route.markers.size() - 1); - if (me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString())) { - me.withIconImage(((Integer) DB.LOCATION.TYPE_END).toString()); - } + if (!route.markers.isEmpty()) { + SymbolOptions me = route.markers.get(route.markers.size() - 1); + if (me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString())) { + me.withIconImage(((Integer) DB.LOCATION.TYPE_END).toString()); } + } - return route; + return route; + } + + /** + * This method runs on the main UI thread. It takes the processed route data and applies it to the + * map, drawing the polyline and markers. + * + * @param route The processed route data. + * @param mapboxMap The map instance to update. + */ + private void onRouteLoaded(Route route, MapboxMap mapboxMap) { + if (route == null) { + return; } - @SuppressLint("ObsoleteSdkInt") - @Override - protected void onPostExecute(Route route) { - - if (route != null && route.map != null) { - - ScaleBarPlugin scaleBarPlugin = new ScaleBarPlugin(mapView, route.map); - - scaleBarPlugin.create(new ScaleBarOptions(route.context)); - - if (route.path.size() > 1) { - LineOptions lineOptions = - new LineOptions() - .withLatLngs(route.path) - .withLineColor(ColorUtils.colorToRgbaString(Color.RED)) - .withLineWidth(3.0f); - lineManager.create(lineOptions); - Log.v(getClass().getName(), "Added line"); - - final LatLngBounds box = new LatLngBounds.Builder().includes(route.path).build(); - final CameraUpdate initialCameraPosition = CameraUpdateFactory.newLatLngBounds(box, 50); - route.map.moveCamera(initialCameraPosition); - - // Since MapBox 4.2.0-beta.3 moving the camera in onMapReady is not working if map is not - // visible - // The proper solution is a redesign using fragments, see - // https://github.com/mapbox/mapbox-gl-native/issues/6855#event-841575956 - // A workaround is to try move the camera at view updates as long as position is 0,0 - mapView - .getViewTreeObserver() - .addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - // We check which build version we are using. - @Override - public void onGlobalLayout() { - if (route.map.getCameraPosition().target.getLatitude() != 0 - || route.map.getCameraPosition().target.getLongitude() != 0) { - mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } else { - route.map.moveCamera(initialCameraPosition); - } - } - }); - } + ScaleBarPlugin scaleBarPlugin = new ScaleBarPlugin(mapView, mapboxMap); + scaleBarPlugin.create(new ScaleBarOptions(context)); + + if (route.path.size() > 1) { + LineOptions lineOptions = + new LineOptions() + .withLatLngs(route.path) + .withLineColor(ColorUtils.colorToRgbaString(Color.RED)) + .withLineWidth(3.0f); + lineManager.create(lineOptions); + Log.v(getClass().getName(), "Added line"); + + final LatLngBounds box = new LatLngBounds.Builder().includes(route.path).build(); + final CameraUpdate initialCameraPosition = CameraUpdateFactory.newLatLngBounds(box, 50); + mapboxMap.moveCamera(initialCameraPosition); + + // Since MapBox 4.2.0-beta.3 moving the camera in onMapReady is not working if map is not + // visible + // The proper solution is a redesign using fragments, see + // https://github.com/mapbox/mapbox-gl-native/issues/6855#event-841575956 + // A workaround is to try move the camera at view updates as long as position is 0,0 + mapView + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mapboxMap.getCameraPosition().target.getLatitude() != 0 + || mapboxMap.getCameraPosition().target.getLongitude() != 0) { + mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + mapboxMap.moveCamera(initialCameraPosition); + } + } + }); + } - // Images id from text - route - .map - .getStyle() - .addImage( - ((Integer) DB.LOCATION.TYPE_START).toString(), - Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_start))), - false); - route - .map - .getStyle() - .addImage( - ((Integer) DB.LOCATION.TYPE_END).toString(), - Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_end))), - false); - route - .map - .getStyle() - .addImage( - ((Integer) DB.LOCATION.TYPE_PAUSE).toString(), - Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_pause))), - false); - route - .map - .getStyle() - .addImage( - ((Integer) DB.LOCATION.TYPE_RESUME).toString(), - Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_resume))), - false); - route - .map - .getStyle() - .addImage( - "lap", - Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_lap))), - false); - - symbolManager.setIconAllowOverlap(true); - if (route.markers.size() > 0) { - for (SymbolOptions m : route.markers) { - symbolManager.create(m); - } - } - Log.v(getClass().getName(), "Added " + route.markers.size() + " markers"); + // Images id from text + Objects.requireNonNull(mapboxMap + .getStyle()) + .addImage( + ((Integer) DB.LOCATION.TYPE_START).toString(), + Objects.requireNonNull( + BitmapUtils.getBitmapFromDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_start))), + false); + mapboxMap + .getStyle() + .addImage( + ((Integer) DB.LOCATION.TYPE_END).toString(), + Objects.requireNonNull( + BitmapUtils.getBitmapFromDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_end))), + false); + mapboxMap + .getStyle() + .addImage( + ((Integer) DB.LOCATION.TYPE_PAUSE).toString(), + Objects.requireNonNull( + BitmapUtils.getBitmapFromDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_pause))), + false); + mapboxMap + .getStyle() + .addImage( + ((Integer) DB.LOCATION.TYPE_RESUME).toString(), + Objects.requireNonNull( + BitmapUtils.getBitmapFromDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_resume))), + false); + mapboxMap + .getStyle() + .addImage( + "lap", + Objects.requireNonNull( + BitmapUtils.getBitmapFromDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_lap))), + false); + + symbolManager.setIconAllowOverlap(true); + if (!route.markers.isEmpty()) { + for (SymbolOptions m : route.markers) { + symbolManager.create(m); } } + Log.v(getClass().getName(), "Added " + route.markers.size() + " markers"); } } diff --git a/app/src/osmdroid/org/runnerup/util/MapWrapper.java b/app/src/osmdroid/org/runnerup/util/MapWrapper.java index 9c4c51a98..101b4d0ef 100644 --- a/app/src/osmdroid/org/runnerup/util/MapWrapper.java +++ b/app/src/osmdroid/org/runnerup/util/MapWrapper.java @@ -19,14 +19,14 @@ import static org.runnerup.util.Formatter.Format.TXT_SHORT; -import android.annotation.SuppressLint; import android.content.Context; import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; import android.os.Bundle; -import java.util.LinkedList; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; @@ -35,23 +35,26 @@ import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.Polyline; -import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.db.entities.LocationEntity; public class MapWrapper implements Constants { - private final Context context; private final SQLiteDatabase mDB; private final long mID; private final Formatter formatter; private final MapView mapView; - private static final String OSMDROID_USER_AGENT = "org.runnerup.free"; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + private static final java.lang.String OSMDROID_USER_AGENT = "org.runnerup.free"; public MapWrapper( - Context context, SQLiteDatabase mDB, long mID, Formatter formatter, Object mapView) { - this.context = context; + Context context, + SQLiteDatabase mDB, + long mID, + Formatter formatter, + java.lang.Object mapView) { this.mDB = mDB; this.mID = mID; this.formatter = formatter; @@ -69,102 +72,79 @@ public void onCreate(Bundle savedInstanceState) { Configuration.getInstance().setUserAgentValue(OSMDROID_USER_AGENT); - IMapController iMapController = mapView.getController(); - iMapController.setZoom(15.); - - new LoadRoute().execute(new LoadParam(context, mDB, mID, mapView, iMapController)); + loadRouteAsync(); } - class Route { - Route(Context context, MapView mapView) { - this.context = context; - this.mapView = mapView; - this.map = new Polyline(mapView, true); - } - - final Context context; - final MapView mapView; - final Polyline map; - List markers = new ArrayList<>(2); - } - - private class LoadParam { - LoadParam( - Context context, - SQLiteDatabase mDB, - long mID, - MapView mapView, - IMapController iMapController) { - this.context = context; - this.mDB = mDB; - this.mID = mID; - this.mapView = mapView; - this.iMapController = iMapController; - } - - final Context context; - final SQLiteDatabase mDB; - final long mID; - final MapView mapView; - final IMapController iMapController; + // The results from the database query + record Route(Polyline map, List markers, GeoPoint firstPoint) {} + + /** + * Loads the route from the database on a background thread and then updates the UI on the main + * thread. + */ + private void loadRouteAsync() { + executor.execute( + () -> { + final Route route = loadRouteData(); + + // UI update + mapView.post( + () -> { + IMapController mapController = mapView.getController(); + mapController.setZoom(15.); + + if (route.firstPoint != null) { + mapController.setCenter(route.firstPoint); + } + + // Add markers and the polyline to the map + for (Marker marker : route.markers) { + mapView.getOverlays().add(marker); + } + mapView.getOverlays().add(route.map); + mapView.invalidate(); + }); + }); } - @SuppressLint("StaticFieldLeak") - private class LoadRoute extends AsyncTask { - @Override - protected Route doInBackground(LoadParam... params) { - Route route = new Route(params[0].context, params[0].mapView); - SQLiteDatabase mDB = params[0].mDB; - long mID = params[0].mID; - IMapController iMapController = params[0].iMapController; - - route.map.setInfoWindow(null); - route.map.getOutlinePaint().setStrokeWidth(10.f); - LocationEntity.LocationList ll = new LocationEntity.LocationList<>(mDB, mID); - List points = new LinkedList<>(); - int lastLap = -1; - for (LocationEntity loc : ll) { - GeoPoint point = new GeoPoint(loc.getLatitude(), loc.getLongitude()); - points.add(point); - - int lap = loc.getLap(); - if (lastLap != lap) { - Marker marker = new Marker(route.mapView); - marker.setPosition(point); - String info = - "#" + loc.getLap() + " " - + formatter.formatDistance(TXT_SHORT, loc.getDistance().longValue()) - + " " - + formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)); - lastLap = lap; - marker.setTextIcon(info); - marker.setInfoWindow(null); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); - route.markers.add(marker); - } - } - ll.close(); - route.map.setPoints(points); - - if (!points.isEmpty()) { - iMapController.setCenter(points.get(0)); + /** The long-running database query and data processing logic. */ + private Route loadRouteData() { + Polyline polyline = new Polyline(mapView, true); + polyline.setInfoWindow(null); + polyline.getOutlinePaint().setStrokeWidth(10.f); + + java.util.List markers = new ArrayList<>(); + java.util.List points = new LinkedList<>(); + + LocationEntity.LocationList ll = new LocationEntity.LocationList<>(mDB, mID); + int lastLap = -1; + for (LocationEntity loc : ll) { + GeoPoint point = new GeoPoint(loc.getLatitude(), loc.getLongitude()); + points.add(point); + + int lap = loc.getLap(); + if (lastLap != lap) { + Marker marker = new Marker(mapView); + marker.setPosition(point); + java.lang.String info = + "#" + + loc.getLap() + + " " + + formatter.formatDistance(TXT_SHORT, loc.getDistance().longValue()) + + " " + + formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)); + lastLap = lap; + marker.setTextIcon(info); + marker.setInfoWindow(null); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + markers.add(marker); } - - return route; } + ll.close(); + polyline.setPoints(points); - @Override - protected void onPostExecute(Route route) { - - if (route != null && route.map != null && route.markers != null) { - for (Marker marker : route.markers) { - route.mapView.getOverlays().add(marker); - } - - route.mapView.getOverlays().add(route.map); - //Log.v(getClass().getName(), "Added " + route.markers.size() + " markers"); - } - } + GeoPoint firstPoint = points.isEmpty() ? null : points.get(0); + return new Route(polyline, markers, firstPoint); } public void onResume() { @@ -183,5 +163,7 @@ public void onSaveInstanceState(Bundle outState) {} public void onLowMemory() {} - public void onDestroy() {} + public void onDestroy() { + executor.shutdown(); + } } From 31b7f2028abb1d86bd4c49207e0632934123c198 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 2 Jan 2026 17:55:22 +0100 Subject: [PATCH 12/31] feat: MapBox v11 migration For markers, use popup instead of displaying on map. In addition to time/instace, include current pace/elevation/hr. --- .github/workflows/actions.yml | 2 +- app/build.gradle | 17 +- app/res/values/mapbox.xml | 2 +- app/res/values/pref_keys.xml | 2 +- app/src/main/org/runnerup/util/Formatter.java | 6 +- .../org/runnerup/view/DetailActivity.java | 3 +- .../org/runnerup/util/MapViewWrapper.java | 2 +- .../mapbox/org/runnerup/util/MapWrapper.java | 354 +++++++++--------- 8 files changed, 196 insertions(+), 192 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 8065d0c82..454c4b74b 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -106,7 +106,7 @@ jobs: rm $GITHUB_WORKSPACE/mapbox.properties # For special build steps, see https://gitlab.com/fdroid/fdroiddata.git metadata/org.runnerup.free.yml rm -rf wear ANT-Android-SDKs $GITHUB_WORKSPACE/dropbox.properties $GITHUB_WORKSPACE/runalyze.properties - sed -i -e '/play-services/d' -e '/mapboxsdk/d' -e '/api.mapbox.com/d' app/build.gradle + sed -i -e '/play-services/d' -e '/com.mapbox.maps/d' -e '/api.mapbox.com/d' app/build.gradle sed -i -e '/wearable/d' common/build.gradle ./gradlew clean :app:assembleLatestRelease diff --git a/app/build.gradle b/app/build.gradle index 401c321e3..76cedc2d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,18 +149,6 @@ repositories { } } -// Duplicate class kotlin.collections.jdk8 (from MapBox?) -dependencies { - constraints { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0") { - because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") - } - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0") { - because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") - } - } -} - dependencies { implementation project(':common') implementation project(':hrdevice') @@ -187,10 +175,7 @@ dependencies { latestImplementation "com.google.android.gms:play-services-location:${rootProject.ext.googlePlayServicesVersion}" } if (rootProject.ext.mapboxEnabled) { - //noinspection GradleDependency - latestImplementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.6.2' - latestImplementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9:0.9.0' - latestImplementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-scalebar-v8:0.2.0' + latestImplementation 'com.mapbox.maps:android-ndk27:11.17.1' } else { implementation 'org.osmdroid:osmdroid-android:6.1.11' } diff --git a/app/res/values/mapbox.xml b/app/res/values/mapbox.xml index e6ccfe1d1..1ad78ca45 100644 --- a/app/res/values/mapbox.xml +++ b/app/res/values/mapbox.xml @@ -16,5 +16,5 @@ ~ along with this program. If not, see . --> - mapbox://styles/mapbox/outdoors-v11 + mapbox://styles/mapbox/outdoors-v12 diff --git a/app/res/values/pref_keys.xml b/app/res/values/pref_keys.xml index 1e1643eea..6cb084243 100644 --- a/app/res/values/pref_keys.xml +++ b/app/res/values/pref_keys.xml @@ -111,7 +111,7 @@ pref_path_simplification_tolerance pref_path_simplification_algorithm - pref_mapbox_default_style2 + pref_mapbox_default_style3 pref_runneruplive_active pref_runneruplive_serveradress diff --git a/app/src/main/org/runnerup/util/Formatter.java b/app/src/main/org/runnerup/util/Formatter.java index a4c78e712..e93251e12 100644 --- a/app/src/main/org/runnerup/util/Formatter.java +++ b/app/src/main/org/runnerup/util/Formatter.java @@ -400,8 +400,9 @@ public String formatHeartRate(Format target, double heart_rate) { } case TXT: case TXT_SHORT: - case TXT_LONG: return Integer.toString(val2); + case TXT_LONG: + return cueResources.getQuantityString(R.plurals.cue_bpm, val2, val2); } return ""; } @@ -426,8 +427,9 @@ public String formatCadence(Format target, double val) { } case TXT: case TXT_SHORT: - case TXT_LONG: return Integer.toString(val2); + case TXT_LONG: + return cueResources.getQuantityString(R.plurals.cue_rpm, val2, val2); } return ""; } diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index 27e69666a..cb04888c0 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -755,8 +755,7 @@ public View getView(int position, View convertView, ViewGroup parent) { : 0; if (hr > 0) { viewHolder.tvHr.setVisibility(View.VISIBLE); - // Use CUE_LONG instead of TXT_LONG to include unit - viewHolder.tvHr.setText(formatter.formatHeartRate(Formatter.Format.CUE_LONG, hr)); + viewHolder.tvHr.setText(formatter.formatHeartRate(Formatter.Format.TXT_LONG, hr)); } else if (lapHrPresent) { viewHolder.tvHr.setVisibility(View.INVISIBLE); } else { diff --git a/app/src/mapbox/org/runnerup/util/MapViewWrapper.java b/app/src/mapbox/org/runnerup/util/MapViewWrapper.java index 944b59e89..f0d93b776 100644 --- a/app/src/mapbox/org/runnerup/util/MapViewWrapper.java +++ b/app/src/mapbox/org/runnerup/util/MapViewWrapper.java @@ -3,7 +3,7 @@ import android.content.Context; import android.util.AttributeSet; -public class MapViewWrapper extends com.mapbox.mapboxsdk.maps.MapView { +public class MapViewWrapper extends com.mapbox.maps.MapView { public MapViewWrapper(Context context) { super(context); diff --git a/app/src/mapbox/org/runnerup/util/MapWrapper.java b/app/src/mapbox/org/runnerup/util/MapWrapper.java index 0d9e07f8e..2f090f633 100644 --- a/app/src/mapbox/org/runnerup/util/MapWrapper.java +++ b/app/src/mapbox/org/runnerup/util/MapWrapper.java @@ -17,6 +17,7 @@ package org.runnerup.util; +import static org.runnerup.util.Formatter.Format.TXT_LONG; import static org.runnerup.util.Formatter.Format.TXT_SHORT; import android.content.Context; @@ -26,27 +27,26 @@ import android.graphics.Color; import android.os.Bundle; import android.util.Log; -import android.view.ViewTreeObserver; -import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.app.AlertDialog; import androidx.preference.PreferenceManager; -import com.mapbox.mapboxsdk.Mapbox; -import com.mapbox.mapboxsdk.camera.CameraUpdate; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.maps.MapView; -import com.mapbox.mapboxsdk.maps.MapboxMap; -import com.mapbox.mapboxsdk.maps.Style; -import com.mapbox.mapboxsdk.plugins.annotation.LineManager; -import com.mapbox.mapboxsdk.plugins.annotation.LineOptions; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions; -import com.mapbox.mapboxsdk.style.layers.Property; -import com.mapbox.mapboxsdk.utils.BitmapUtils; -import com.mapbox.mapboxsdk.utils.ColorUtils; -import com.mapbox.pluginscalebar.ScaleBarOptions; -import com.mapbox.pluginscalebar.ScaleBarPlugin; +import com.mapbox.common.MapboxOptions; +import com.mapbox.geojson.Point; +import com.mapbox.maps.CameraOptions; +import com.mapbox.maps.MapView; +import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor; +import com.mapbox.maps.extension.style.layers.properties.generated.TextAnchor; +import com.mapbox.maps.plugin.Plugin; +import com.mapbox.maps.plugin.annotation.AnnotationConfig; +import com.mapbox.maps.plugin.annotation.AnnotationPlugin; +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager; +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManagerKt; +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions; +import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager; +import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManagerKt; +import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions; +import com.mapbox.maps.plugin.locationcomponent.utils.BitmapUtils; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -58,9 +58,6 @@ public class MapWrapper implements Constants { private final MapView mapView; - private LineManager lineManager; - private SymbolManager symbolManager; - private final long mID; private final SQLiteDatabase mDB; private final Context context; @@ -82,87 +79,54 @@ public MapWrapper( } public static void start(Context context) { - Mapbox.getInstance(context, BuildConfig.MAPBOX_ACCESS_TOKEN); + MapboxOptions.setAccessToken(BuildConfig.MAPBOX_ACCESS_TOKEN); } public void onCreate(Bundle savedInstanceState) { - mapView.onCreate(savedInstanceState); - mapView.getMapAsync( - mapboxMap -> { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - Resources res = context.getResources(); - String val = - prefs.getString( - res.getString(R.string.pref_mapbox_default_style), - res.getString(R.string.mapboxDefaultStyle)); - - Style.Builder style = new Style.Builder().fromUri(val); - mapboxMap.setStyle( - style, - mapStyle -> { - lineManager = new LineManager(mapView, mapboxMap, mapStyle); - symbolManager = new SymbolManager(mapView, mapboxMap, mapStyle); - // Map is set up and the style has loaded, now load the route data - loadRouteAsync(mapboxMap); - }); - }); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Resources res = context.getResources(); + String val = + prefs.getString( + res.getString(R.string.pref_mapbox_default_style), + res.getString(R.string.mapboxDefaultStyle)); + mapView + .getMapboxMap() + .loadStyle( + val, + mapStyle -> { + loadRouteAsync(mapView); + }); } - public void onResume() { - mapView.onResume(); - } + public void onResume() {} - public void onStart() { - mapView.onStart(); - } + public void onStart() {} - public void onStop() { - mapView.onStop(); - } + public void onStop() {} - public void onPause() { - mapView.onPause(); - } + public void onPause() {} - public void onSaveInstanceState(Bundle outState) { - mapView.onSaveInstanceState(outState); - } + public void onSaveInstanceState(Bundle outState) {} - public void onLowMemory() { - mapView.onLowMemory(); - } + public void onLowMemory() {} public void onDestroy() { executor.shutdown(); - - if (lineManager != null) { - lineManager.onDestroy(); - } - if (symbolManager != null) { - symbolManager.onDestroy(); - } - mapView.onDestroy(); - } - - /** Class to hold the processed route data from the background task. */ - static class Route { - final java.util.List path = new ArrayList<>(10); - final java.util.ArrayList markers = new ArrayList<>(10); } /** * Kicks off the process of loading route data on a background thread and then updating the UI. * - * @param mapboxMap The map instance to update once data is loaded. + * @param mapView The map instance to update once data is loaded. */ - private void loadRouteAsync(final MapboxMap mapboxMap) { + private void loadRouteAsync(final MapView mapView) { executor.execute( () -> { // DB queries, data processing on the background thread final Route route = loadRouteDataInBackground(); // update the UI on the main thread - mapView.post(() -> onRouteLoaded(route, mapboxMap)); + mapView.post(() -> onRouteLoaded(route, mapView)); }); } @@ -177,7 +141,7 @@ private Route loadRouteDataInBackground() { LocationEntity.LocationList ll = new LocationEntity.LocationList<>(mDB, mID); int lastLap = 0; for (LocationEntity loc : ll) { - LatLng point = new LatLng(loc.getLatitude(), loc.getLongitude()); + Point point = Point.fromLngLat(loc.getLongitude(), loc.getLatitude()); route.path.add(point); java.lang.Integer type; @@ -202,27 +166,26 @@ private Route loadRouteDataInBackground() { } if (iconImage != null) { - // TBD Implement Info popup with the info instead, using the annotaion plugin (currently - // no examples) - java.lang.String info; - if (type == DB.LOCATION.TYPE_START) { - info = null; - } else { - info = - (iconImage.equals("lap") ? "#" + loc.getLap() + "\n" : "") - + formatter.formatDistance(TXT_SHORT, loc.getDistance().longValue()) - + "\n" - + formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)); - } - - SymbolOptions m = - new SymbolOptions() - .withLatLng(point) - .withIconImage(iconImage) - .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) - .withTextField(info) - .withTextAnchor(Property.TEXT_ANCHOR_TOP); - route.markers.add(m); + String pointInfo; + String popupInfo; + pointInfo = (iconImage.equals("lap") ? "#" + loc.getLap() + "\n" : ""); + popupInfo = + pointInfo + + (formatter.formatDistance(TXT_LONG, loc.getDistance().longValue()) + "\n") + + (formatter.formatElapsedTime(TXT_SHORT, Math.round(loc.getElapsed() / 1000.0)) + + "\n") + + (formatter.formatDateTime(loc.getTime() / 1000) + "\n") + + (loc.getSpeed() != null + ? formatter.formatPaceSpeed(TXT_LONG, loc.getSpeed()) + "\n" + : "") + + (loc.getAltitude() != null + ? formatter.formatElevation(TXT_LONG, loc.getAltitude()) + "\n" + : "") + + (loc.getHr() != null + ? formatter.formatHeartRate(TXT_LONG, loc.getHr()) + "\n" + : ""); + + route.markers.add(new RouteMarker(point, iconImage, pointInfo, popupInfo)); } } ll.close(); @@ -230,18 +193,18 @@ private Route loadRouteDataInBackground() { // Track is normally ended with a pause, not always followed by an end // Ignore the pause if (route.markers.size() >= 2) { - SymbolOptions m = route.markers.get(route.markers.size() - 2); - SymbolOptions me = route.markers.get(route.markers.size() - 1); - if (m.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString()) - && me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_END).toString())) { + RouteMarker m = route.markers.get(route.markers.size() - 2); + RouteMarker me = route.markers.get(route.markers.size() - 1); + if (m.image.equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString()) + && me.image.equals(((Integer) DB.LOCATION.TYPE_END).toString())) { route.markers.remove(route.markers.size() - 2); } } if (!route.markers.isEmpty()) { - SymbolOptions me = route.markers.get(route.markers.size() - 1); - if (me.getIconImage().equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString())) { - me.withIconImage(((Integer) DB.LOCATION.TYPE_END).toString()); + RouteMarker me = route.markers.get(route.markers.size() - 1); + if (me.image.equals(((Integer) DB.LOCATION.TYPE_PAUSE).toString())) { + me.image = (((Integer) DB.LOCATION.TYPE_END).toString()); } } @@ -253,98 +216,153 @@ private Route loadRouteDataInBackground() { * map, drawing the polyline and markers. * * @param route The processed route data. - * @param mapboxMap The map instance to update. + * @param mapView The map instance to update. */ - private void onRouteLoaded(Route route, MapboxMap mapboxMap) { - if (route == null) { + private void onRouteLoaded(Route route, MapView mapView) { + AnnotationPlugin annotationApi = mapView.getPlugin(Plugin.MAPBOX_ANNOTATION_PLUGIN_ID); + if (route == null + || annotationApi == null + || route.path.size() <= 1 + || route.markers.isEmpty()) { return; } - ScaleBarPlugin scaleBarPlugin = new ScaleBarPlugin(mapView, mapboxMap); - scaleBarPlugin.create(new ScaleBarOptions(context)); - - if (route.path.size() > 1) { - LineOptions lineOptions = - new LineOptions() - .withLatLngs(route.path) - .withLineColor(ColorUtils.colorToRgbaString(Color.RED)) - .withLineWidth(3.0f); - lineManager.create(lineOptions); - Log.v(getClass().getName(), "Added line"); - - final LatLngBounds box = new LatLngBounds.Builder().includes(route.path).build(); - final CameraUpdate initialCameraPosition = CameraUpdateFactory.newLatLngBounds(box, 50); - mapboxMap.moveCamera(initialCameraPosition); - - // Since MapBox 4.2.0-beta.3 moving the camera in onMapReady is not working if map is not - // visible - // The proper solution is a redesign using fragments, see - // https://github.com/mapbox/mapbox-gl-native/issues/6855#event-841575956 - // A workaround is to try move the camera at view updates as long as position is 0,0 - mapView - .getViewTreeObserver() - .addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - if (mapboxMap.getCameraPosition().target.getLatitude() != 0 - || mapboxMap.getCameraPosition().target.getLongitude() != 0) { - mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } else { - mapboxMap.moveCamera(initialCameraPosition); - } - } - }); - } + // Add layers bottom to top + AnnotationConfig configLine = new AnnotationConfig(null, null, "bottom"); + PolylineAnnotationManager polylineAnnotationManager = + PolylineAnnotationManagerKt.createPolylineAnnotationManager(annotationApi, configLine); + + AnnotationConfig configMarker = new AnnotationConfig(null, null, "top"); + PointAnnotationManager pointAnnotationManager = + PointAnnotationManagerKt.createPointAnnotationManager(annotationApi, configMarker); + + PolylineAnnotationOptions options = + new PolylineAnnotationOptions() + .withPoints(route.path) + .withLineColor(Color.RED) + .withLineWidth(3.0f); + polylineAnnotationManager.create(options); + Log.v(getClass().getName(), "Added line"); + + // The view must adjust to the route path bounds after the view is first loaded + // subscribeMapLoaded could be used too, but the async version of cameraForCoordinates() is + // still needed + CameraOptions camera = new CameraOptions.Builder().build(); + com.mapbox.maps.EdgeInsets padding = new com.mapbox.maps.EdgeInsets(100.0, 50.0, 50.0, 50.0); + mapView + .getMapboxMap() + .cameraForCoordinates( + route.path, + camera, + padding, + null, + null, + cameraOptions -> { + mapView.getMapboxMap().setCamera(cameraOptions); + Log.d(getClass().getName(), "Camera zoomed to route bounds after map idle event."); + return null; + }); // Images id from text - Objects.requireNonNull(mapboxMap - .getStyle()) + Objects.requireNonNull(mapView.getMapboxMap().getStyle()) .addImage( ((Integer) DB.LOCATION.TYPE_START).toString(), Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_start))), + BitmapUtils.INSTANCE.getBitmapFromDrawableRes( + context, R.drawable.ic_map_marker_start)), false); - mapboxMap + mapView + .getMapboxMap() .getStyle() .addImage( ((Integer) DB.LOCATION.TYPE_END).toString(), Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_end))), + BitmapUtils.INSTANCE.getBitmapFromDrawableRes( + context, R.drawable.ic_map_marker_end)), false); - mapboxMap + mapView + .getMapboxMap() .getStyle() .addImage( ((Integer) DB.LOCATION.TYPE_PAUSE).toString(), Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_pause))), + BitmapUtils.INSTANCE.getBitmapFromDrawableRes( + context, R.drawable.ic_map_marker_pause)), false); - mapboxMap + mapView + .getMapboxMap() .getStyle() .addImage( ((Integer) DB.LOCATION.TYPE_RESUME).toString(), Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_resume))), + BitmapUtils.INSTANCE.getBitmapFromDrawableRes( + context, R.drawable.ic_map_marker_resume)), false); - mapboxMap + mapView + .getMapboxMap() .getStyle() .addImage( "lap", Objects.requireNonNull( - BitmapUtils.getBitmapFromDrawable( - AppCompatResources.getDrawable(context, R.drawable.ic_map_marker_lap))), + BitmapUtils.INSTANCE.getBitmapFromDrawableRes( + context, R.drawable.ic_map_marker_lap)), false); - symbolManager.setIconAllowOverlap(true); - if (!route.markers.isEmpty()) { - for (SymbolOptions m : route.markers) { - symbolManager.create(m); - } + List points = new ArrayList<>(route.markers.size()); + for (RouteMarker m : route.markers) { + PointAnnotationOptions o = + new PointAnnotationOptions() + .withPoint(m.point) + .withIconImage(m.image) + .withIconAnchor(IconAnchor.BOTTOM) + .withTextField(m.info) + .withTextOffset( + new ArrayList<>() { + { + add(0.0); + add(1.0); + } + }) + .withTextAnchor(TextAnchor.BOTTOM); + points.add(o); } - Log.v(getClass().getName(), "Added " + route.markers.size() + " markers"); + pointAnnotationManager.create(points); + + pointAnnotationManager.addClickListener( + pointAnnotation -> { + // Find the original marker data, ignore overlapping + for (RouteMarker m : route.markers) { + if (m.point.equals(pointAnnotation.getPoint())) { + // Show a dialog with its info. + new AlertDialog.Builder(context) + .setMessage(m.popupInfo) + .setPositiveButton(android.R.string.ok, null) + .show(); + break; + } + } + return true; + }); + } + + /** RouteMarker (start, pause, end, lap) */ + static class RouteMarker { + Point point; + String image; + String info; + String popupInfo; + + public RouteMarker(Point point, String iconImage, String pointInfo, String popupInfo) { + this.point = point; + this.image = iconImage; + this.info = pointInfo; + this.popupInfo = popupInfo; + } + } + + /** Class to hold the processed route data from the background task. */ + static class Route { + final java.util.List path = new ArrayList<>(10); + final java.util.ArrayList markers = new ArrayList<>(10); } } From 261afd1983654a01bcde8e15015a027fba0ad9ed Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 6 Jan 2026 21:09:54 +0100 Subject: [PATCH 13/31] fix: progress osmdroid --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 76cedc2d1..d932832bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,7 +177,7 @@ dependencies { if (rootProject.ext.mapboxEnabled) { latestImplementation 'com.mapbox.maps:android-ndk27:11.17.1' } else { - implementation 'org.osmdroid:osmdroid-android:6.1.11' + implementation 'org.osmdroid:osmdroid-android:6.1.20' } latestImplementation 'com.jjoe64:graphview:4.2.2' From b75856353b92f0c4af9bfa1114f94b9940145163 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Mon, 5 Jan 2026 18:13:48 +0100 Subject: [PATCH 14/31] fix: use Executor for background tasks Followup to previous AsyncTask migration. Partly using Gemini, but manual fixes required. --- .../main/org/runnerup/export/SyncManager.java | 755 +++++++++--------- .../main/org/runnerup/util/GraphWrapper.java | 538 +++++++------ .../runnerup/view/ManageWorkoutsActivity.java | 16 +- 3 files changed, 645 insertions(+), 664 deletions(-) diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index 2e7ffd033..235cc2321 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -18,7 +18,6 @@ package org.runnerup.export; import android.Manifest; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; @@ -28,9 +27,10 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; import android.os.Build; import android.os.Environment; +import android.os.Handler; +import android.os.Looper; import android.text.InputType; import android.util.Log; import android.util.Pair; @@ -56,6 +56,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.json.JSONException; import org.json.JSONObject; import org.runnerup.BuildConfig; @@ -76,33 +78,96 @@ public class SyncManager { public static final long ERROR_ACTIVITY_ID = -1L; // Id to identify a permission request. private static final int REQUEST_STORAGE = 3003; - - private SQLiteDatabase mDB = null; - private AppCompatActivity mActivity = null; - private Context mContext = null; private final Map synchronizers = new HashMap<>(); private final LongSparseArray synchronizersById = new LongSparseArray<>(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); PathSimplifier simplifier; - + private SQLiteDatabase mDB = null; + private AppCompatActivity mActivity = null; + private Context mContext = null; private ProgressDialog mSpinner = null; + private Synchronizer authSynchronizer = null; + private Callback authCallback = null; + private long mID = 0; + private Callback uploadCallback = null; + private HashSet pendingSynchronizers = null; + private final Callback disableSynchronizerCallback = + (synchronizerName, status) -> nextSynchronizer(); + private Callback listWorkoutCallback = null; + private HashSet pendingListWorkout = null; + private ArrayList workoutRef = null; - public enum SyncMode { - DOWNLOAD(org.runnerup.common.R.string.Downloading_from_1s), - UPLOAD(org.runnerup.common.R.string.Uploading_to_1s); + /** Synch set of activities for a specific synchronizer */ + private List syncActivitiesList = null; - final int textId; + private Callback syncActivityCallback = null; + private StringBuffer cancelSync = null; - SyncMode(int textId) { - this.textId = textId; + public SyncManager(AppCompatActivity activity) { + init(activity, activity, new ProgressDialog(activity)); + } + + public SyncManager(Context context) { + init(null, context, new ProgressDialog(context)); + } + + public SyncManager(Context context, ProgressDialog spinner) { + init(null, context, spinner); + } + + private static void externalIdCompleted( + Synchronizer synchronizer, SQLiteDatabase copyDB, Synchronizer.Status status) { + ContentValues tmp = new ContentValues(); + tmp.put(DB.EXPORT.STATUS, status.externalIdStatus.getInt()); + tmp.put(DB.EXPORT.EXTERNAL_ID, status.externalId); + String[] args = {Long.toString(synchronizer.getId()), Long.toString(status.activityId)}; + copyDB.update( + DB.EXPORT.TABLE, tmp, DB.EXPORT.ACCOUNT + "= ? AND " + DB.EXPORT.ACTIVITY + " = ?", args); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean compareFiles(File w, File f) { + if (w.length() != f.length()) return false; + + boolean cmp = true; + FileInputStream f1 = null; + FileInputStream f2 = null; + try { + f1 = new FileInputStream(w); + f2 = new FileInputStream(f); + byte[] buf1 = new byte[1024]; + byte[] buf2 = new byte[1024]; + do { + int cnt1 = f1.read(buf1); + int cnt2 = f2.read(buf2); + if (cnt1 <= 0 || cnt2 <= 0) break; + + if (!java.util.Arrays.equals(buf1, buf2)) { + cmp = false; + break; + } + } while (true); + } catch (Exception ex) { + // f1, f2 checked } - public int getTextId() { - return textId; + if (f1 != null) { + try { + f1.close(); + } catch (IOException e) { + e.printStackTrace(); + } } - } - public interface Callback { - void run(String synchronizerName, Synchronizer.Status status); + if (f2 != null) { + try { + f2.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return cmp; } private void init(AppCompatActivity activity, Context context, ProgressDialog spinner) { @@ -114,19 +179,8 @@ private void init(AppCompatActivity activity, Context context, ProgressDialog sp simplifier = PathSimplifier.getPathSimplifierForExport(context); } - public SyncManager(AppCompatActivity activity) { - init(activity, activity, new ProgressDialog(activity)); - } - - public SyncManager(Context context) { - init(null, context, new ProgressDialog(context)); - } - - public SyncManager(Context context, ProgressDialog spinner) { - init(null, context, spinner); - } - public synchronized void close() { + executor.shutdown(); if (mDB != null) { DBHelper.closeDB(mDB); } @@ -279,9 +333,6 @@ private Status handleRefreshComplete(final Synchronizer synchronizer, final Stat return s; } - private Synchronizer authSynchronizer = null; - private Callback authCallback = null; - private void handleAuth(Callback callback, final Synchronizer l, AuthMethod authMethod) { authSynchronizer = l; authCallback = callback; @@ -402,32 +453,31 @@ private void askUsernamePassword(final Synchronizer sync, final AuthMethod authM .show(); } - @SuppressLint("StaticFieldLeak") private void testUserPass(final Synchronizer l, final JSONObject authConfig) { mSpinner.setTitle("Testing login " + l.getName()); - new AsyncTask() { + executor.execute( + () -> { + final ContentValues config = new ContentValues(); + config.put(DB.ACCOUNT.AUTH_CONFIG, authConfig.toString()); + config.put("_id", l.getId()); + l.init(config); - final ContentValues config = new ContentValues(); + Status result; + try { + result = l.connect(); + } catch (Exception ex) { + Log.e(getClass().getName(), "Connection test failed", ex); + result = Synchronizer.Status.ERROR; + } - @Override - protected Synchronizer.Status doInBackground(Synchronizer... params) { - config.put(DB.ACCOUNT.AUTH_CONFIG, authConfig.toString()); - config.put("_id", l.getId()); - l.init(config); - try { - return params[0].connect(); - } catch (Exception ex) { - ex.printStackTrace(); - return Synchronizer.Status.ERROR; - } - } + final Status finalResult = result; - @Override - protected void onPostExecute(Synchronizer.Status result) { - handleAuthComplete(l, result); - } - }.execute(l); + mainHandler.post( + () -> { + handleAuthComplete(l, finalResult); + }); + }); } private void askFileUrl(final Synchronizer sync) { @@ -521,7 +571,7 @@ private boolean checkStoragePermissions(final AppCompatActivity activity) { defaultPerms.add(perm); } } - if (defaultPerms.size() > 0) { + if (!defaultPerms.isEmpty()) { // Request permission, dont care about the result final String[] perms = new String[defaultPerms.size()]; defaultPerms.toArray(perms); @@ -532,10 +582,6 @@ private boolean checkStoragePermissions(final AppCompatActivity activity) { return result; } - private long mID = 0; - private Callback uploadCallback = null; - private HashSet pendingSynchronizers = null; - public void startUploading(Callback callback, HashSet synchronizers, long id) { mID = id; uploadCallback = callback; @@ -557,7 +603,6 @@ private void nextSynchronizer() { doUpload(synchronizer); } - @SuppressLint("StaticFieldLeak") private void doUpload(final Synchronizer synchronizer) { final ProgressDialog copySpinner = mSpinner; final SQLiteDatabase copyDB = DBHelper.getWritableDatabase(mContext); @@ -565,58 +610,54 @@ private void doUpload(final Synchronizer synchronizer) { copySpinner.setMessage( getResources().getString(SyncMode.UPLOAD.getTextId(), synchronizer.getName())); - new AsyncTask() { - - @Override - protected Synchronizer.Status doInBackground(Synchronizer... params) { - try { - Synchronizer.Status s2 = params[0].upload(copyDB, mID); - // See doUpload() for motivation - if (s2 == Synchronizer.Status.NEED_REFRESH) { - s2 = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); - if (s2 == Synchronizer.Status.OK) { - s2 = params[0].upload(copyDB, mID); + executor.execute( + () -> { + Status result; + try { + result = synchronizer.upload(copyDB, mID); + // See doUpload() for motivation + if (result == Synchronizer.Status.NEED_REFRESH) { + result = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); + if (result == Synchronizer.Status.OK) { + result = synchronizer.upload(copyDB, mID); + } } + } catch (Exception ex) { + Log.e(getClass().getName(), "Upload failed", ex); + result = Synchronizer.Status.ERROR; } - return s2; - } catch (Exception ex) { - ex.printStackTrace(); - return Synchronizer.Status.ERROR; - } - } - @Override - protected void onPostExecute(Synchronizer.Status result) { - switch (result) { - case OK: - syncOK(synchronizer, copySpinner, copyDB, result); - nextSynchronizer(); - break; - - case NEED_AUTH: - handleAuth( - (synchronizerName, status) -> { - if (status == Synchronizer.Status.OK) { - doUpload(synchronizer); - } else { + final Status finalResult = result; + + mainHandler.post( + () -> { + switch (finalResult) { + case OK: + syncOK(synchronizer, copySpinner, copyDB, finalResult); nextSynchronizer(); - } - }, - synchronizer, - result.authMethod); - return; - - case CANCEL: - pendingSynchronizers.clear(); - doneUploading(); - return; - - default: - nextSynchronizer(); - break; - } - } - }.execute(synchronizer); + break; + case NEED_AUTH: + handleAuth( + (synchronizerName, status) -> { + if (status == Synchronizer.Status.OK) { + doUpload(synchronizer); + } else { + nextSynchronizer(); + } + }, + synchronizer, + finalResult.authMethod); + break; + case CANCEL: + pendingSynchronizers.clear(); + doneUploading(); + break; + default: + nextSynchronizer(); + break; + } + }); + }); } /** @@ -627,31 +668,24 @@ protected void onPostExecute(Synchronizer.Status result) { * @param copyDB * @param status */ - private static void getExternalId( + private void getExternalId( final Synchronizer synchronizer, final SQLiteDatabase copyDB, final Synchronizer.Status status) { if (status.externalIdStatus == Synchronizer.ExternalIdStatus.PENDING) { - new AsyncTask() { - - @Override - protected Synchronizer.Status doInBackground(Void... args) { - // Implementation must delay the call rate - return synchronizer.getExternalId(copyDB, status); - } - - @Override - protected void onPostExecute(Synchronizer.Status result) { - // the external status is updated, check - externalIdCompleted(synchronizer, copyDB, result); - } - }.execute(); + executor.execute( + () -> { + // Implementation must delay the call rate + final Status result = synchronizer.getExternalId(copyDB, status); + + mainHandler.post( + () -> + // the external status is updated, check + externalIdCompleted(synchronizer, copyDB, result)); + }); } } - private final Callback disableSynchronizerCallback = - (synchronizerName, status) -> nextSynchronizer(); - private void syncOK( Synchronizer synchronizer, ProgressDialog copySpinner, @@ -670,16 +704,6 @@ private void syncOK( getExternalId(synchronizer, copyDB, status); } - private static void externalIdCompleted( - Synchronizer synchronizer, SQLiteDatabase copyDB, Synchronizer.Status status) { - ContentValues tmp = new ContentValues(); - tmp.put(DB.EXPORT.STATUS, status.externalIdStatus.getInt()); - tmp.put(DB.EXPORT.EXTERNAL_ID, status.externalId); - String[] args = {Long.toString(synchronizer.getId()), Long.toString(status.activityId)}; - copyDB.update( - DB.EXPORT.TABLE, tmp, DB.EXPORT.ACCOUNT + "= ? AND " + DB.EXPORT.ACTIVITY + " = ?", args); - } - private void doneUploading() { try { mSpinner.dismiss(); @@ -712,17 +736,31 @@ private void disableSynchronizer( private void resetDB( final Callback callback, final Synchronizer synchronizer, final boolean clearUploads) { - final String[] args = {Long.toString(synchronizer.getId())}; - ContentValues config = new ContentValues(); - config.putNull(DB.ACCOUNT.AUTH_CONFIG); - mDB.update(DB.ACCOUNT.TABLE, config, "_id = ?", args); + mSpinner.show(); + mSpinner.setTitle("Resetting " + synchronizer.getName()); - if (clearUploads) { - mDB.delete(DB.EXPORT.TABLE, DB.EXPORT.ACCOUNT + " = ?", args); - } + executor.execute( + () -> { + try { + final String[] args = {Long.toString(synchronizer.getId())}; + ContentValues config = new ContentValues(); + config.putNull(DB.ACCOUNT.AUTH_CONFIG); + mDB.update(DB.ACCOUNT.TABLE, config, "_id = ?", args); - synchronizer.reset(); - callback.run(synchronizer.getName(), Synchronizer.Status.OK); + if (clearUploads) { + mDB.delete(DB.EXPORT.TABLE, DB.EXPORT.ACCOUNT + " = ?", args); + } + synchronizer.reset(); + } catch (Exception ex) { + Log.e(getClass().getName(), "Failed to reset " + synchronizer.getName(), ex); + } + + mainHandler.post( + () -> { + mSpinner.dismiss(); + callback.run(synchronizer.getName(), Synchronizer.Status.OK); + }); + }); } public void clearUploadsByName(Callback callback, String synchronizerName) { @@ -741,7 +779,6 @@ public void clearUpload(String name, long id) { } } - @SuppressLint("StaticFieldLeak") public void loadActivityList( final List items, final String synchronizerName, final Callback callback) { mSpinner.setTitle(getResources().getString(org.runnerup.common.R.string.Loading_activities)); @@ -750,35 +787,27 @@ public void loadActivityList( .getString(org.runnerup.common.R.string.Fetching_activities_from_1s, synchronizerName)); mSpinner.show(); - new AsyncTask() { - @Override - protected Synchronizer.Status doInBackground(Synchronizer... params) { - return params[0].listActivities(items); - } - - @Override - protected void onPostExecute(Synchronizer.Status result) { - callback.run(synchronizerName, result); - mSpinner.dismiss(); - } - }.execute(synchronizers.get(synchronizerName)); - } - - public static class WorkoutRef { - public WorkoutRef(String synchronizerName, String key, String workoutName) { - this.synchronizer = synchronizerName; - this.workoutKey = key; - this.workoutName = workoutName; + final Synchronizer synchronizer = synchronizers.get(synchronizerName); + if (synchronizer == null) { + mainHandler.post( + () -> { + mSpinner.dismiss(); + callback.run(synchronizerName, Status.ERROR); + }); + return; } - public final String synchronizer; - public final String workoutKey; - public final String workoutName; - } + executor.execute( + () -> { + final Status result = synchronizer.listActivities(items); - private Callback listWorkoutCallback = null; - private HashSet pendingListWorkout = null; - private ArrayList workoutRef = null; + mainHandler.post( + () -> { + callback.run(synchronizerName, result); + mSpinner.dismiss(); + }); + }); + } public void loadWorkoutList( ArrayList workoutRef, Callback callback, HashSet wourkouts) { @@ -809,62 +838,58 @@ private void nextListWorkout() { doListWorkout(synchronizer); } - @SuppressLint("StaticFieldLeak") private void doListWorkout(final Synchronizer synchronizer) { final ProgressDialog copySpinner = mSpinner; - copySpinner.setMessage("Listing from " + synchronizer.getName()); - final ArrayList> list = new ArrayList<>(); - - new AsyncTask() { - - @Override - protected Synchronizer.Status doInBackground(Synchronizer... params) { - try { - Synchronizer.Status s2 = params[0].listWorkouts(list); - // See doUpload() for motivation - if (s2 == Synchronizer.Status.NEED_REFRESH) { - s2 = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); - if (s2 == Synchronizer.Status.OK) { - s2 = params[0].listWorkouts(list); + + executor.execute( + () -> { + final ArrayList> list = new ArrayList<>(); + Status result; + try { + result = synchronizer.listWorkouts(list); + if (result == Synchronizer.Status.NEED_REFRESH) { + result = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); + if (result == Synchronizer.Status.OK) { + result = synchronizer.listWorkouts(list); + } } + } catch (Exception ex) { + Log.e(getClass().getName(), "List workouts failed", ex); + result = Synchronizer.Status.ERROR; } - return s2; - } catch (Exception ex) { - ex.printStackTrace(); - return Synchronizer.Status.ERROR; - } - } - @Override - protected void onPostExecute(Synchronizer.Status result) { - switch (result) { - case OK: - for (Pair w : list) { - workoutRef.add(new WorkoutRef(synchronizer.getName(), w.first, w.second)); - } - nextListWorkout(); - break; - - case NEED_AUTH: - handleAuth( - (synchronizerName, status) -> { - if (status == Synchronizer.Status.OK) { - doListWorkout(synchronizer); - } else { // Unexpected result, nothing to do + final Status finalResult = result; + mainHandler.post( + () -> { + switch (finalResult) { + case OK: + for (Pair w : list) { + workoutRef.add(new WorkoutRef(synchronizer.getName(), w.first, w.second)); + } nextListWorkout(); - } - }, - synchronizer, - result.authMethod); - return; - - default: - nextListWorkout(); - break; - } - } - }.execute(synchronizer); + break; + + case NEED_AUTH: + handleAuth( + (synchronizerName, status) -> { + if (status == Synchronizer.Status.OK) { + doListWorkout(synchronizer); + } else { + // Unexpected result, nothing to do + nextListWorkout(); + } + }, + synchronizer, + finalResult.authMethod); + return; + + default: + nextListWorkout(); + break; + } + }); + }); } private void doneListing() { @@ -874,105 +899,55 @@ private void doneListing() { if (cb != null) cb.run(null, Status.OK); } - @SuppressLint("StaticFieldLeak") public void loadWorkouts(final HashSet pendingWorkouts, final Callback callback) { int cnt = pendingWorkouts.size(); mSpinner.setTitle("Downloading workouts (" + cnt + ")"); mSpinner.show(); - new AsyncTask() { - - @Override - protected void onProgressUpdate(String... values) { - mSpinner.setMessage("Loading " + values[0] + " from " + values[1]); - } - @Override - protected Synchronizer.Status doInBackground(String... params0) { - for (WorkoutRef ref : pendingWorkouts) { - publishProgress(ref.workoutName, ref.synchronizer); - Synchronizer synchronizer = synchronizers.get(ref.synchronizer); - File f = WorkoutSerializer.getFile(mContext, ref.workoutName); - File w = f; - if (f.exists()) { - w = WorkoutSerializer.getFile(mContext, ref.workoutName + ".tmp"); - } - try { - synchronizer.downloadWorkout(w, ref.workoutKey); - if (w != f) { - if (!compareFiles(w, f)) { - Log.w(getClass().getName(), "overwriting " + f.getPath() + " with " + w.getPath()); - // TODO dialog - //noinspection ResultOfMethodCallIgnored - f.delete(); - //noinspection ResultOfMethodCallIgnored - w.renameTo(f); - } else { - Log.i(getClass().getName(), "file identical...deleting temporary " + w.getPath()); - //noinspection ResultOfMethodCallIgnored - w.delete(); + executor.execute( + () -> { + for (WorkoutRef ref : pendingWorkouts) { + mainHandler.post( + () -> + mSpinner.setMessage( + "Loading " + ref.workoutName + " from " + ref.synchronizer)); + + Synchronizer synchronizer = synchronizers.get(ref.synchronizer); + if (synchronizer == null) continue; + + File f = WorkoutSerializer.getFile(mContext, ref.workoutName); + File w = f; + if (f.exists()) { + w = WorkoutSerializer.getFile(mContext, ref.workoutName + ".tmp"); + } + try { + synchronizer.downloadWorkout(w, ref.workoutKey); + if (w != f) { + if (!compareFiles(w, f)) { + Log.w( + getClass().getName(), "overwriting " + f.getPath() + " with " + w.getPath()); + // TODO dialog + if (f.exists()) f.delete(); + w.renameTo(f); + } else { + Log.v(getClass().getName(), "file identical...deleting temporary " + w.getPath()); + w.delete(); + } } + } catch (Exception e) { + Log.e(getClass().getName(), "Workout download failed for " + ref.workoutName, e); + w.delete(); } - } catch (Exception e) { - e.printStackTrace(); - //noinspection ResultOfMethodCallIgnored - w.delete(); } - } - return Synchronizer.Status.OK; - } - @Override - protected void onPostExecute(Synchronizer.Status result) { - mSpinner.dismiss(); - if (callback != null) { - callback.run(null, Synchronizer.Status.OK); - } - } - }.execute("string"); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private static boolean compareFiles(File w, File f) { - if (w.length() != f.length()) return false; - - boolean cmp = true; - FileInputStream f1 = null; - FileInputStream f2 = null; - try { - f1 = new FileInputStream(w); - f2 = new FileInputStream(f); - byte[] buf1 = new byte[1024]; - byte[] buf2 = new byte[1024]; - do { - int cnt1 = f1.read(buf1); - int cnt2 = f2.read(buf2); - if (cnt1 <= 0 || cnt2 <= 0) break; - - if (!java.util.Arrays.equals(buf1, buf2)) { - cmp = false; - break; - } - } while (true); - } catch (Exception ex) { - // f1, f2 checked - } - - if (f1 != null) { - try { - f1.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - if (f2 != null) { - try { - f2.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return cmp; + mainHandler.post( + () -> { + mSpinner.dismiss(); + if (callback != null) { + callback.run(null, Synchronizer.Status.OK); + } + }); + }); } /** @@ -999,12 +974,6 @@ public Context getContext() { return mContext; } - /** Synch set of activities for a specific synchronizer */ - private List syncActivitiesList = null; - - private Callback syncActivityCallback = null; - private StringBuffer cancelSync = null; - public void syncActivities( SyncMode mode, Callback synchCallback, @@ -1097,85 +1066,80 @@ private void syncNextActivity(final Synchronizer synchronizer, SyncMode mode) { doSyncMulti(synchronizer, mode, ai); } - @SuppressLint("StaticFieldLeak") private void doSyncMulti( final Synchronizer synchronizer, final SyncMode mode, final SyncActivityItem activityItem) { final ProgressDialog copySpinner = mSpinner; final SQLiteDatabase copyDB = DBHelper.getWritableDatabase(mContext); - copySpinner.setMessage(Long.toString(1 + syncActivitiesList.size()) + " remaining"); - new AsyncTask() { - - @Override - protected Synchronizer.Status doInBackground(Synchronizer... params) { - try { - Synchronizer.Status s2; - switch (mode) { - case UPLOAD: - s2 = synchronizer.upload(copyDB, activityItem.getId()); - break; - case DOWNLOAD: - s2 = synchronizer.download(copyDB, activityItem); - break; - default: - s2 = Synchronizer.Status.INCORRECT_USAGE; - } - // See doUpload() for motivation - if (s2 == Synchronizer.Status.NEED_REFRESH) { - s2 = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); - if (s2 == Synchronizer.Status.OK) { - switch (mode) { - case UPLOAD: - s2 = synchronizer.upload(copyDB, activityItem.getId()); - break; - case DOWNLOAD: - s2 = synchronizer.download(copyDB, activityItem); - break; - default: - s2 = Synchronizer.Status.INCORRECT_USAGE; + copySpinner.setMessage((1 + syncActivitiesList.size()) + " remaining"); + + executor.execute( + () -> { + Status result; + try { + switch (mode) { + case UPLOAD: + result = synchronizer.upload(copyDB, activityItem.getId()); + break; + case DOWNLOAD: + result = synchronizer.download(copyDB, activityItem); + break; + default: + result = Synchronizer.Status.INCORRECT_USAGE; + } + + // See doUpload() for motivation + if (result == Synchronizer.Status.NEED_REFRESH) { + result = handleRefreshComplete(synchronizer, synchronizer.refreshToken()); + if (result == Synchronizer.Status.OK) { + switch (mode) { + case UPLOAD: + result = synchronizer.upload(copyDB, activityItem.getId()); + break; + case DOWNLOAD: + result = synchronizer.download(copyDB, activityItem); + break; + default: + result = Synchronizer.Status.INCORRECT_USAGE; + } } } + } catch (Exception ex) { + Log.e(getClass().getName(), "Sync (multi) failed", ex); + result = Synchronizer.Status.ERROR; } - return s2; - } catch (Exception ex) { - ex.printStackTrace(); - return Synchronizer.Status.ERROR; - } - } - @Override - protected void onPostExecute(Synchronizer.Status result) { - switch (result) { - case OK: - syncOK(synchronizer, copySpinner, copyDB, result); - syncNextActivity(synchronizer, mode); - break; - - // TODO Handling of NEED_AUTH and CANCEL hangs the app - case NEED_AUTH: - handleAuth( - (synchronizerName, s2) -> { - if (s2 == Synchronizer.Status.OK) { - doSyncMulti(synchronizer, mode, activityItem); - } else { // Unexpected result, nothing to do + final Status finalResult = result; + + mainHandler.post( + () -> { + switch (finalResult) { + case OK: + syncOK(synchronizer, copySpinner, copyDB, finalResult); syncNextActivity(synchronizer, mode); - } - }, - synchronizer, - result.authMethod); - return; - - case CANCEL: - syncActivitiesList.clear(); - syncNextActivity(synchronizer, mode); - break; - - default: - syncNextActivity(synchronizer, mode); - break; - } - } - }.execute(synchronizer); + break; + case NEED_AUTH: + handleAuth( + (synchronizerName, s2) -> { + if (s2 == Synchronizer.Status.OK) { + doSyncMulti(synchronizer, mode, activityItem); + } else { + syncNextActivity(synchronizer, mode); + } + }, + synchronizer, + finalResult.authMethod); + return; + case CANCEL: + syncActivitiesList.clear(); + syncNextActivity(synchronizer, mode); + break; + default: + syncNextActivity(synchronizer, mode); + break; + } + }); + }); } public void loadLiveLoggers(List liveLoggers) { @@ -1218,4 +1182,25 @@ public void loadLiveLoggers(List liveLoggers) { Log.e(getClass().getName(), "Query for liveloggers failed:", ex); } } + + public enum SyncMode { + DOWNLOAD(org.runnerup.common.R.string.Downloading_from_1s), + UPLOAD(org.runnerup.common.R.string.Uploading_to_1s); + + final int textId; + + SyncMode(int textId) { + this.textId = textId; + } + + public int getTextId() { + return textId; + } + } + + public interface Callback { + void run(String synchronizerName, Synchronizer.Status status); + } + + public record WorkoutRef(String synchronizer, String workoutKey, String workoutName) {} } diff --git a/app/src/main/org/runnerup/util/GraphWrapper.java b/app/src/main/org/runnerup/util/GraphWrapper.java index 429e013c8..dbb1db37d 100644 --- a/app/src/main/org/runnerup/util/GraphWrapper.java +++ b/app/src/main/org/runnerup/util/GraphWrapper.java @@ -22,7 +22,8 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.LinearLayout; @@ -36,6 +37,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.db.entities.LocationEntity; @@ -52,14 +55,11 @@ public class GraphWrapper implements Constants { private final LinearLayout hrzonesBarLayout; private final LoadParam loadParam; - interface XAxis { - String label(); - String formatValue(double val); - double getX(double distance, double time_ms); - }; - + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final Handler handler = new Handler(Looper.getMainLooper()); private final XAxis distanceXAxis; private final XAxis timeXAxis; + boolean firstLoad = true; boolean useDistanceAsX = true; private XAxis xAxis; @@ -77,37 +77,41 @@ public GraphWrapper( this.hrzonesBarLayout = hrzonesBarLayout; this.formatter = formatter; - this.distanceXAxis = new XAxis() { - @Override - public String label() { - return context.getString(org.runnerup.common.R.string.Distance); - } - @Override - public String formatValue(double value) { - return formatter.formatDistance(Formatter.Format.TXT_SHORT, - (long)value); - } - @Override - public double getX(double distance, double time_ms) { - return distance; - } - }; + this.distanceXAxis = + new XAxis() { + @Override + public String label() { + return context.getString(org.runnerup.common.R.string.Distance); + } - this.timeXAxis = new XAxis() { - @Override - public String label() { - return context.getString(org.runnerup.common.R.string.Time); - } - @Override - public String formatValue(double value) { - return formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, - (long)value); - } - @Override - public double getX(double distance, double time_ms) { - return time_ms / 1000; - } - }; + @Override + public String formatValue(double value) { + return formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) value); + } + + @Override + public double getX(double distance, double time_ms) { + return distance; + } + }; + + this.timeXAxis = + new XAxis() { + @Override + public String label() { + return context.getString(org.runnerup.common.R.string.Time); + } + + @Override + public String formatValue(double value) { + return formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, (long) value); + } + + @Override + public double getX(double distance, double time_ms) { + return time_ms / 1000; + } + }; this.useDistanceAsX = use_distance_as_x; if (use_distance_as_x) { @@ -134,9 +138,7 @@ public String formatLabel(double value, boolean isValueX) { } }); graphView.getGridLabelRenderer().setVerticalAxisTitle(formatter.getVelocityUnit(context)); - graphView - .getGridLabelRenderer() - .setHorizontalAxisTitle(xAxis.label()); + graphView.getGridLabelRenderer().setHorizontalAxisTitle(xAxis.label()); // enable zoom graphView.getViewport().setScalable(true); graphView.getViewport().setScrollable(true); @@ -144,9 +146,7 @@ public String formatLabel(double value, boolean isValueX) { graphView2 = new GraphView(context); graphView2.setTitle(context.getString(org.runnerup.common.R.string.Heart_rate)); graphView2.getGridLabelRenderer().setVerticalAxisTitle("bpm"); - graphView2 - .getGridLabelRenderer() - .setHorizontalAxisTitle(xAxis.label()); + graphView2.getGridLabelRenderer().setHorizontalAxisTitle(xAxis.label()); graphView2 .getGridLabelRenderer() .setLabelFormatter( @@ -164,7 +164,7 @@ public String formatLabel(double value, boolean isValueX) { graphView2.getViewport().setScrollable(true); hrzonesBar = new HRZonesBar(context); - new LoadGraph().execute(loadParam); + loadGraph(); } public void setUseDistanceAsX(boolean val) { @@ -179,36 +179,131 @@ public void setUseDistanceAsX(boolean val) { } graphView.removeAllSeries(); graphView2.removeAllSeries(); - new LoadGraph().execute(loadParam); + loadGraph(); + } + + private void loadGraph() { + executor.execute( + () -> { + // Background work + GraphProducer producer = doLoadGraphInBackground(loadParam); + // Post result to UI thread + handler.post(() -> onPostExecute(producer)); + }); + } + + private GraphProducer doLoadGraphInBackground(LoadParam params) { + LocationEntity.LocationList ll = + new LocationEntity.LocationList<>(params.mDB, params.mID); + GraphProducer graphData = new GraphProducer(params.context, ll.getCount()); + double lastDistance = 0; + long lastTime = 0; + int lastLap = -1; + Double tot_distance = 0.0; + double tot_time = 0.0; + for (LocationEntity loc : ll) { + Long time = loc.getElapsed(); + time = time != null ? time : lastTime; + tot_time = time.doubleValue(); + Integer lap = loc.getLap(); + lap = lap != null ? lap : 0; + tot_distance = tot_distance != null ? loc.getDistance() : lastDistance; + + double tot_X = xAxis.getX(tot_distance, time); + if (lap != lastLap) { + graphData.clearSmooth(tot_X); + lastLap = lap; + } + + graphData.addObservation(time - lastTime, tot_distance - lastDistance, tot_X, loc); + lastTime = time; + lastDistance = tot_distance; + } + graphData.clearSmooth(xAxis.getX(tot_distance, tot_time)); + + ll.close(); + return graphData; + } + + @SuppressLint("ObsoleteSdkInt") + private void onPostExecute(GraphProducer graphData) { + if (graphData == null) return; + + graphData.complete(graphView); + graphTab.removeView(graphView); + graphTab.removeView(graphView2); + if (graphData.HasPaceInfo() && !graphData.HasHRInfo()) { + graphTab.addView(graphView); + } else if (!graphData.HasPaceInfo() && graphData.HasHRInfo()) { + graphTab.addView(graphView2); + } else if (graphData.HasPaceInfo() && graphData.HasHRInfo()) { + graphTab.addView(graphView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 0.5f)); + + graphTab.addView(graphView2, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 0.5f)); + } + + hrzonesBarLayout.removeView(hrzonesBar); + if (graphData.HasHRZHist()) { + hrzonesBarLayout.setVisibility(View.VISIBLE); + hrzonesBarLayout.addView(hrzonesBar); + } else { + hrzonesBarLayout.setVisibility(View.GONE); + } + if (!firstLoad) { + graphTab.invalidate(); + } else { + firstLoad = false; + } + } + + private double calculateAverageHr(int[] data) { + int sum = 0; + int no = 0; + + for (int aData : data) { + if (aData > 0) { + sum = sum + aData; + no++; + } + } + // TODO Average of pointe, not over time + if (no == 0) { + return 0; + } else { + return (double) sum / no; + } + } + + interface XAxis { + String label(); + + String formatValue(double val); + + double getX(double distance, double time_ms); } class GraphProducer { final int interval; - boolean first = true; - int pos = 0; final double[] time; final double[] distance; + final int[] hr; + final List velocityList; + final List hrList; + final HRZones hrCalc; + final SpeedUnit preferred_speedunit; + boolean first = true; + int pos = 0; double sum_time = 0; double sum_distance = 0; double acc_time = 0; - - final int[] hr; double[] hrzHist = null; - double tot_avg_hr = 0; - double avg_velocity = 0; double min_velocity = Double.MAX_VALUE; double max_velocity = Double.MIN_VALUE; - final List velocityList; - final List hrList; - boolean showPace = false; boolean showHR = false; boolean showHRZhist = false; - final HRZones hrCalc; - - final SpeedUnit preferred_speedunit; public GraphProducer(Context context, int noPoints) { final int GRAPH_INTERVAL_SECONDS = 5; // 1 point every 5 sec @@ -295,8 +390,9 @@ void addObservation( pos += 1; acc_time += delta_time; - if (pos >= this.time.length && (acc_time >= 1000 * interval) && - xAxis.getX(sum_distance, sum_time) > 0) { + if (pos >= this.time.length + && (acc_time >= 1000 * interval) + && xAxis.getX(sum_distance, sum_time) > 0) { emit(tot_X); } } @@ -337,124 +433,12 @@ void emit(double tot_X) { } } - class GraphFilter { - - final double[] data; - final List source; - - GraphFilter(List velocityList) { - source = velocityList; - data = new double[velocityList.size()]; - for (int i = 0; i < velocityList.size(); i++) data[i] = velocityList.get(i).getY(); - } - - void complete() { - for (int i = 0; i < source.size(); i++) - source.set(i, new DataPoint(source.get(i).getX(), data[i])); - } - - void init(double[] window, double val) { - for (int j = 0; j < window.length - 1; j++) window[j] = val; - } - - void shiftLeft(double[] window, double newVal) { - System.arraycopy(window, 1, window, 0, window.length - 1); - window[window.length - 1] = newVal; - } - - /** Perform in place moving average */ - void movingAvergage(int windowLen) { - double[] window = new double[windowLen]; - init(window, data[0]); - - final int mid = (window.length - 1) / 2; - final int last = window.length - 1; - for (int i = 0; i < data.length && i <= mid; i++) { - window[i + mid] = data[i]; - } - - double sum = 0; - for (double aWindow : window) sum += aWindow; - - for (int i = 0; i < data.length; i++) { - double newY = sum / windowLen; - data[i] = newY; - sum -= window[0]; - shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); - sum += window[last]; - } - } - - /** Perform in place moving average */ - void movingMedian(int windowLen) { - double[] window = new double[windowLen]; - init(window, data[0]); - - final int mid = (window.length - 1) / 2; - for (int i = 0; i < data.length && i <= mid; i++) { - window[i + mid] = data[i]; - } - - double[] sort = new double[windowLen]; - for (int i = 0; i < data.length; i++) { - System.arraycopy(window, 0, sort, 0, windowLen); - Arrays.sort(sort); - data[i] = sort[mid]; - shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); - } - } - - /** Perform in place SavitzkyGolay windowLen = 5 */ - void SavitzkyGolay5() { - final int len = 5; - double[] window = new double[len]; - init(window, data[0]); - - final int mid = (window.length - 1) / 2; - for (int i = 0; i < data.length && i <= mid; i++) { - window[i + mid] = data[i]; - } - for (int i = 0; i < data.length; i++) { - double newY = - (-3 * window[0] + 12 * window[1] + 17 * window[2] + 12 * window[3] - 3 * window[4]) - / 35; - data[i] = newY; - shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); - } - } - - /** Perform in place SavitzkyGolay windowLen = 7 */ - void SavitzkyGolay7() { - final int len = 7; - double[] window = new double[len]; - init(window, data[0]); - - final int mid = (window.length - 1) / 2; - for (int i = 0; i < data.length && i <= mid; i++) { - window[i + mid] = data[i]; - } - for (int i = 0; i < data.length; i++) { - double newY = - (-2 * window[0] - + 3 * window[1] - + 6 * window[2] - + 7 * window[3] - + 6 * window[4] - + 3 * window[5] - - 2 * window[6]) - / 21; - data[i] = newY; - shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); - } - } - - void KolmogorovZurbenko(int n, int len) { - for (int i = 0; i < n; i++) movingAvergage(len); - } - } - public void complete(final GraphView graphView) { - avg_velocity /= velocityList.size(); + if (velocityList.isEmpty()) { + avg_velocity = 0; + } else { + avg_velocity /= velocityList.size(); + } Log.d(getClass().getName(), "graph: " + velocityList.size() + " points"); boolean smoothData = @@ -478,7 +462,8 @@ public void complete(final GraphView graphView) { .getString(R.string.pref_pace_graph_smoothing_filters), defaultFilterList); final String[] filters = filterList.split(";"); - StringBuilder s = new StringBuilder("Applying filters(" + filters.length + ", >" + filterList + "<):"); + StringBuilder s = + new StringBuilder("Applying filters(" + filters.length + ", >" + filterList + "<):"); for (String filter : filters) { int[] args = getArgs(filter); if (filter.startsWith("mm")) { @@ -552,9 +537,11 @@ public void complete(final GraphView graphView) { for (double aHrzHist : hrzHist) { sum += aHrzHist; } - for (int i = 0; i < hrzHist.length; i++) { - hrzHist[i] = hrzHist[i] / sum; - s.append(" ").append(hrzHist[i]); + if (sum > 0) { + for (int i = 0; i < hrzHist.length; i++) { + hrzHist[i] = hrzHist[i] / sum; + s.append(" ").append(hrzHist[i]); + } } Log.d(getClass().getName(), s.toString()); hrzonesBar.pushHrzData(hrzHist); @@ -589,114 +576,123 @@ public boolean HasHRInfo() { public boolean HasHRZHist() { return showHR && showHRZhist; } - } - private double calculateAverageHr(int[] data) { - int sum = 0; - int no = 0; + class GraphFilter { - for (int aData : data) { - if (aData > 0) { - sum = sum + aData; - no++; + final double[] data; + final List source; + + GraphFilter(List velocityList) { + source = velocityList; + data = new double[velocityList.size()]; + for (int i = 0; i < velocityList.size(); i++) data[i] = velocityList.get(i).getY(); } - } - // TODO Average of pointe, not over time - if (no == 0) { - return 0; - } else { - return (double) sum / no; - } - } - class LoadParam { - public LoadParam(Context context, SQLiteDatabase mDB, long mID) { - this.context = context; - this.mDB = mDB; - this.mID = mID; - } + void complete() { + for (int i = 0; i < source.size(); i++) + source.set(i, new DataPoint(source.get(i).getX(), data[i])); + } - final Context context; - final SQLiteDatabase mDB; - final long mID; - } + void init(double[] window, double val) { + for (int j = 0; j < window.length - 1; j++) window[j] = val; + } - boolean firstLoad = true; + void shiftLeft(double[] window, double newVal) { + System.arraycopy(window, 1, window, 0, window.length - 1); + window[window.length - 1] = newVal; + } + + /** Perform in place moving average */ + void movingAvergage(int windowLen) { + double[] window = new double[windowLen]; + init(window, data[0]); - @SuppressLint("StaticFieldLeak") - private class LoadGraph extends AsyncTask { - @Override - protected GraphProducer doInBackground(LoadParam... params) { - - LocationEntity.LocationList ll = - new LocationEntity.LocationList<>(params[0].mDB, params[0].mID); - GraphProducer graphData = new GraphProducer(params[0].context, ll.getCount()); - double lastDistance = 0; - long lastTime = 0; - int lastLap = -1; - Double tot_distance = 0.0; - Double tot_time = 0.0; - int cnt = 0; - for (LocationEntity loc : ll) { - cnt++; - Long time = loc.getElapsed(); - time = time != null ? time : lastTime; - if (time != null) { - tot_time = time.doubleValue(); + final int mid = (window.length - 1) / 2; + final int last = window.length - 1; + for (int i = 0; i < data.length && i <= mid; i++) { + window[i + mid] = data[i]; } - Integer lap = loc.getLap(); - lap = lap != null ? lap : 0; - tot_distance = tot_distance != null ? loc.getDistance() : lastDistance; - - double tot_X = xAxis.getX(tot_distance, time); - if (lap != lastLap) { - graphData.clearSmooth(tot_X); - lastLap = lap; + + double sum = 0; + for (double aWindow : window) sum += aWindow; + + for (int i = 0; i < data.length; i++) { + double newY = sum / windowLen; + data[i] = newY; + sum -= window[0]; + shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); + sum += window[last]; } + } - graphData.addObservation(time - lastTime, tot_distance - lastDistance, - tot_X, loc); - lastTime = time; - lastDistance = tot_distance; + /** Perform in place moving average */ + void movingMedian(int windowLen) { + double[] window = new double[windowLen]; + init(window, data[0]); + + final int mid = (window.length - 1) / 2; + for (int i = 0; i < data.length && i <= mid; i++) { + window[i + mid] = data[i]; + } + + double[] sort = new double[windowLen]; + for (int i = 0; i < data.length; i++) { + System.arraycopy(window, 0, sort, 0, windowLen); + Arrays.sort(sort); + data[i] = sort[mid]; + shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); + } } - graphData.clearSmooth(xAxis.getX(tot_distance, tot_time)); - ll.close(); - // Log.e(getClass().getName(), "Finished loading " + cnt + " points" - // + " => " + graphData.velocityList.size() + " points"); - return graphData; - } + /** Perform in place SavitzkyGolay windowLen = 5 */ + void SavitzkyGolay5() { + final int len = 5; + double[] window = new double[len]; + init(window, data[0]); - @SuppressLint("ObsoleteSdkInt") - @Override - protected void onPostExecute(GraphProducer graphData) { - graphData.complete(graphView); - graphTab.removeView(graphView); - graphTab.removeView(graphView2); - if (graphData.HasPaceInfo() && !graphData.HasHRInfo()) { - graphTab.addView(graphView); - } else if (!graphData.HasPaceInfo() && graphData.HasHRInfo()) { - graphTab.addView(graphView2); - } else if (graphData.HasPaceInfo() && graphData.HasHRInfo()) { - graphTab.addView(graphView, - new LayoutParams(LayoutParams.MATCH_PARENT, 0, 0.5f)); - - graphTab.addView(graphView2, - new LayoutParams(LayoutParams.MATCH_PARENT, 0, 0.5f)); + final int mid = (window.length - 1) / 2; + for (int i = 0; i < data.length && i <= mid; i++) { + window[i + mid] = data[i]; + } + for (int i = 0; i < data.length; i++) { + double newY = + (-3 * window[0] + 12 * window[1] + 17 * window[2] + 12 * window[3] - 3 * window[4]) + / 35; + data[i] = newY; + shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); + } } - hrzonesBarLayout.removeView(hrzonesBar); - if (graphData.HasHRZHist()) { - hrzonesBarLayout.setVisibility(View.VISIBLE); - hrzonesBarLayout.addView(hrzonesBar); - } else { - hrzonesBarLayout.setVisibility(View.GONE); + /** Perform in place SavitzkyGolay windowLen = 7 */ + void SavitzkyGolay7() { + final int len = 7; + double[] window = new double[len]; + init(window, data[0]); + + final int mid = (window.length - 1) / 2; + for (int i = 0; i < data.length && i <= mid; i++) { + window[i + mid] = data[i]; + } + for (int i = 0; i < data.length; i++) { + double newY = + (-2 * window[0] + + 3 * window[1] + + 6 * window[2] + + 7 * window[3] + + 6 * window[4] + + 3 * window[5] + - 2 * window[6]) + / 21; + data[i] = newY; + shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_velocity); + } } - if (!firstLoad) { - graphTab.invalidate(); - } else { - firstLoad = false; + + void KolmogorovZurbenko(int n, int len) { + for (int i = 0; i < n; i++) movingAvergage(len); } } } + + record LoadParam(Context context, SQLiteDatabase mDB, long mID) {} } diff --git a/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java b/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java index d511e4638..41693ab82 100644 --- a/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java +++ b/app/src/main/org/runnerup/view/ManageWorkoutsActivity.java @@ -281,7 +281,7 @@ private void handleButtons() { } WorkoutRef selected = (WorkoutRef) currentlySelectedWorkout.getTag(); - if (PHONE_STRING.contentEquals(selected.synchronizer)) { + if (PHONE_STRING.contentEquals(selected.synchronizer())) { deleteButton.setEnabled(true); shareButton.setEnabled(true); editButton.setEnabled(true); @@ -413,7 +413,7 @@ private ArrayList filter( final WorkoutRef selected = (WorkoutRef) currentlySelectedWorkout.getTag(); new AlertDialog.Builder(ManageWorkoutsActivity.this) .setTitle( - getString(org.runnerup.common.R.string.Delete_workout) + " " + selected.workoutName) + getString(org.runnerup.common.R.string.Delete_workout) + " " + selected.workoutName()) .setMessage(org.runnerup.common.R.string.Are_you_sure) .setPositiveButton( org.runnerup.common.R.string.Yes, @@ -429,11 +429,11 @@ private ArrayList filter( }; private void deleteWorkout(WorkoutRef selected) { - File f = WorkoutSerializer.getFile(this, selected.workoutName); + File f = WorkoutSerializer.getFile(this, selected.workoutName()); //noinspection ResultOfMethodCallIgnored f.delete(); SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); - if (selected.workoutName.contentEquals( + if (selected.workoutName().contentEquals( pref.getString(getResources().getString(R.string.pref_advanced_workout), ""))) { pref.edit().putString(getResources().getString(R.string.pref_advanced_workout), "").apply(); } @@ -460,7 +460,7 @@ private void deleteWorkout(WorkoutRef selected) { final AppCompatActivity context = ManageWorkoutsActivity.this; final WorkoutRef selected = (WorkoutRef) currentlySelectedWorkout.getTag(); - final String name = selected.workoutName; + final String name = selected.workoutName(); final Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra( @@ -484,7 +484,7 @@ private void deleteWorkout(WorkoutRef selected) { final WorkoutRef selected = (WorkoutRef) currentlySelectedWorkout.getTag(); final Intent intent = new Intent(ManageWorkoutsActivity.this, CreateAdvancedWorkout.class); - intent.putExtra(WORKOUT_NAME, selected.workoutName); + intent.putExtra(WORKOUT_NAME, selected.workoutName()); intent.putExtra(WORKOUT_EXISTS, true); startActivity(intent); }; @@ -537,7 +537,7 @@ public View getChildView( cb.setChecked( currentlySelectedWorkout != null && currentlySelectedWorkout.getTag() == workout); cb.setOnCheckedChangeListener(onWorkoutChecked); - cb.setText(workout.workoutName); + cb.setText(workout.workoutName()); return view; } @@ -664,7 +664,7 @@ public void onGroupCollapsed(int groupPosition) { String provider = getProvider(groupPosition); if (currentlySelectedWorkout != null) { WorkoutRef ref = (WorkoutRef) currentlySelectedWorkout.getTag(); - if (ref.synchronizer.contentEquals(provider)) { + if (ref.synchronizer().contentEquals(provider)) { currentlySelectedWorkout.setChecked(false); currentlySelectedWorkout = null; } From 49ee36f84e436e6e62ae6485cf2fc1249416f754 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Mon, 5 Jan 2026 19:49:44 +0100 Subject: [PATCH 15/31] fix: remove obsolete lint supressions Many no longer needed with minsdk >= 21 --- app/lint.xml | 42 ++++--------------- app/res/values-v21/fonts.xml | 4 -- app/res/values/fonts.xml | 2 +- .../runnerup/db/entities/AbstractEntity.java | 3 -- .../main/org/runnerup/export/SyncManager.java | 13 ++---- .../tracker/component/TrackerElevation.java | 4 +- .../tracker/component/TrackerTemperature.java | 2 - .../main/org/runnerup/util/Encryption.java | 2 - .../main/org/runnerup/util/GraphWrapper.java | 3 -- .../org/runnerup/view/DetailActivity.java | 2 - .../org/runnerup/view/UploadActivity.java | 2 - 11 files changed, 14 insertions(+), 65 deletions(-) delete mode 100644 app/res/values-v21/fonts.xml diff --git a/app/lint.xml b/app/lint.xml index 743715ebc..6b691efe0 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -1,29 +1,21 @@ + + + - - - - + + + - - - - - - - - - - @@ -35,12 +27,6 @@ - - - - - - @@ -48,24 +34,12 @@ - - - - - - - - + - - - - - - + diff --git a/app/res/values-v21/fonts.xml b/app/res/values-v21/fonts.xml deleted file mode 100644 index 916ec93d7..000000000 --- a/app/res/values-v21/fonts.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/res/values/fonts.xml b/app/res/values/fonts.xml index 70102359c..e3fd033ea 100644 --- a/app/res/values/fonts.xml +++ b/app/res/values/fonts.xml @@ -1,4 +1,4 @@ - sans-serif + sans-serif-medium diff --git a/app/src/main/org/runnerup/db/entities/AbstractEntity.java b/app/src/main/org/runnerup/db/entities/AbstractEntity.java index daec0e4e6..74158ccfa 100644 --- a/app/src/main/org/runnerup/db/entities/AbstractEntity.java +++ b/app/src/main/org/runnerup/db/entities/AbstractEntity.java @@ -17,7 +17,6 @@ package org.runnerup.db.entities; -import android.annotation.SuppressLint; import android.content.ContentValues; import android.database.Cursor; import android.database.CursorIndexOutOfBoundsException; @@ -73,7 +72,6 @@ public void update(SQLiteDatabase db) { } } - @SuppressLint("ObsoleteSdkInt") void toContentValues(Cursor c) { if (c.isClosed() || c.isAfterLast() || c.isBeforeFirst()) { throw new CursorIndexOutOfBoundsException("Cursor not readable"); @@ -94,7 +92,6 @@ void toContentValues(Cursor c) { // This is a replacement for DatabaseUtils.cursorRowToContentValues // see https://code.google.com/p/android/issues/detail?id=22219 - @SuppressLint("NewApi") private static void cursorRowToContentValues(Cursor cursor, ContentValues values) { String[] columns = cursor.getColumnNames(); int length = columns.length; diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index 235cc2321..6c2cce6b1 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -554,15 +554,10 @@ private void askFileUrl(final Synchronizer sync) { private boolean checkStoragePermissions(final AppCompatActivity activity) { boolean result = true; String[] requiredPerms; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - //noinspection InlinedApi - requiredPerms = - new String[] { - Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - }; - } else { - requiredPerms = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}; - } + requiredPerms = + new String[] { + Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE + }; List defaultPerms = new ArrayList<>(); for (final String perm : requiredPerms) { if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) { diff --git a/app/src/main/org/runnerup/tracker/component/TrackerElevation.java b/app/src/main/org/runnerup/tracker/component/TrackerElevation.java index 51b3e7a5c..234e5c00c 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerElevation.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerElevation.java @@ -17,7 +17,6 @@ package org.runnerup.tracker.component; -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.hardware.Sensor; @@ -77,11 +76,10 @@ Double getOffset(Tracker tracker) { } } - @SuppressLint("NewApi") public Double getValue() { Double val; Float pressure = tracker.getCurrentPressure(); - if (pressure != null && BuildConfig.VERSION_CODE >= 9) { + if (pressure != null) { // Pressure available - use it for elevation // TODO get real sea level pressure (online) or set offset from start/end //noinspection InlinedApi diff --git a/app/src/main/org/runnerup/tracker/component/TrackerTemperature.java b/app/src/main/org/runnerup/tracker/component/TrackerTemperature.java index ffcff80b4..0adc16313 100644 --- a/app/src/main/org/runnerup/tracker/component/TrackerTemperature.java +++ b/app/src/main/org/runnerup/tracker/component/TrackerTemperature.java @@ -17,7 +17,6 @@ package org.runnerup.tracker.component; -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.hardware.Sensor; @@ -73,7 +72,6 @@ public static boolean isAvailable(final Context context) { return ((new TrackerTemperature()).getSensor(context) != null) || isMockSensor; } - @SuppressLint("ObsoleteSdkInt") private Sensor getSensor(final Context context) { Sensor sensor; if (sensorManager == null) { diff --git a/app/src/main/org/runnerup/util/Encryption.java b/app/src/main/org/runnerup/util/Encryption.java index d5c634873..82f6180cf 100644 --- a/app/src/main/org/runnerup/util/Encryption.java +++ b/app/src/main/org/runnerup/util/Encryption.java @@ -17,7 +17,6 @@ package org.runnerup.util; -import android.annotation.SuppressLint; import android.util.Base64; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -76,7 +75,6 @@ public static String calculateRFC2104HMAC(final String data, final String key) * @param key * @throws Exception */ - @SuppressLint("TrulyRandom") private static void encrypt(final InputStream in, final OutputStream out, final String key) throws Exception { final PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray()); diff --git a/app/src/main/org/runnerup/util/GraphWrapper.java b/app/src/main/org/runnerup/util/GraphWrapper.java index dbb1db37d..0aa5c29a9 100644 --- a/app/src/main/org/runnerup/util/GraphWrapper.java +++ b/app/src/main/org/runnerup/util/GraphWrapper.java @@ -17,7 +17,6 @@ package org.runnerup.util; -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; @@ -64,7 +63,6 @@ public class GraphWrapper implements Constants { private XAxis xAxis; /** Called when the activity is first created. */ - @SuppressLint("ObsoleteSdkInt") public GraphWrapper( Context context, LinearLayout graphTab, @@ -225,7 +223,6 @@ private GraphProducer doLoadGraphInBackground(LoadParam params) { return graphData; } - @SuppressLint("ObsoleteSdkInt") private void onPostExecute(GraphProducer graphData) { if (graphData == null) return; diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index cb04888c0..a2f86abf1 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -20,7 +20,6 @@ import static org.runnerup.content.ActivityProvider.GPX_MIME; import static org.runnerup.content.ActivityProvider.TCX_MIME; -import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -131,7 +130,6 @@ public class DetailActivity extends AppCompatActivity implements Constants { private static final int EDIT_ACCOUNT_REQUEST = 2; /** Called when the activity is first created. */ - @SuppressLint("ObsoleteSdkInt") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/org/runnerup/view/UploadActivity.java b/app/src/main/org/runnerup/view/UploadActivity.java index 6142a0fd6..24222d783 100644 --- a/app/src/main/org/runnerup/view/UploadActivity.java +++ b/app/src/main/org/runnerup/view/UploadActivity.java @@ -17,7 +17,6 @@ package org.runnerup.view; -import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -328,7 +327,6 @@ private class ViewHolderUploadActivity { private long activityID; } - @SuppressLint("ObsoleteSdkInt") @Override public View getView(int arg0, View convertView, ViewGroup parent) { View view = convertView; From f1e8a3637c154175f69e505d0b04d186a177c669 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Mon, 5 Jan 2026 19:51:09 +0100 Subject: [PATCH 16/31] fix: remove RtlHardcoded overrides no longer needed since sdk 21 > 17 right/left attributes can be removed --- app/lint.xml | 2 -- app/res/layout/account_list.xml | 1 - app/res/layout/account_row.xml | 2 -- app/res/layout/actionbar_dropdown_spinner.xml | 2 +- app/res/layout/actionbar_spinner.xml | 2 +- app/res/layout/detail.xml | 3 +-- app/res/layout/filepermission.xml | 1 - app/res/layout/history_row.xml | 1 - app/res/layout/hr_settings.xml | 9 ++----- app/res/layout/manage_workouts.xml | 2 -- app/res/layout/manual.xml | 6 ----- app/res/layout/run.xml | 27 +++++-------------- app/res/layout/settings_wrapper.xml | 2 -- app/res/layout/start.xml | 19 +++---------- app/res/layout/start_advanced.xml | 9 +++---- app/res/layout/step_button.xml | 6 ----- app/res/layout/title_spinner.xml | 4 --- app/res/layout/upload.xml | 3 --- app/res/layout/upload_row.xml | 12 ++------- app/res/layout/userpass.xml | 5 +--- 20 files changed, 21 insertions(+), 97 deletions(-) diff --git a/app/lint.xml b/app/lint.xml index 6b691efe0..0f23394ae 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -37,8 +37,6 @@ - - diff --git a/app/res/layout/account_list.xml b/app/res/layout/account_list.xml index cda247c1b..ad27c4b41 100644 --- a/app/res/layout/account_list.xml +++ b/app/res/layout/account_list.xml @@ -26,7 +26,6 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_alignParentTop="true" > diff --git a/app/res/layout/account_row.xml b/app/res/layout/account_row.xml index 2c7654aaf..fba2b8c55 100644 --- a/app/res/layout/account_row.xml +++ b/app/res/layout/account_row.xml @@ -65,7 +65,6 @@ style="@style/AccountListText" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginLeft="16dp" android:layout_marginStart="16dp" android:layout_weight="1" android:minWidth="48dp" @@ -75,7 +74,6 @@ android:id="@+id/account_row_upload" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_marginLeft="16dp" android:layout_marginStart="16dp" android:minWidth="48dp" android:minHeight="48dp" diff --git a/app/res/layout/actionbar_dropdown_spinner.xml b/app/res/layout/actionbar_dropdown_spinner.xml index 3fd319e3b..754c50b77 100644 --- a/app/res/layout/actionbar_dropdown_spinner.xml +++ b/app/res/layout/actionbar_dropdown_spinner.xml @@ -4,4 +4,4 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" - android:gravity="left" /> \ No newline at end of file + android:gravity="start" /> \ No newline at end of file diff --git a/app/res/layout/actionbar_spinner.xml b/app/res/layout/actionbar_spinner.xml index cd5cd040b..20fa7d330 100644 --- a/app/res/layout/actionbar_spinner.xml +++ b/app/res/layout/actionbar_spinner.xml @@ -3,4 +3,4 @@ style="@style/TextAppearance.AppCompat.Title" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="left" /> \ No newline at end of file + android:gravity="start" /> \ No newline at end of file diff --git a/app/res/layout/detail.xml b/app/res/layout/detail.xml index 3a13046a9..4f887ed77 100644 --- a/app/res/layout/detail.xml +++ b/app/res/layout/detail.xml @@ -140,7 +140,7 @@ android:id="@+id/notes_text" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="top|left" + android:gravity="top|start" android:hint="@string/Notes_about_your_workout" android:inputType="textCapSentences|textMultiLine" android:minLines="4" @@ -229,7 +229,6 @@ android:background="@drawable/btn_blue" android:drawablePadding="-32dp" android:drawableEnd="@drawable/ic_av_play_arrow" - android:drawableRight="@drawable/ic_av_play_arrow" android:text="@string/Resume" />