From f82ba6e12b0d47ddd70f10dc0a318933be602ce0 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Wed, 20 Jan 2016 12:42:02 +0100 Subject: [PATCH] Support for runtime permissios, to read/write local storage for settings/torrents. Fixes #258. --- app/build.gradle | 2 +- .../app/settings/SettingsPersistence.java | 25 +++--- .../transdroid/core/gui/TorrentsActivity.java | 18 +++++ .../core/gui/navigation/NavigationHelper.java | 80 +++++++++++++++++++ .../gui/settings/SystemSettingsActivity.java | 66 ++++++++++----- app/src/main/res/values/strings.xml | 6 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 8 files changed, 166 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 98f7741a..0f60ed1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ android { } } lintOptions { - disable 'MissingTranslation', 'ExtraTranslation' + disable 'MissingTranslation', 'ExtraTranslation', 'StringFormatInvalid' } } diff --git a/app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java b/app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java index 74efa74b..d38d4237 100644 --- a/app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java +++ b/app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java @@ -134,9 +134,9 @@ public class SettingsPersistence { if (server.has("new_torrent_alarm")) editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm")); if (server.has("alarm_filter_exclude")) - editor.putBoolean("server_alarmexclude_" + postfix, server.getBoolean("alarm_filter_exclude")); + editor.putString("server_alarmexclude_" + postfix, server.getString("alarm_filter_exclude")); if (server.has("alarm_filter_include")) - editor.putBoolean("server_alarminclude_" + postfix, server.getBoolean("alarm_filter_include")); + editor.putString("server_alarminclude_" + postfix, server.getString("alarm_filter_include")); } } @@ -174,11 +174,13 @@ public class SettingsPersistence { if (feed.has("new_item_alarm")) editor.putBoolean("rssfeed_alarmnew_" + postfix, feed.getBoolean("new_item_alarm")); if (feed.has("alarm_filter_include")) - editor.putBoolean("rssfeed_alarminclude_" + postfix, feed.getBoolean("alarm_filter_include")); + editor.putString("rssfeed_include_" + postfix, feed.getString("alarm_filter_include")); if (feed.has("alarm_filter_exclude")) - editor.putBoolean("rssfeed_alarmexclude_" + postfix, feed.getBoolean("alarm_filter_exclude")); - if (feed.has("last_seen")) - editor.putString("rssfeed_lastnew_" + postfix, feed.getString("last_seen")); + editor.putString("rssfeed_exclude_" + postfix, feed.getString("alarm_filter_exclude")); + if (feed.has("last_seen_time")) + editor.putLong("rssfeed_lastviewed_" + postfix, feed.getLong("last_seen_time")); + if (feed.has("last_seen_item")) + editor.putString("rssfeed_lastvieweditemurl_" + postfix, feed.getString("last_seen_item")); } } @@ -280,8 +282,8 @@ public class SettingsPersistence { server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null)); server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false)); server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false)); - server.put("alarm_filter_exclude", prefs.getBoolean("server_alarmexclude_" + postfixi, false)); - server.put("alarm_filter_include", prefs.getBoolean("server_alarminclude_" + postfixi, false)); + server.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixi, null)); + server.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixi, null)); servers.put(server); i++; @@ -317,9 +319,10 @@ public class SettingsPersistence { feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null)); feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false)); feed.put("new_item_alarm", prefs.getBoolean("rssfeed_alarmnew_" + postfixk, false)); - feed.put("alarm_filter_exclude", prefs.getBoolean("server_alarmexclude_" + postfixk, false)); - feed.put("alarm_filter_include", prefs.getBoolean("server_alarminclude_" + postfixk, false)); - feed.put("last_seen", prefs.getString("rssfeed_lastnew_" + postfixk, null)); + feed.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixk, null)); + feed.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixk, null)); + feed.put("last_seen_time", prefs.getLong("rssfeed_lastviewed_" + postfixk, -1)); + feed.put("last_seen_item", prefs.getString("rssfeed_lastvieweditemurl_" + postfixk, null)); feeds.put(feed); k++; diff --git a/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java b/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java index 5cd615fe..7192c34c 100644 --- a/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java @@ -24,6 +24,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; @@ -215,6 +216,9 @@ public class TorrentsActivity extends AppCompatActivity implements TorrentTasksE // Auto refresh task private AsyncTask autoRefreshTask; + private String awaitingAddLocalFile; + private String awaitingAddTitle; + @Override public void onCreate(Bundle savedInstanceState) { // Set the theme according to the user preference @@ -739,6 +743,14 @@ public class TorrentsActivity extends AppCompatActivity implements TorrentTasksE return true; } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (awaitingAddLocalFile != null && awaitingAddTitle != null && + Boolean.TRUE.equals(navigationHelper.handleTorrentReadPermissionResult(requestCode, grantResults))) { + addTorrentByFile(awaitingAddLocalFile, awaitingAddTitle); + } + } + @Click(R.id.addmenu_link_button) protected void startUrlEntryDialog() { addmenuButton.collapse(); @@ -1043,6 +1055,12 @@ public class TorrentsActivity extends AppCompatActivity implements TorrentTasksE @Background protected void addTorrentByFile(String localFile, String title) { + if (!navigationHelper.checkTorrentReadPermission(this)) { + // No read permission yet (which we get the result of in onRequestPermissionsResult) + awaitingAddLocalFile = localFile; + awaitingAddTitle = title; + return; + } DaemonTaskResult result = AddByFileTask.create(currentConnection, localFile).execute(log); if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title)); diff --git a/app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java b/app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java index 71ead2b6..f70c16fc 100644 --- a/app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java +++ b/app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java @@ -16,15 +16,25 @@ */ package org.transdroid.core.gui.navigation; +import android.Manifest; import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.style.TypefaceSpan; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache; import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator; import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache; @@ -50,10 +60,80 @@ import java.util.List; @EBean public class NavigationHelper { + private static final int REQUEST_TORRENT_READ_PERMISSION = 0; + private static final int REQUEST_SETTINGS_READ_PERMISSION = 1; + private static final int REQUEST_SETTINGS_WRITE_PERMISSION = 2; + private static ImageLoader imageCache; @RootContext protected Context context; + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean checkTorrentReadPermission(final Activity activity) { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || + checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_TORRENT_READ_PERMISSION); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean checkSettingsReadPermission(final Activity activity) { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || + checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_SETTINGS_READ_PERMISSION); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean checkSettingsWritePermission(final Activity activity) { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || + checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_SETTINGS_WRITE_PERMISSION); + } + + private boolean checkPermission(final Activity activity, final String permission, final int requestCode) { + if (hasPermission(permission)) + // Permission already granted + return true; + if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + // Never asked again: show a dialog with an explanation + new MaterialDialog.Builder(context).content(R.string.permission_readtorrent).positiveText(android.R.string.ok) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode); + } + }).show(); + return false; + } + // Permission not granted (and we asked for it already before) + ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_TORRENT_READ_PERMISSION); + return false; + } + + private boolean hasPermission(String requiredPermission) { + return ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED; + } + + public Boolean handleTorrentReadPermissionResult(int requestCode, int[] grantResults) { + if (requestCode == REQUEST_TORRENT_READ_PERMISSION) { + // Return permission granting result + return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + } + return null; + } + + public Boolean handleSettingsReadPermissionResult(int requestCode, int[] grantResults) { + if (requestCode == REQUEST_SETTINGS_READ_PERMISSION) { + // Return permission granting result + return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + } + return null; + } + + public Boolean handleSettingsWritePermissionResult(int requestCode, int[] grantResults) { + if (requestCode == REQUEST_SETTINGS_WRITE_PERMISSION) { + // Return permission granting result + return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + } + return null; + } + /** * Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font * @param string A plain text {@link String} diff --git a/app/src/main/java/org/transdroid/core/gui/settings/SystemSettingsActivity.java b/app/src/main/java/org/transdroid/core/gui/settings/SystemSettingsActivity.java index 1c46164b..82da4cbb 100644 --- a/app/src/main/java/org/transdroid/core/gui/settings/SystemSettingsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/settings/SystemSettingsActivity.java @@ -28,8 +28,8 @@ 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 android.support.annotation.NonNull; import android.text.TextUtils; import com.nispok.snackbar.Snackbar; @@ -81,6 +81,7 @@ public class SystemSettingsActivity extends PreferenceCompatActivity { protected ErrorLogSender errorLogSender; @Bean protected SettingsPersistence settingsPersistence; + private OnPreferenceClickListener onCheckUpdatesClick = new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { @@ -103,19 +104,12 @@ public class SystemSettingsActivity extends PreferenceCompatActivity { private OnClickListener importSettingsFromFile = new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this); - try { - settingsPersistence.importSettingsFromFile(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE); - SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.pref_import_success)); - } catch (FileNotFoundException e) { - SnackbarManager - .show(Snackbar.with(SystemSettingsActivity.this).text(R.string.error_file_not_found).colorResource(R.color.red)); - } catch (JSONException e) { - SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this) - .text(getString(R.string.error_no_valid_settings_file, getString(R.string.app_name))).colorResource(R.color.red)); - } + if (!navigationHelper.checkSettingsReadPermission(SystemSettingsActivity.this)) + return; // We are requesting permission to access file storage + importSettingsFromFile(); } }; + private OnClickListener importSettingsFromQr = new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -125,16 +119,12 @@ public class SystemSettingsActivity extends PreferenceCompatActivity { private OnClickListener exportSettingsToFile = new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this); - try { - settingsPersistence.exportSettingsToFile(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE); - SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.pref_export_success)); - } catch (JSONException | IOException e) { - SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.error_cant_write_settings_file) - .colorResource(R.color.red)); - } + if (!navigationHelper.checkSettingsWritePermission(SystemSettingsActivity.this)) + return; // We are requesting permission to access file storage + exportSettingsToFile(); } }; + private OnClickListener exportSettingsToQr = new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -176,8 +166,42 @@ public class SystemSettingsActivity extends PreferenceCompatActivity { MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (Boolean.TRUE.equals(navigationHelper.handleSettingsReadPermissionResult(requestCode, grantResults))) { + importSettingsFromFile(); + } else if (Boolean.TRUE.equals(navigationHelper.handleSettingsWritePermissionResult(requestCode, grantResults))) { + exportSettingsToFile(); + } + } + + private void importSettingsFromFile() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this); + try { + settingsPersistence.importSettingsFromFile(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE); + SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.pref_import_success)); + } catch (FileNotFoundException e) { + SnackbarManager + .show(Snackbar.with(SystemSettingsActivity.this).text(R.string.error_file_not_found).colorResource(R.color.red)); + } catch (JSONException e) { + SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this) + .text(getString(R.string.error_no_valid_settings_file, getString(R.string.app_name))).colorResource(R.color.red)); + } + } + + private void exportSettingsToFile() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this); + try { + settingsPersistence.exportSettingsToFile(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE); + SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.pref_export_success)); + } catch (JSONException | IOException e) { + SnackbarManager.show(Snackbar.with(SystemSettingsActivity.this).text(R.string.error_cant_write_settings_file) + .colorResource(R.color.red)); + } + } + @OnActivityResult(BarcodeHelper.ACTIVITY_BARCODE_QRSETTINGS) - public void onQrCodeScanned(int resultCode, Intent data) { + public void onQrCodeScanned(@SuppressWarnings("UnusedParameters") int resultCode, Intent data) { // We should have received Intent extras with the QR-decoded data representing Transdroid settings if (data == null || !data.hasExtra("SCAN_RESULT")) return; // Cancelled scan; ignore diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04614782..a8181e78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,7 +435,11 @@ 43200 86400 - + + Transdroid requires read access to your file storage in order to read local .torrent files + Transdroid requires read access to your file storage if you want to read from a local settings file + Transdroid requires write access to your file storage to write the local settings file + Error during communication; check your connection Your torrent client does not support this operation Internal error building request diff --git a/build.gradle b/build.gradle index dca0e3b0..955f46a3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:2.0.0-alpha5' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ee69fb3..2890231d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Dec 09 09:45:38 CET 2014 +#Wed Jan 20 12:20:00 CET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip