From ca6d08280aa93c6f5773a8de9647076c5e3023df Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 28 Jul 2020 11:32:52 +0200 Subject: [PATCH 01/11] SDK Build Tools 30.0.1 --- .travis.yml | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e71115664..62a1804d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ env: global: - ANDROID_API=28 - - ANDROID_BUILD_TOOLS=29.0.3 + - ANDROID_BUILD_TOOLS=30.0.1 - ADB_INSTALL_TIMEOUT=5 language: android jdk: diff --git a/build.gradle b/build.gradle index 6097471e5..63c66bcdb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ project.ext { //Common settings for all builds //Note that Android Studio does not know about the 'ext' module and will warn //minSdkVersion differs between modules - buildToolsVersion = '29.0.3' //Update Travis manually + buildToolsVersion = '30.0.1' //Update Travis manually compileSdkVersion = 28 //Update Travis manually targetSdkVersion = 28 From 12c645f457c73819abda0082cdb8de8e4198e337 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Thu, 30 Jul 2020 00:49:11 +0200 Subject: [PATCH 02/11] Disable build arch specific apk --- app/build.gradle | 3 ++- build.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5e190850e..a288120ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,7 +54,8 @@ android { splits { abi { - enable rootProject.ext.allowNonFree && gradle.startParameter.taskNames.contains("assembleLatestRelease") + // Disable, app bundles used in play + // 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 diff --git a/build.gradle b/build.gradle index 63c66bcdb..c418b333e 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ project.ext { //The Git tag for the release must be identical for F-Droid versionName = '2.1.0.1' versionCode = 241 - latestBaseVersionCode = 14000000 + latestBaseVersionCode = 15000000 travisBuild = System.getenv("TRAVIS") == "true" // allows for -Dpre-dex=false to be set From e75bb4ce0eda053f609dc95224e0114a96d3c978 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Wed, 29 Jul 2020 13:35:21 +0200 Subject: [PATCH 03/11] GPS accuracy: Also use number of sats to determine poor accuracy DEBUG only: show all accuracy values --- .../org/runnerup/tracker/GpsInformation.java | 2 +- .../main/org/runnerup/view/StartActivity.java | 44 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/src/main/org/runnerup/tracker/GpsInformation.java b/app/src/main/org/runnerup/tracker/GpsInformation.java index 8c7595d75..715c275f3 100644 --- a/app/src/main/org/runnerup/tracker/GpsInformation.java +++ b/app/src/main/org/runnerup/tracker/GpsInformation.java @@ -1,7 +1,7 @@ package org.runnerup.tracker; public interface GpsInformation { - String getGpsAccuracy(); + float getGpsAccuracy(); int getSatellitesAvailable(); diff --git a/app/src/main/org/runnerup/view/StartActivity.java b/app/src/main/org/runnerup/view/StartActivity.java index 080e85eff..279b7a736 100644 --- a/app/src/main/org/runnerup/view/StartActivity.java +++ b/app/src/main/org/runnerup/view/StartActivity.java @@ -657,10 +657,10 @@ private void toggleStatusDetails() { updateView(); } - private GpsLevel getGpsLevel(double gpsAccuracyMeters) { - if (gpsAccuracyMeters <= 7) + private GpsLevel getGpsLevel(double gpsAccuracyMeters, int sats) { + if (gpsAccuracyMeters <= 7 && sats > 7) return GpsLevel.GOOD; - else if (gpsAccuracyMeters <= 15) + else if (gpsAccuracyMeters <= 15 && sats > 4) return GpsLevel.ACCEPTABLE; else return GpsLevel.POOR; } @@ -707,14 +707,7 @@ private void updateGPSView() { int satAvailCount = mGpsStatus.getSatellitesAvailable(); // gps accuracy - float accuracy = -1; - if (mTracker != null) { - Location l = mTracker.getLastKnownLocation(); - - if (l != null) { - accuracy = l.getAccuracy(); - } - } + float accuracy = getGpsAccuracy(); // gps details String gpsAccuracy = getGpsAccuracyString(accuracy); @@ -740,7 +733,7 @@ private void updateGPSView() { } gpsEnable.setVisibility(View.GONE); - switch (getGpsLevel(accuracy)) { + switch (getGpsLevel(accuracy, satFixedCount)) { case POOR: gpsIndicator.setImageResource(R.drawable.ic_gps_1); gpsDetailIndicator.setImageResource(R.drawable.ic_gps_1); @@ -826,28 +819,43 @@ private boolean updateWearOSView() { } @Override - public String getGpsAccuracy() { + public float getGpsAccuracy() { if (mTracker != null) { Location l = mTracker.getLastKnownLocation(); if (l != null) { - return getGpsAccuracyString(l.getAccuracy()); + return l.getAccuracy(); } } - return ""; + return -1; } public String getGpsAccuracyString(float accuracy) { + String res = ""; if (accuracy > 0) { String accString = formatter.formatElevation(Formatter.Format.TXT_SHORT, accuracy); if (mTracker.getCurrentElevation() != null) { - return String.format(Locale.getDefault(), getString(R.string.GPS_accuracy_elevation), + res = String.format(Locale.getDefault(), getString(R.string.GPS_accuracy_elevation), accString, formatter.formatElevation(Formatter.Format.TXT_SHORT, mTracker.getCurrentElevation())); } else { - return String.format(Locale.getDefault(), getString(R.string.GPS_accuracy_no_elevation), + res = String.format(Locale.getDefault(), getString(R.string.GPS_accuracy_no_elevation), accString); } - } else return ""; + } + if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Extra info in debug builds + if (mTracker != null) { + Location l = mTracker.getLastKnownLocation(); + + if (l != null) { + res += " [" + + l.getVerticalAccuracyMeters() + " m, " + + l.getSpeedAccuracyMetersPerSecond() + " m/s, " + + l.getBearingAccuracyDegrees() + " deg]"; + } + } + } + return res; } private String getHRDetailString() { From d5dcad1c0926b3e31a9a99da30e3770da55419c2 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 28 Jul 2020 13:11:09 +0200 Subject: [PATCH 04/11] Refactor: Use VERSION_CODES instead of hardcoded SDK --- .../runnerup/export/RunnerUpLiveSynchronizer.java | 2 +- app/src/main/org/runnerup/export/SyncManager.java | 13 ++++++++----- .../org/runnerup/notification/GpsBoundState.java | 2 +- .../runnerup/notification/GpsSearchingState.java | 2 +- .../notification/NotificationStateManager.java | 2 +- .../org/runnerup/notification/OngoingState.java | 2 +- app/src/main/org/runnerup/view/AccountActivity.java | 2 +- app/src/main/org/runnerup/widget/WidgetUtil.java | 2 +- .../java/org/runnerup/service/ListenerService.java | 2 +- 9 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java b/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java index 20f0f0e36..2ed98a19b 100644 --- a/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java +++ b/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java @@ -194,7 +194,7 @@ public void workoutEvent(WorkoutInfo workoutInfo, int type) { .putExtra(LiveService.PARAM_IN_USERNAME, username) .putExtra(LiveService.PARAM_IN_PASSWORD, password) .putExtra(LiveService.PARAM_IN_SERVERADRESS, postUrl); - if (Build.VERSION.SDK_INT >= 28) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { context.startForegroundService(msgIntent); } else { context.startService(msgIntent); diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index 07e568197..cf4fa6c5e 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -483,10 +483,13 @@ private void askFileUrl(final Synchronizer sync) { final TextView tvAuthNotice = (TextView) view.findViewById(R.id.textViewAuthNotice); String path; - if (Build.VERSION.SDK_INT >= 19) { - //noinspection InlinedApi - path = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOCUMENTS).getPath(); + if (Build.VERSION.SDK_INT >= 29) { + // All paths are related to Environment.DIRECTORY_DOCUMENTS + path = ""; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + //noinspection InlinedApi + path = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOCUMENTS).getPath() + File.separator; } else { path = Environment.getExternalStorageDirectory().getPath(); } @@ -545,7 +548,7 @@ public boolean onKey(DialogInterface dialogInterface, int i, KeyEvent keyEvent) private boolean checkStoragePermissions(final AppCompatActivity activity) { boolean result = true; String[] requiredPerms; - if (Build.VERSION.SDK_INT >= 16) { + 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 { diff --git a/app/src/main/org/runnerup/notification/GpsBoundState.java b/app/src/main/org/runnerup/notification/GpsBoundState.java index fca8b8b90..884f23612 100644 --- a/app/src/main/org/runnerup/notification/GpsBoundState.java +++ b/app/src/main/org/runnerup/notification/GpsBoundState.java @@ -37,7 +37,7 @@ public GpsBoundState(Context context) { .setLocalOnly(true) .addAction(R.drawable.ic_av_play_arrow, context.getString(R.string.Start), pendingStart); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_SERVICE); } diff --git a/app/src/main/org/runnerup/notification/GpsSearchingState.java b/app/src/main/org/runnerup/notification/GpsSearchingState.java index 5827802b0..acba1a940 100644 --- a/app/src/main/org/runnerup/notification/GpsSearchingState.java +++ b/app/src/main/org/runnerup/notification/GpsSearchingState.java @@ -36,7 +36,7 @@ public GpsSearchingState(Context context, GpsInformation gpsInformation) { .setSmallIcon(R.drawable.ic_stat_notify) .setOnlyAlertOnce(true) .setLocalOnly(true); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_SERVICE); } diff --git a/app/src/main/org/runnerup/notification/NotificationStateManager.java b/app/src/main/org/runnerup/notification/NotificationStateManager.java index 50ab75758..f7898bbd0 100644 --- a/app/src/main/org/runnerup/notification/NotificationStateManager.java +++ b/app/src/main/org/runnerup/notification/NotificationStateManager.java @@ -20,7 +20,7 @@ public class NotificationStateManager { * @return */ public static String getChannelId(Context context) { - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (mChannel == null) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/org/runnerup/notification/OngoingState.java b/app/src/main/org/runnerup/notification/OngoingState.java index 4c2c1b11e..9743e1bd0 100644 --- a/app/src/main/org/runnerup/notification/OngoingState.java +++ b/app/src/main/org/runnerup/notification/OngoingState.java @@ -55,7 +55,7 @@ public OngoingState(Formatter formatter, WorkoutInfo workoutInfo, Context contex .setPriority(NotificationCompat.PRIORITY_MAX) .addAction(R.drawable.ic_av_newlap, context.getString(R.string.Lap), pendingLap) .addAction(R.drawable.ic_av_pause, context.getString(R.string.Pause), pendingPause); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_SERVICE); } diff --git a/app/src/main/org/runnerup/view/AccountActivity.java b/app/src/main/org/runnerup/view/AccountActivity.java index 4c9f34085..ee4690a19 100644 --- a/app/src/main/org/runnerup/view/AccountActivity.java +++ b/app/src/main/org/runnerup/view/AccountActivity.java @@ -166,7 +166,7 @@ private void fillData() { tv.setTag(synchronizer.getPublicUrl()); // FileSynchronizer: SDK 24 requires the file URI to be handled as FileProvider // Something like OI File Manager is needed too - if (Build.VERSION.SDK_INT < 24 || !synchronizer.getName().equals(FileSynchronizer.NAME)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !synchronizer.getName().equals(FileSynchronizer.NAME)) { tv.setOnClickListener(urlButtonClick); } } diff --git a/app/src/main/org/runnerup/widget/WidgetUtil.java b/app/src/main/org/runnerup/widget/WidgetUtil.java index 3b88aa80a..1a5540b3a 100644 --- a/app/src/main/org/runnerup/widget/WidgetUtil.java +++ b/app/src/main/org/runnerup/widget/WidgetUtil.java @@ -60,7 +60,7 @@ public static View createHoloTabIndicator(Context ctx, String title) { @SuppressWarnings("deprecation") public static void setBackground(View v, Drawable d) { - if (Build.VERSION.SDK_INT < 16) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { v.setBackgroundDrawable(d); } else { v.setBackground(d); diff --git a/wear/src/main/java/org/runnerup/service/ListenerService.java b/wear/src/main/java/org/runnerup/service/ListenerService.java index 95f4817e0..7bea4e943 100644 --- a/wear/src/main/java/org/runnerup/service/ListenerService.java +++ b/wear/src/main/java/org/runnerup/service/ListenerService.java @@ -83,7 +83,7 @@ private void handleNotification(DataEvent ev) { * @return */ public static String getChannelId(Context context) { - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (mChannel == null) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); From 84fe5f9e8abbe15e8f66090489f6e8ff858b6a04 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Tue, 28 Jul 2020 20:38:28 +0200 Subject: [PATCH 05/11] Use Cancel for negative button also if only option --- app/src/main/org/runnerup/db/DBHelper.java | 4 ++-- app/src/main/org/runnerup/view/HRSettingsActivity.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/org/runnerup/db/DBHelper.java b/app/src/main/org/runnerup/db/DBHelper.java index 76c6b212e..aed14d075 100644 --- a/app/src/main/org/runnerup/db/DBHelper.java +++ b/app/src/main/org/runnerup/db/DBHelper.java @@ -646,7 +646,7 @@ public void onClick(DialogInterface dialog, int which) { .setPositiveButton(ctx.getString(R.string.OK), listener); } catch (IOException e) { builder.setMessage("Exception: " + e.toString()) - .setNegativeButton(ctx.getString(R.string.OK), listener); + .setNegativeButton(ctx.getString(R.string.Cancel), listener); } builder.show(); } @@ -668,7 +668,7 @@ public void onClick(DialogInterface dialog, int which) { .setPositiveButton(ctx.getString(R.string.OK), listener); } catch (IOException e) { builder.setMessage("Exception: " + e.toString()) - .setNegativeButton(ctx.getString(R.string.OK), listener); + .setNegativeButton(ctx.getString(R.string.Cancel), listener); } builder.show(); } diff --git a/app/src/main/org/runnerup/view/HRSettingsActivity.java b/app/src/main/org/runnerup/view/HRSettingsActivity.java index 486b5554a..eecb940a6 100644 --- a/app/src/main/org/runnerup/view/HRSettingsActivity.java +++ b/app/src/main/org/runnerup/view/HRSettingsActivity.java @@ -314,7 +314,7 @@ public void onClick(DialogInterface dialog, int which) { AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(getString(R.string.Heart_rate_monitor_is_not_supported_for_your_device)) - .setNegativeButton(getString(R.string.OK), listener); + .setNegativeButton(getString(R.string.Cancel), listener); builder.show(); } From 57c3dcebc796415d48e17c7d48de1c294ffe112f Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Sun, 26 Jul 2020 18:19:11 +0200 Subject: [PATCH 06/11] Target SDK 29/Android 10 --- .travis.yml | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62a1804d8..796a8cceb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ env: global: - - ANDROID_API=28 + - ANDROID_API=29 - ANDROID_BUILD_TOOLS=30.0.1 - ADB_INSTALL_TIMEOUT=5 language: android diff --git a/build.gradle b/build.gradle index c418b333e..7373eae61 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,8 @@ project.ext { //Note that Android Studio does not know about the 'ext' module and will warn //minSdkVersion differs between modules buildToolsVersion = '30.0.1' //Update Travis manually - compileSdkVersion = 28 //Update Travis manually - targetSdkVersion = 28 + compileSdkVersion = 29 //Update Travis manually + targetSdkVersion = 29 appcompat_version = "1.1.0" //Note: Later Play Services will require a rewrite of NodeApi.NodeListener From 79f49f506b32ab555be374ff930432ed17721f8b Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 24 Jul 2020 15:09:05 +0200 Subject: [PATCH 07/11] ForegroundService update for SDK28 --- app/AndroidManifest.xml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index 1485dd590..d920ff1f1 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -38,7 +38,7 @@ - + - - + + + Date: Tue, 28 Jul 2020 13:29:46 +0200 Subject: [PATCH 08/11] Use SDK 29 scooped storage for external files For FileSynchronizer, save exports to a subdirectory of Documents, similar to the previous defaults. For db import/export use hardcoded getExternalFilesDir() and let the user copy files A file picker could be used eventually. --- app/AndroidManifest.xml | 4 +- app/res/layout/filepermission.xml | 2 +- app/src/main/org/runnerup/db/DBHelper.java | 28 +- .../org/runnerup/export/FileSynchronizer.java | 472 ++++++++++-------- .../main/org/runnerup/export/SyncManager.java | 40 +- .../main/org/runnerup/view/MainLayout.java | 48 +- .../org/runnerup/view/SettingsActivity.java | 127 +---- 7 files changed, 312 insertions(+), 409 deletions(-) diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index d920ff1f1..a1cfb194a 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -25,8 +25,8 @@ - - + + diff --git a/app/res/layout/filepermission.xml b/app/res/layout/filepermission.xml index 970eff823..052be80d3 100644 --- a/app/res/layout/filepermission.xml +++ b/app/res/layout/filepermission.xml @@ -41,7 +41,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="8dp" - android:text="URL" + android:text="URI" tools:ignore="HardcodedText"/> . - */ - -package org.runnerup.export; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.runnerup.R; -import org.runnerup.common.util.Constants; -import org.runnerup.common.util.Constants.DB; -import org.runnerup.db.PathSimplifier; -import org.runnerup.export.format.GPX; -import org.runnerup.export.format.TCX; -import org.runnerup.util.FileNameHelper; -import org.runnerup.workout.FileFormats; -import org.runnerup.workout.Sport; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; - - -public class FileSynchronizer extends DefaultSynchronizer { - - public static final String NAME = "File"; - - private long id = 0; - private String mPath; - private FileFormats mFormat; - private PathSimplifier simplifier; - - FileSynchronizer() {} - - FileSynchronizer(Context context, PathSimplifier simplifier) { - this(); - this.simplifier = simplifier; - } - - @Override - public long getId() { - return id; - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getPublicUrl() { - return "file://" + mPath; - } - - @Override - public int getIconId() {return R.drawable.service_file;} - - @Override - public int getColorId() {return R.color.colorPrimary;} - - static public String contentValuesToAuthConfig(ContentValues config) { - FileSynchronizer f = new FileSynchronizer(); - f.mPath = config.getAsString(DB.ACCOUNT.URL); - return f.getAuthConfig(); - } - - @Override - public void init(ContentValues config) { - String authConfig = config.getAsString(DB.ACCOUNT.AUTH_CONFIG); - if (authConfig != null) { - try { - mFormat = new FileFormats(config.getAsString(DB.ACCOUNT.FORMAT)); - JSONObject tmp = new JSONObject(authConfig); - mPath = tmp.optString(DB.ACCOUNT.URL, null); - } catch (JSONException e) { - Log.w(getName(), "init: Dropping config due to failure to parse json from " + authConfig + ", " + e); - } - } - id = config.getAsLong("_id"); - } - - @Override - public String getAuthConfig() { - JSONObject tmp = new JSONObject(); - if (isConfigured()) { - try { - tmp.put(DB.ACCOUNT.URL, mPath); - } catch (JSONException e) { - Log.w(getName(), "getAuthConfig: Failure to create json for " + mPath + ", " + e); - } - } - return tmp.toString(); - } - - @Override - public boolean isConfigured() { - return !TextUtils.isEmpty(mPath); - } - - @Override - public void reset() { - mPath = null; - } - - @Override - public Status connect() { - Status s = Status.NEED_AUTH; - s.authMethod = AuthMethod.FILEPERMISSION; - if (TextUtils.isEmpty(mPath)) - return s; - try { - File dstDir = new File(mPath); - //noinspection ResultOfMethodCallIgnored - dstDir.mkdirs(); - if (dstDir.isDirectory()) { - s = Status.OK; - } - } catch (SecurityException e) { - //Status is NEED_AUTH - } - return s; - } - - @Override - public Status upload(SQLiteDatabase db, final long mID) { - Status s = Status.ERROR; - s.activityId = mID; - if ((s = connect()) != Status.OK) { - return s; - } - - Sport sport = Sport.RUNNING; - long startTime = 0; - try { - String[] columns = { - Constants.DB.ACTIVITY.SPORT, - DB.ACTIVITY.START_TIME, - }; - Cursor c = null; - try { - c = db.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + mID, - null, null, null, null); - if (c.moveToFirst()) { - sport = Sport.valueOf(c.getInt(0)); - startTime = c.getLong(1); - } - } finally { - if (c != null) { - c.close(); - } - } - - String fileBase = new File(mPath).getAbsolutePath() + File.separator + - FileNameHelper.getExportFileName(startTime, sport.TapiriikType()); - - if (mFormat.contains(FileFormats.TCX)) { - TCX tcx = new TCX(db, simplifier); - File file = new File(fileBase + FileFormats.TCX.getValue()); - OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); - tcx.export(mID, new OutputStreamWriter(out)); - s.externalId = Uri.fromFile(file).toString(); - s.externalIdStatus = ExternalIdStatus.NONE; //Not working yet - } - if (mFormat.contains(FileFormats.GPX)) { - GPX gpx = new GPX(db, true, true, simplifier); - File file = new File(fileBase + FileFormats.GPX.getValue()); - OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); - gpx.export(mID, new OutputStreamWriter(out)); - } - s = Status.OK; - } catch (IOException e) { - //Status is ERROR - } - return s; - } - - @Override - public boolean checkSupport(Feature f) { - switch (f) { - case UPLOAD: - case FILE_FORMAT: - return true; - default: - return false; - } - } - - @Override - public void logout() { - } -} +/* + * Copyright (C) 2016 gerhard.nospam@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.runnerup.export; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; +import org.runnerup.R; +import org.runnerup.common.util.Constants; +import org.runnerup.common.util.Constants.DB; +import org.runnerup.content.ActivityProvider; +import org.runnerup.db.PathSimplifier; +import org.runnerup.export.format.GPX; +import org.runnerup.export.format.TCX; +import org.runnerup.util.FileNameHelper; +import org.runnerup.workout.FileFormats; +import org.runnerup.workout.Sport; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + + +public class FileSynchronizer extends DefaultSynchronizer { + + public static final String NAME = "File"; + + private long id = 0; + private Context mContext; + private String mPath; + private FileFormats mFormat; + private PathSimplifier simplifier; + + private FileSynchronizer() {} + + FileSynchronizer(Context context, PathSimplifier simplifier) { + this(); + this.mContext = context; + this.simplifier = simplifier; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getPublicUrl() { + return "file://" + + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + // Only for display + ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getPath() + File.separator + : "") + + mPath; + } + + @Override + public int getIconId() {return R.drawable.service_file;} + + @Override + public int getColorId() {return R.color.colorPrimary;} + + static public String contentValuesToAuthConfig(ContentValues config) { + FileSynchronizer f = new FileSynchronizer(); + f.mPath = config.getAsString(DB.ACCOUNT.URL); + return f.getAuthConfig(); + } + + @Override + public void init(ContentValues config) { + String authConfig = config.getAsString(DB.ACCOUNT.AUTH_CONFIG); + if (authConfig != null) { + try { + mFormat = new FileFormats(config.getAsString(DB.ACCOUNT.FORMAT)); + JSONObject tmp = new JSONObject(authConfig); + mPath = tmp.optString(DB.ACCOUNT.URL, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && mPath != null && mPath.startsWith(File.separator)) { + // Migrate to use scooped storage + mPath = mPath.substring(mPath.lastIndexOf(File.separator)); + } + } catch (JSONException e) { + Log.w(getName(), "init: Dropping config due to failure to parse json from " + authConfig + ", " + e); + } + } + id = config.getAsLong("_id"); + } + + @Override + public String getAuthConfig() { + JSONObject tmp = new JSONObject(); + if (isConfigured()) { + try { + tmp.put(DB.ACCOUNT.URL, mPath); + } catch (JSONException e) { + Log.w(getName(), "getAuthConfig: Failure to create json for " + mPath + ", " + e); + } + } + return tmp.toString(); + } + + @Override + public boolean isConfigured() { + return !TextUtils.isEmpty(mPath); + } + + @Override + public void reset() { + mPath = null; + } + + @Override + public Status connect() { + Status s = Status.NEED_AUTH; + s.authMethod = AuthMethod.FILEPERMISSION; + if (TextUtils.isEmpty(mPath)) { + return s; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + s = Status.OK; + return s; + } + try { + File dstDir = new File(mPath); + //noinspection ResultOfMethodCallIgnored + dstDir.mkdirs(); + if (dstDir.isDirectory()) { + s = Status.OK; + } + } catch (SecurityException e) { + //Status is NEED_AUTH + } + + return s; + } + + @Override + public Status upload(SQLiteDatabase db, final long mID) { + Status s = Status.ERROR; + s.activityId = mID; + if ((s = connect()) != Status.OK) { + return s; + } + + Sport sport = Sport.RUNNING; + long startTime = 0; + try { + String[] columns = { + Constants.DB.ACTIVITY.SPORT, + DB.ACTIVITY.START_TIME, + }; + Cursor c = null; + try { + c = db.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + mID, + null, null, null, null); + if (c.moveToFirst()) { + sport = Sport.valueOf(c.getInt(0)); + startTime = c.getLong(1); + } + } finally { + if (c != null) { + c.close(); + } + } + + String fileBase = FileNameHelper.getExportFileName(startTime, sport.TapiriikType()); + if (mFormat.contains(FileFormats.TCX)) { + OutputStream out = getOutputStream(fileBase + FileFormats.TCX.getValue(), ActivityProvider.TCX_MIME); + TCX tcx = new TCX(db, simplifier); + tcx.export(mID, new OutputStreamWriter(out)); + } + if (mFormat.contains(FileFormats.GPX)) { + OutputStream out = getOutputStream(fileBase + FileFormats.GPX.getValue(), ActivityProvider.GPX_MIME); + GPX gpx = new GPX(db, true, true, simplifier); + gpx.export(mID, new OutputStreamWriter(out)); + } + s.externalIdStatus = ExternalIdStatus.NONE; + s = Status.OK; + } catch (IOException e) { + //Status is ERROR + } + return s; + } + + private OutputStream getOutputStream(String fileName, String mimeType) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // mPath must be a relative location + final String relativeLocation = Environment.DIRECTORY_DOCUMENTS + File.separator + mPath; + + final ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName); + contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, relativeLocation); + // TODO int mediaType = Build.VERSION.SDK_INT == Build.VERSION_CODES.Q ? 0 : MediaStore.Files.FileColumns.MEDIA_TYPE_DOCUMENT; + contentValues.put(MediaStore.Files.FileColumns.MEDIA_TYPE, 0); + contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, mimeType); + + final ContentResolver resolver = mContext.getApplicationContext().getContentResolver(); + + final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + Uri uri = resolver.insert(contentUri, contentValues); + return resolver.openOutputStream(uri); + } else { + String path = new File(mPath).getAbsolutePath() + File.separator + fileName; + File file = new File(path); + return new BufferedOutputStream(new FileOutputStream(file)); + } + } + + @Override + public boolean checkSupport(Feature f) { + switch (f) { + case UPLOAD: + case FILE_FORMAT: + return true; + default: + return false; + } + } + + @Override + public void logout() { + } +} diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index cf4fa6c5e..f22657ad0 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -292,7 +292,7 @@ private Status handleRefreshComplete(final Synchronizer synchronizer, final Stat ContentValues tmp = new ContentValues(); tmp.put("_id", synchronizer.getId()); tmp.put(DB.ACCOUNT.AUTH_CONFIG, synchronizer.getAuthConfig()); - String args[] = { + String[] args = { Long.toString(synchronizer.getId()) }; mDB.update(DB.ACCOUNT.TABLE, tmp, "_id = ?", args); @@ -335,19 +335,15 @@ private void handleAuthComplete(Synchronizer synchronizer, Status s) { authCallback = null; authSynchronizer = null; if (s == Status.OK) { - try { - ContentValues tmp = new ContentValues(); - tmp.put("_id", synchronizer.getId()); - tmp.put(DB.ACCOUNT.AUTH_CONFIG, synchronizer.getAuthConfig()); + ContentValues tmp = new ContentValues(); + tmp.put("_id", synchronizer.getId()); + tmp.put(DB.ACCOUNT.AUTH_CONFIG, synchronizer.getAuthConfig()); - String[] args = { - Long.toString(synchronizer.getId()) - }; - mDB.update(DB.ACCOUNT.TABLE, tmp, "_id = ?", args); - } catch (Exception ex) { - Log.e(getClass().getName(), "Update failed:", ex); - } - } else { + String[] args = { + Long.toString(synchronizer.getId()) + }; + mDB.update(DB.ACCOUNT.TABLE, tmp, "_id = ?", args); + } else { synchronizer.reset(); } cb.run(synchronizer.getName(), s); @@ -483,7 +479,7 @@ private void askFileUrl(final Synchronizer sync) { final TextView tvAuthNotice = (TextView) view.findViewById(R.id.textViewAuthNotice); String path; - if (Build.VERSION.SDK_INT >= 29) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // All paths are related to Environment.DIRECTORY_DOCUMENTS path = ""; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { @@ -491,9 +487,9 @@ private void askFileUrl(final Synchronizer sync) { path = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOCUMENTS).getPath() + File.separator; } else { - path = Environment.getExternalStorageDirectory().getPath(); + path = Environment.getExternalStorageDirectory().getPath() + File.separator; } - path += File.separator + "RunnerUp"; + path += "RunnerUp"; tv1.setText(path); if (sync.getAuthNotice() != 0) { @@ -513,9 +509,17 @@ private void askFileUrl(final Synchronizer sync) { @Override public void onClick(DialogInterface dialog, int which) { //Set default values - ContentValues tmp = new ContentValues(); - tmp.put(DB.ACCOUNT.URL, tv1.getText().toString()); + String uri = tv1.getText().toString().trim(); + while (uri.endsWith(File.separator)){ + uri = uri.substring(0, uri.length()-1); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + while (uri.startsWith(File.separator)) { + uri = uri.substring(1); + } + } + tmp.put(DB.ACCOUNT.URL, uri); ContentValues config = new ContentValues(); config.put("_id", sync.getId()); config.put(DB.ACCOUNT.AUTH_CONFIG, FileSynchronizer.contentValuesToAuthConfig(tmp)); diff --git a/app/src/main/org/runnerup/view/MainLayout.java b/app/src/main/org/runnerup/view/MainLayout.java index 116ae5ef8..24ac595fe 100644 --- a/app/src/main/org/runnerup/view/MainLayout.java +++ b/app/src/main/org/runnerup/view/MainLayout.java @@ -187,13 +187,9 @@ public void onCreate(Bundle savedInstanceState) { } if (filePath != null) { - if (requestReadStoragePermissions(MainLayout.this)) { - Log.i(getClass().getSimpleName(), "Importing database from " + filePath); - DBHelper.importDatabase(MainLayout.this, filePath); - } else { - Toast.makeText(this, "Storage permission not granted in Android settings, db is not imported.", - Toast.LENGTH_SHORT).show(); - } + // No check for permissions or that this is within scooped storage (>=SDK29) + Log.i(getClass().getSimpleName(), "Importing database from " + filePath); + DBHelper.importDatabase(MainLayout.this, filePath); } } @@ -357,52 +353,16 @@ public void onClick(View view) { } } - private static boolean requestReadStoragePermissions(final Activity activity) { - boolean ret = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && - ContextCompat.checkSelfPermission(activity, - Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - ret = false; - - //Request permission (not using shouldShowRequestPermissionRationale()) - ActivityCompat.requestPermissions(activity, - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_READ_EXTERNAL_STORAGE); - String s = "Requesting read permission"; - Log.i(activity.getClass().getSimpleName(), s); - } - return ret; - } - /** * Id to identify a permission request. */ private static final int REQUEST_LOCATION = 1000; - private static final int REQUEST_READ_EXTERNAL_STORAGE = 2000; - private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 2001; @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == REQUEST_READ_EXTERNAL_STORAGE || requestCode == REQUEST_WRITE_EXTERNAL_STORAGE) { - // Check if the only required permission has been granted (could react on the response) - //noinspection StatementWithEmptyBody - if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - //OK, could redo request here - } else { - String s = (requestCode == REQUEST_READ_EXTERNAL_STORAGE ? "READ" : "WRITE") - + " permission was NOT granted"; - if (grantResults.length >= 1) { - s += grantResults[0]; - } - - Log.i(getClass().getSimpleName(), s); - //Toast.makeText(SettingsActivity.this, s, Toast.LENGTH_SHORT).show(); - } - - } else if (requestCode == REQUEST_LOCATION) { + if (requestCode == REQUEST_LOCATION) { // Check if the only required permission has been granted (could react on the response) if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { String s = "Permission response OK: " + grantResults.length; diff --git a/app/src/main/org/runnerup/view/SettingsActivity.java b/app/src/main/org/runnerup/view/SettingsActivity.java index 1c9ba9f89..cb3a24c4f 100644 --- a/app/src/main/org/runnerup/view/SettingsActivity.java +++ b/app/src/main/org/runnerup/view/SettingsActivity.java @@ -17,30 +17,17 @@ package org.runnerup.view; -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import org.runnerup.R; import org.runnerup.db.DBHelper; @@ -49,8 +36,7 @@ import org.runnerup.tracker.component.TrackerTemperature; -public class SettingsActivity extends PreferenceActivity - implements ActivityCompat.OnRequestPermissionsResultCallback{ +public class SettingsActivity extends PreferenceActivity { public void onCreate(Bundle savedInstanceState) { Resources res = getResources(); @@ -120,128 +106,25 @@ public static boolean hasHR(Context ctx) { return btProviderName != null && btAddress != null; } - @SuppressLint("InlinedApi") - public static boolean requestReadStoragePermissions(final Activity activity) { - boolean ret = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && - ContextCompat.checkSelfPermission(activity, - Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - ret = false; - - //Request permission - not working from Settings.Activity - ActivityCompat.requestPermissions(activity, - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_READ_EXTERNAL_STORAGE); - String s = "Requesting read permission"; - Log.i(activity.getClass().getSimpleName(), s); - } - return ret; - } - - private static boolean requestWriteStoragePermissions(final Activity activity) { - boolean ret = true; - if (ContextCompat.checkSelfPermission(activity, - Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - ret = false; - - //Request permission (not using shouldShowRequestPermissionRationale()) - // not working from Settings.Activity - ActivityCompat.requestPermissions(activity, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - REQUEST_WRITE_EXTERNAL_STORAGE); - String s = "Requesting write permission"; - Log.i(activity.getClass().getSimpleName(), s); - } - return ret; - } - - /** - * Id to identify a permission request. - */ - private static final int REQUEST_READ_EXTERNAL_STORAGE = 2000; - private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 2001; - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - - if (requestCode == REQUEST_READ_EXTERNAL_STORAGE || requestCode == REQUEST_WRITE_EXTERNAL_STORAGE) { - // Check if the only required permission has been granted (could react on the response) - //noinspection StatementWithEmptyBody - if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - //OK, could redo request here - } else { - String s = (requestCode == REQUEST_READ_EXTERNAL_STORAGE ? "READ" : "WRITE") - + " permission was NOT granted"; - if (grantResults.length >= 1) { - s += grantResults[0]; - } - - Log.i(getClass().getSimpleName(), s); - //Toast.makeText(SettingsActivity.this, s, Toast.LENGTH_SHORT).show(); - } - - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - private final OnPreferenceClickListener onExportClick = new OnPreferenceClickListener() { - @Override public boolean onPreferenceClick(Preference preference) { - if (requestWriteStoragePermissions(SettingsActivity.this)) { - String dstdir = Environment.getExternalStorageDirectory().getPath(); - String to = dstdir + "/runnerup.db.export"; - DBHelper.exportDatabase(SettingsActivity.this, to); - - } else { - DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - - }; - AlertDialog.Builder builder = new AlertDialog.Builder(SettingsActivity.this) - .setTitle("Export runnerup.db") - .setMessage("Storage permission not granted in Android settings") - .setNegativeButton(getString(R.string.OK), listener); - builder.show(); - } + // TODO Use picker with ACTION_CREATE_DOCUMENT + DBHelper.exportDatabase(SettingsActivity.this, null); return false; } }; private final OnPreferenceClickListener onImportClick = new OnPreferenceClickListener() { - @Override public boolean onPreferenceClick(Preference preference) { - if (requestReadStoragePermissions(SettingsActivity.this)) { - String srcdir = Environment.getExternalStorageDirectory().getPath(); - String from = srcdir + "/runnerup.db.export"; - DBHelper.importDatabase(SettingsActivity.this, from); - } else { - DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - - }; - AlertDialog.Builder builder = new AlertDialog.Builder(SettingsActivity.this) - .setTitle("Import runnerup.db") - .setMessage("Storage permission not granted in Android settings") - .setNegativeButton(getString(R.string.OK), listener); - builder.show(); - } + // TODO Use picker with ACTION_OPEN_DOCUMENT + DBHelper.importDatabase(SettingsActivity.this, null); return false; } }; private final OnPreferenceClickListener onPruneClick = new OnPreferenceClickListener() { - @Override public boolean onPreferenceClick(Preference preference) { final ProgressDialog dialog = new ProgressDialog(SettingsActivity.this); From 606d1cd56091c441b00b5025b8e733419715e00e Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 24 Jul 2020 01:49:01 +0200 Subject: [PATCH 09/11] Permissions for activity and background for Android 10 If permissions are denied, give motivation and let the user try again (unless "don't ask again" is ticked) Remove snackbar as it will not rerequest permissions if the user ticks "don't ask again". Instead use a popup that asks the user to go to system settings, without starting the workout. (Linking to system settings is not recommended in the guidelines.) TODO onRequestPermissionsResult() is not called, why permissions are checked onCreate (similar to how requested for autostart GPS) instead of when selecting Enable GPS. --- app/AndroidManifest.xml | 2 + .../main/org/runnerup/view/MainLayout.java | 103 +------ .../org/runnerup/view/SettingsActivity.java | 284 +++++++++--------- .../main/org/runnerup/view/StartActivity.java | 162 ++++++++-- common/src/main/res/values/strings.xml | 5 + 5 files changed, 296 insertions(+), 260 deletions(-) diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index a1cfb194a..15fd4baf7 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -25,6 +25,8 @@ + + diff --git a/app/src/main/org/runnerup/view/MainLayout.java b/app/src/main/org/runnerup/view/MainLayout.java index 24ac595fe..d41668a93 100644 --- a/app/src/main/org/runnerup/view/MainLayout.java +++ b/app/src/main/org/runnerup/view/MainLayout.java @@ -17,19 +17,15 @@ package org.runnerup.view; -import android.Manifest; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.Service; import android.app.TabActivity; import android.content.ContentValues; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.content.res.Resources; @@ -37,9 +33,7 @@ import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; @@ -48,16 +42,10 @@ import android.webkit.WebView; import android.widget.ImageView; import android.widget.TabHost; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import com.google.android.material.snackbar.Snackbar; - import org.runnerup.R; import org.runnerup.common.util.Constants.DB; import org.runnerup.db.DBHelper; @@ -67,12 +55,9 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -public class MainLayout extends TabActivity - implements ActivityCompat.OnRequestPermissionsResultCallback { +public class MainLayout extends TabActivity { private View getTabView(CharSequence label, int iconResource) { @SuppressLint("InflateParams")View tabView = getLayoutInflater().inflate(R.layout.bottom_tab_indicator, null); @@ -164,8 +149,6 @@ public void onCreate(Bundle savedInstanceState) { if (upgradeState == UpgradeState.UPGRADE) { whatsNew(); } - //GPS is essential, always nag user if not granted - requestGpsPermissions(this, tabHost.getCurrentView()); //Import workouts/schemes. No permission needed handleBundled(getApplicationContext().getAssets(), "bundled", getFilesDir().getPath() + "/.."); @@ -296,88 +279,12 @@ private void whatsNew() { LayoutInflater inflater = (LayoutInflater) getSystemService(Service.LAYOUT_INFLATER_SERVICE); @SuppressLint("InflateParams") View view = inflater.inflate(R.layout.whatsnew, null); WebView wv = (WebView) view.findViewById(R.id.web_view1); - AlertDialog.Builder builder = new AlertDialog.Builder(this) + new AlertDialog.Builder(this) .setTitle(getString(R.string.Whats_new)) .setView(view) - .setPositiveButton(getString(R.string.Rate_RunnerUp), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - onRateClick.onClick(null); - } - - }) - .setNegativeButton(getString(R.string.OK), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - builder.show(); + .setPositiveButton(getString(R.string.Rate_RunnerUp), (dialog, which) -> onRateClick.onClick(null)) + .setNegativeButton(getString(R.string.OK), (dialog, which) -> dialog.dismiss()) + .show(); wv.loadUrl("file:///android_asset/changes.html"); } - - private static void requestGpsPermissions(final Activity activity, final View view) { - String[] requiredPerms = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}; - List defaultPerms = new ArrayList<>(); - List shouldPerms = new ArrayList<>(); - for (final String perm : requiredPerms) { - if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) { - - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, perm)) { - shouldPerms.add(perm); - } else { - defaultPerms.add(perm); - } - } - } - if (defaultPerms.size() > 0) { - // No explanation needed, we can request the permission. - final String[] perms = new String[defaultPerms.size()]; - defaultPerms.toArray(perms); - ActivityCompat.requestPermissions(activity, perms, REQUEST_LOCATION); - } - - if (shouldPerms.size() > 0) { - //Snackbar, no popup - final String[] perms = new String[shouldPerms.size()]; - shouldPerms.toArray(perms); - Snackbar.make(view, activity.getResources().getString(R.string.GPS_permission_required), - Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.OK, new View.OnClickListener() { - @Override - public void onClick(View view) { - ActivityCompat.requestPermissions(activity, perms, REQUEST_LOCATION); - } - }) - .show(); - } - } - - /** - * Id to identify a permission request. - */ - private static final int REQUEST_LOCATION = 1000; - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - - if (requestCode == REQUEST_LOCATION) { - // Check if the only required permission has been granted (could react on the response) - if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - String s = "Permission response OK: " + grantResults.length; - Log.v("MainLayout", s); - //Toast.makeText(MainLayout.this, s, Toast.LENGTH_SHORT).show(); - } else { - String s = "Location Permission was not granted: "; - Log.i("MainLayout", s); - Toast.makeText(MainLayout.this, s, Toast.LENGTH_SHORT).show(); - } - - } else { - String s = "Unexpected permission request: " + requestCode; - Log.w("MainLayout", s); - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } } diff --git a/app/src/main/org/runnerup/view/SettingsActivity.java b/app/src/main/org/runnerup/view/SettingsActivity.java index cb3a24c4f..e8120f747 100644 --- a/app/src/main/org/runnerup/view/SettingsActivity.java +++ b/app/src/main/org/runnerup/view/SettingsActivity.java @@ -1,142 +1,142 @@ -/* - * Copyright (C) 2012 - 2014 jonas.oreland@gmail.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.runnerup.view; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceActivity; -import android.preference.PreferenceManager; - -import org.runnerup.R; -import org.runnerup.db.DBHelper; -import org.runnerup.tracker.component.TrackerCadence; -import org.runnerup.tracker.component.TrackerPressure; -import org.runnerup.tracker.component.TrackerTemperature; - - -public class SettingsActivity extends PreferenceActivity { - - public void onCreate(Bundle savedInstanceState) { - Resources res = getResources(); - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.settings); - setContentView(R.layout.settings_wrapper); - { - Preference btn = findPreference(res.getString(R.string.pref_exportdb)); - btn.setOnPreferenceClickListener(onExportClick); - } - { - Preference btn = findPreference(res.getString(R.string.pref_importdb)); - btn.setOnPreferenceClickListener(onImportClick); - } - { - Preference btn = findPreference(res.getString(R.string.pref_prunedb)); - btn.setOnPreferenceClickListener(onPruneClick); - } - - - if (!hasHR(this)) { - getPreferenceManager().findPreference(res.getString(R.string.cue_configure_hrzones)).setEnabled(false); - getPreferenceManager().findPreference(res.getString(R.string.pref_battery_level_low_threshold)).setEnabled(false); - getPreferenceManager().findPreference(res.getString(R.string.pref_battery_level_high_threshold)).setEnabled(false); - } - { - //Preference pref = findPreference(this.getString(R.string.pref_experimental_features)); - //pref.setSummary(null); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Preference pref = findPreference(this.getString(R.string.pref_keystartstop_active)); - pref.setEnabled(false); - } - if (!TrackerCadence.isAvailable(this)) { - Preference pref = findPreference(this.getString(R.string.pref_use_cadence_step_sensor)); - pref.setEnabled(false); - } - if (!TrackerTemperature.isAvailable(this)) { - Preference pref = findPreference(this.getString(R.string.pref_use_temperature_sensor)); - pref.setEnabled(false); - } - if (!TrackerPressure.isAvailable(this)) { - Preference pref = findPreference(this.getString(R.string.pref_use_pressure_sensor)); - pref.setEnabled(false); - } - CheckBoxPreference simplifyOnSave = (CheckBoxPreference) findPreference(getString(R.string.pref_path_simplification_on_save)); - CheckBoxPreference simplifyOnExport = (CheckBoxPreference) findPreference(getString(R.string.pref_path_simplification_on_export)); - if (simplifyOnSave.isChecked()) { - simplifyOnExport.setChecked(true); - } - simplifyOnSave.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newValue){ - if ((Boolean) newValue) { - simplifyOnExport.setChecked(true); - }; - return true; - } - }); - } - - public static boolean hasHR(Context ctx) { - Resources res = ctx.getResources(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - String btAddress = prefs.getString(res.getString(R.string.pref_bt_address), null); - String btProviderName = prefs.getString(res.getString(R.string.pref_bt_provider), null); - return btProviderName != null && btAddress != null; - } - - private final OnPreferenceClickListener onExportClick = new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - // TODO Use picker with ACTION_CREATE_DOCUMENT - DBHelper.exportDatabase(SettingsActivity.this, null); - return false; - } - }; - - private final OnPreferenceClickListener onImportClick = new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - // TODO Use picker with ACTION_OPEN_DOCUMENT - DBHelper.importDatabase(SettingsActivity.this, null); - return false; - } - }; - - private final OnPreferenceClickListener onPruneClick = new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - final ProgressDialog dialog = new ProgressDialog(SettingsActivity.this); - dialog.setTitle(R.string.Pruning_deleted_activities_from_database); - dialog.show(); - DBHelper.purgeDeletedActivities(SettingsActivity.this, dialog, new Runnable() { - @Override - public void run() { - dialog.dismiss(); - } - }); - return false; - } - }; -} +/* + * Copyright (C) 2012 - 2014 jonas.oreland@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.runnerup.view; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; + +import org.runnerup.R; +import org.runnerup.db.DBHelper; +import org.runnerup.tracker.component.TrackerCadence; +import org.runnerup.tracker.component.TrackerPressure; +import org.runnerup.tracker.component.TrackerTemperature; + + +public class SettingsActivity extends PreferenceActivity { + + public void onCreate(Bundle savedInstanceState) { + Resources res = getResources(); + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + setContentView(R.layout.settings_wrapper); + { + Preference btn = findPreference(res.getString(R.string.pref_exportdb)); + btn.setOnPreferenceClickListener(onExportClick); + } + { + Preference btn = findPreference(res.getString(R.string.pref_importdb)); + btn.setOnPreferenceClickListener(onImportClick); + } + { + Preference btn = findPreference(res.getString(R.string.pref_prunedb)); + btn.setOnPreferenceClickListener(onPruneClick); + } + + + if (!hasHR(this)) { + getPreferenceManager().findPreference(res.getString(R.string.cue_configure_hrzones)).setEnabled(false); + getPreferenceManager().findPreference(res.getString(R.string.pref_battery_level_low_threshold)).setEnabled(false); + getPreferenceManager().findPreference(res.getString(R.string.pref_battery_level_high_threshold)).setEnabled(false); + } + { + //Preference pref = findPreference(this.getString(R.string.pref_experimental_features)); + //pref.setSummary(null); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Preference pref = findPreference(this.getString(R.string.pref_keystartstop_active)); + pref.setEnabled(false); + } + if (!TrackerCadence.isAvailable(this)) { + Preference pref = findPreference(this.getString(R.string.pref_use_cadence_step_sensor)); + pref.setEnabled(false); + } + if (!TrackerTemperature.isAvailable(this)) { + Preference pref = findPreference(this.getString(R.string.pref_use_temperature_sensor)); + pref.setEnabled(false); + } + if (!TrackerPressure.isAvailable(this)) { + Preference pref = findPreference(this.getString(R.string.pref_use_pressure_sensor)); + pref.setEnabled(false); + } + CheckBoxPreference simplifyOnSave = (CheckBoxPreference) findPreference(getString(R.string.pref_path_simplification_on_save)); + CheckBoxPreference simplifyOnExport = (CheckBoxPreference) findPreference(getString(R.string.pref_path_simplification_on_export)); + if (simplifyOnSave.isChecked()) { + simplifyOnExport.setChecked(true); + } + simplifyOnSave.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue){ + if ((Boolean) newValue) { + simplifyOnExport.setChecked(true); + }; + return true; + } + }); + } + + public static boolean hasHR(Context ctx) { + Resources res = ctx.getResources(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + String btAddress = prefs.getString(res.getString(R.string.pref_bt_address), null); + String btProviderName = prefs.getString(res.getString(R.string.pref_bt_provider), null); + return btProviderName != null && btAddress != null; + } + + private final OnPreferenceClickListener onExportClick = new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // TODO Use picker with ACTION_CREATE_DOCUMENT + DBHelper.exportDatabase(SettingsActivity.this, null); + return false; + } + }; + + private final OnPreferenceClickListener onImportClick = new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // TODO Use picker with ACTION_OPEN_DOCUMENT + DBHelper.importDatabase(SettingsActivity.this, null); + return false; + } + }; + + private final OnPreferenceClickListener onPruneClick = new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + final ProgressDialog dialog = new ProgressDialog(SettingsActivity.this); + dialog.setTitle(R.string.Pruning_deleted_activities_from_database); + dialog.show(); + DBHelper.purgeDeletedActivities(SettingsActivity.this, dialog, new Runnable() { + @Override + public void run() { + dialog.dismiss(); + } + }); + return false; + } + }; +} diff --git a/app/src/main/org/runnerup/view/StartActivity.java b/app/src/main/org/runnerup/view/StartActivity.java index 279b7a736..ec65856f3 100644 --- a/app/src/main/org/runnerup/view/StartActivity.java +++ b/app/src/main/org/runnerup/view/StartActivity.java @@ -17,6 +17,7 @@ package org.runnerup.view; +import android.Manifest; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -26,6 +27,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.os.Build; @@ -33,9 +35,14 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.Settings; + +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -66,6 +73,7 @@ import org.runnerup.notification.NotificationStateManager; import org.runnerup.tracker.GpsInformation; import org.runnerup.tracker.Tracker; +import org.runnerup.tracker.component.TrackerCadence; import org.runnerup.tracker.component.TrackerHRM; import org.runnerup.tracker.component.TrackerWear; import org.runnerup.util.Formatter; @@ -87,7 +95,8 @@ import java.util.List; import java.util.Locale; -public class StartActivity extends AppCompatActivity implements TickListener, GpsInformation { +public class StartActivity extends AppCompatActivity + implements ActivityCompat.OnRequestPermissionsResultCallback, TickListener, GpsInformation { private enum GpsLevel {POOR, ACCEPTABLE, GOOD} @@ -462,7 +471,9 @@ private void unregisterStartEventListener() { } private void onGpsTrackerBound() { - if (getAutoStartGps()) { + // check and request permissions at startup + boolean missingEssentialPermission = checkPermissions(false); + if (!missingEssentialPermission && getAutoStartGps()) { startGps(); } else { switch (mTracker.getState()) { @@ -496,7 +507,11 @@ private boolean getAutoStartGps() { } private void startGps() { - Log.e(getClass().getName(), "StartActivity.startGps()"); + Log.v(getClass().getName(), "StartActivity.startGps()"); + if (!mGpsStatus.isEnabled()) { + startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + if (mGpsStatus != null && !mGpsStatus.isLogging()) mGpsStatus.start(this); @@ -549,20 +564,18 @@ private void notificationBatteryLevel(int batteryLevel) { final CheckBox dontShowAgain = new CheckBox(this); dontShowAgain.setText(getResources().getText(R.string.Do_not_show_again)); - AlertDialog.Builder prompt = new AlertDialog.Builder(this) + AlertDialog prompt = new AlertDialog.Builder(this) .setView(dontShowAgain) .setCancelable(false) .setMessage(getResources().getText(R.string.Low_HRM_battery_level) + "\n" + getResources().getText(R.string.Battery_level) + ": " + batteryLevel + "%") .setTitle(getResources().getText(R.string.Warning)) - .setPositiveButton(getResources().getText(R.string.OK), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (dontShowAgain.isChecked()) { - prefs.edit().putBoolean(pref_key, true).apply(); - } - } - }); - prompt.show(); + .setPositiveButton(getResources().getText(R.string.OK), (dialog, which) -> { + if (dontShowAgain.isChecked()) { + prefs.edit().putBoolean(pref_key, true).apply(); + } + }) + .show(); } private final OnTabChangeListener onTabChangeListener = new OnTabChangeListener() { @@ -627,17 +640,126 @@ public void onClick(View v) { } }; - private final OnClickListener gpsEnableClick = new OnClickListener() { - public void onClick(View v) { - if (!mGpsStatus.isEnabled()) { - startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } else if (mTracker.getState() != TrackerState.CONNECTED) { - startGps(); - } - updateView(); + private final OnClickListener gpsEnableClick = v -> { + if (checkPermissions(true)) { + // Handle view update etc in permission callback + return; + } + + if (mTracker.getState() != TrackerState.CONNECTED) { + startGps(); } + updateView(); }; + + private List getPermissions() { + List requiredPerms = new ArrayList<>(); + requiredPerms.add(Manifest.permission.ACCESS_FINE_LOCATION); + requiredPerms.add(Manifest.permission.ACCESS_COARSE_LOCATION); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + requiredPerms.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean enabled = prefs.getBoolean(this.getString(org.runnerup.R.string.pref_use_cadence_step_sensor), true); + if (enabled && TrackerCadence.isAvailable(this)) { + requiredPerms.add(Manifest.permission.ACTIVITY_RECOGNITION); + } + } + + return requiredPerms; + } + + /** + * Check that required permissions are allowed + * @param popup + * @return + */ + private boolean checkPermissions(boolean popup) { + boolean missingEssentialPermission = false; + boolean missingAnyPermission = false; + List requiredPerms = getPermissions(); + List requestPerms = new ArrayList<>(); + + for (final String perm : requiredPerms) { + if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) { + missingAnyPermission = true; + // Filter non essential permissions for result + missingEssentialPermission = missingEssentialPermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !perm.equals(Manifest.permission.ACTIVITY_RECOGNITION); + if (ActivityCompat.shouldShowRequestPermissionRationale(this, perm)) { + // A denied permission, show motivation in a popup + String s = "Permission " + perm + " is explicitly denied"; + Log.i(getClass().getName(), s); + } else { + requestPerms.add(perm); + } + } + } + + if (missingAnyPermission) { + final String[] permissions = new String[requestPerms.size()]; + requestPerms.toArray(permissions); + + if (popup && (missingEssentialPermission || requestPerms.size() > 0)) { + // Essential or requestable permissions missing + String baseMessage = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? getString(R.string.GPS_permission_text) + : getString(R.string.GPS_permission_text_pre_Android10); + + AlertDialog.Builder builder = new AlertDialog.Builder(StartActivity.this) + .setTitle(getString(R.string.GPS_permission_required)) + .setMessage(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? getString(R.string.GPS_permission_text) + : getString(R.string.GPS_permission_text_pre_Android10)) + .setNegativeButton(getString(R.string.Cancel), (dialog, which) -> dialog.dismiss()); + if (requestPerms.size() > 0) { + builder.setPositiveButton(getString(R.string.OK), (dialog, id) -> { + ActivityCompat.requestPermissions(this.getParent(), permissions, REQUEST_LOCATION); + }); + builder.setMessage(baseMessage + "\n" + getString(R.string.Request_permission_text)); + } else { + builder.setMessage(baseMessage); + } + builder.show(); + } else if (requestPerms.size() > 0) { + ActivityCompat.requestPermissions(this.getParent(), permissions, REQUEST_LOCATION); + } + } + + return missingEssentialPermission; + } + + // Id to identify a permission request. + // TODO When released in 1.2.0, use https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.RequestPermission + private static final int REQUEST_LOCATION = 3000; + + // TODO This callback is not called (due to requestPermissions(this.getParent()?), so onCreate() is used + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == REQUEST_LOCATION) { + // Check if the only required permission has been granted (could react on the response) + if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + String s = "Permission response OK"; + Log.i(getClass().getName(), s); + if (mTracker.getState() != TrackerState.CONNECTED) { + startGps(); + } + updateView(); + + } else { + String s = "Permission was not granted: " + " ("+grantResults.length+", "+permissions.length + ")"; + Log.i(getClass().getName(), s); + } + } else { + String s = "Unexpected permission request: " + requestCode; + Log.w(getClass().getName(), s); + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + private void toggleStatusDetails() { statusDetailsShown = !statusDetailsShown; float bottomMargin; diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 0d1fe85fe..7ad09a032 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -296,6 +296,11 @@ Share workout… New audio scheme GPS permission required + Location permission required in system settings + Location permission \"Allow all the time\" required in system settings.\nFor running cadence \"Physical activity\" permission is required too. + Activity recognition permission required + The permission \"Physical activity\" is required for running cadence + Allow permission by clicking \"OK\". Default Workout notes Downloading from %1$s From 1494fcf2ce9eeff49e2c16e52a26800de416092a Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Thu, 30 Jul 2020 12:09:39 +0200 Subject: [PATCH 10/11] Updated Swedish translation --- common/src/main/res/values-pl/strings.xml | 2 +- common/src/main/res/values-sv/strings.xml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 20a6a3fd2..38aca5285 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -114,8 +114,8 @@ Konfiguruj konta Odśwież Wiek - Sychronizuje: aktualizuję kanał... + Synchronizowanie OK Zarządzaj połączeniami Podłącz konto diff --git a/common/src/main/res/values-sv/strings.xml b/common/src/main/res/values-sv/strings.xml index 6752525d5..e207bfd1f 100644 --- a/common/src/main/res/values-sv/strings.xml +++ b/common/src/main/res/values-sv/strings.xml @@ -149,7 +149,6 @@ Skapa nytt schema för ljudmeddelande Ja Skapa - Inte alls Nej Laddar Sparar @@ -295,6 +294,11 @@ Dela träningspass… Nytt ljudschema GPS behörighet krävs + Behörighet för Plats behövs i systeminställningar + Behörighet för Plats behövs i systeminställningar.\nFör stegfrekvens behövs behörighet för Kroppssensorer också. + Behörighet för Kroppssensorer behövs + Behörighet för Kroppssensorer behövs för stegfrekvens + Tillåt behörighet genom att klicka OK. Standard Anteckningar om träningspasset Laddar ner från %1$s From ccf6de6b44ddd1e4a9f1e8aa066be40fa7a34276 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Wed, 29 Jul 2020 01:14:19 +0200 Subject: [PATCH 11/11] Prepare for release --- app/assets/changes.html | 25 ++++++++++++++++++++++--- build.gradle | 4 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/assets/changes.html b/app/assets/changes.html index dc11ce206..078755c97 100644 --- a/app/assets/changes.html +++ b/app/assets/changes.html @@ -5,15 +5,34 @@

What's new

-

v2.1.0.1

+

v2.2.0.0

    -
  • #947 AndroidX and AppCompatActivity migration
  • +
  • Version changed to 2.2 for Play release, previous production is 2.1.0.0
  • +
  • #949 Play console crashe corrections
  • +
  • #948 Target Android 10, update permission handling +
    Changes required to target Android 10 and prepare for Android 11/R, required to update the app in Play. +
      +
    • Use SDK 29 scooped storage for external files, Google is limiting the file system use from Android 10/11 +
      For FileSynchronizer, save exports to a subdirectory of Documents, similar to the previous defaults. +
      For db import/export use hardcoded getExternalFilesDir() and let the user copy files.
    • +
    • Permissions for activity and background for Android 10 +
      If permissions are denied, give motivation and let the user try again +(unless "don't ask again" is ticked) +
      Remove snackbar as it will not rerequest permissions if the user ticks "don't ask again". +Instead use a popup that asks the user to go to system settings, +without starting the workout. (Linking to system settings is not +recommended in the Google guidelines.) +
    • +
    +
  • +
  • #947 AndroidX and AppCompatActivity migration +
    Internal change, support libraries replaced with Google's updated libraries

v2.1.0.0

    -
  • Minor version number changed to 2.1 to prepare for Play release, previous production is 2.0.2.1
  • +
  • Minor version number changed to 2.1 for Play release, previous production is 2.0.2.1
  • #946 Play console feedback
    • Translations update: Czech cue, Romanian, Indonesian
    • diff --git a/build.gradle b/build.gradle index 7373eae61..ef7cd7469 100644 --- a/build.gradle +++ b/build.gradle @@ -30,8 +30,8 @@ project.ext { mockitoVersion = '2.3.7' //The Git tag for the release must be identical for F-Droid - versionName = '2.1.0.1' - versionCode = 241 + versionName = '2.2.0.0' + versionCode = 250 latestBaseVersionCode = 15000000 travisBuild = System.getenv("TRAVIS") == "true"