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);