diff --git a/.travis.yml b/.travis.yml
index e71115664..796a8cceb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
env:
global:
- - ANDROID_API=28
- - ANDROID_BUILD_TOOLS=29.0.3
+ - ANDROID_API=29
+ - ANDROID_BUILD_TOOLS=30.0.1
- ADB_INSTALL_TIMEOUT=5
language: android
jdk:
diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml
index 1485dd590..15fd4baf7 100644
--- a/app/AndroidManifest.xml
+++ b/app/AndroidManifest.xml
@@ -25,8 +25,10 @@
-
-
+
+
+
+
@@ -38,7 +40,7 @@
-
+
-
-
+
+
+
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/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/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/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..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,14 +479,17 @@ 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 >= Build.VERSION_CODES.Q) {
+ // 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();
+ path = Environment.getExternalStorageDirectory().getPath() + File.separator;
}
- path += File.separator + "RunnerUp";
+ path += "RunnerUp";
tv1.setText(path);
if (sync.getAuthNotice() != 0) {
@@ -510,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));
@@ -545,7 +552,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/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/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/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();
}
diff --git a/app/src/main/org/runnerup/view/MainLayout.java b/app/src/main/org/runnerup/view/MainLayout.java
index 116ae5ef8..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() + "/..");
@@ -187,13 +170,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);
}
}
@@ -300,124 +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();
- }
- }
-
- 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) {
- // 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 1c9ba9f89..e8120f747 100644
--- a/app/src/main/org/runnerup/view/SettingsActivity.java
+++ b/app/src/main/org/runnerup/view/SettingsActivity.java
@@ -1,259 +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.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;
-import org.runnerup.tracker.component.TrackerCadence;
-import org.runnerup.tracker.component.TrackerPressure;
-import org.runnerup.tracker.component.TrackerTemperature;
-
-
-public class SettingsActivity extends PreferenceActivity
- implements ActivityCompat.OnRequestPermissionsResultCallback{
-
- 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;
- }
-
- @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();
- }
- 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();
- }
- 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 080e85eff..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;
@@ -657,10 +779,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 +829,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 +855,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 +941,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() {
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/build.gradle b/build.gradle
index 6097471e5..ef7cd7469 100644
--- a/build.gradle
+++ b/build.gradle
@@ -17,9 +17,9 @@ 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
- compileSdkVersion = 28 //Update Travis manually
- targetSdkVersion = 28
+ buildToolsVersion = '30.0.1' //Update Travis manually
+ compileSdkVersion = 29 //Update Travis manually
+ targetSdkVersion = 29
appcompat_version = "1.1.0"
//Note: Later Play Services will require a rewrite of NodeApi.NodeListener
@@ -30,9 +30,9 @@ 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
- latestBaseVersionCode = 14000000
+ versionName = '2.2.0.0'
+ versionCode = 250
+ latestBaseVersionCode = 15000000
travisBuild = System.getenv("TRAVIS") == "true"
// allows for -Dpre-dex=false to be set
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
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
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);