From 5d189292582aeede47c711b189aaa5b1eadc1055 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 2 Jun 2020 15:35:36 +0200 Subject: [PATCH] Implemented runtime permission handling using Dexter library, minor fixes --- ultrasonic/build.gradle | 1 + .../ultrasonic/activity/DownloadActivity.java | 2 +- .../ultrasonic/fragment/SettingsFragment.java | 27 ++- .../service/OfflineMusicService.java | 2 +- .../ultrasonic/service/RESTMusicService.java | 4 +- .../moire/ultrasonic/util/CacheCleaner.java | 12 +- .../org/moire/ultrasonic/util/FileUtil.java | 41 +++-- .../ultrasonic/util/LegacyImageLoader.java | 5 + .../moire/ultrasonic/util/PermissionUtil.java | 167 ++++++++++++++++++ .../java/org/moire/ultrasonic/util/Util.java | 1 + ultrasonic/src/main/res/values-de/strings.xml | 10 ++ ultrasonic/src/main/res/values-es/strings.xml | 10 ++ ultrasonic/src/main/res/values-fr/strings.xml | 10 ++ ultrasonic/src/main/res/values-hu/strings.xml | 10 ++ ultrasonic/src/main/res/values-nl/strings.xml | 10 ++ ultrasonic/src/main/res/values-pl/strings.xml | 10 ++ .../src/main/res/values-pt-rBR/strings.xml | 10 ++ ultrasonic/src/main/res/values-pt/strings.xml | 10 ++ ultrasonic/src/main/res/values/strings.xml | 9 + 19 files changed, 312 insertions(+), 39 deletions(-) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 4816a8fc..3b023668 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -79,6 +79,7 @@ dependencies { testImplementation testing.kotlinJunit testImplementation testing.mockitoKotlin testImplementation testing.kluent + implementation 'com.karumi:dexter:6.1.2' } jacoco { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java index d96c0b90..28db6a4a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java @@ -1649,7 +1649,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void displaySongRating() { - int rating = currentSong.getUserRating() == null ? 0 : currentSong.getUserRating(); + int rating = currentSong == null || currentSong.getUserRating() == null ? 0 : currentSong.getUserRating(); fiveStar1ImageView.setImageDrawable(rating > 0 ? fullStar : hollowStar); fiveStar2ImageView.setImageDrawable(rating > 1 ? fullStar : hollowStar); fiveStar3ImageView.setImageDrawable(rating > 2 ? fullStar : hollowStar); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 70b3b9b0..26fb5d77 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -382,21 +382,18 @@ public class SettingsFragment extends PreferenceFragment File dir = new File(path); if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) { - Util.toast(getActivity(), R.string.settings_cache_location_error, false); - - // Reset it to the default. - String defaultPath = FileUtil.getDefaultMusicDirectory().getPath(); - if (!defaultPath.equals(path)) { - Util.getPreferences(getActivity()).edit() - .putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath) - .apply(); - cacheLocation.setSummary(defaultPath); - cacheLocation.setText(defaultPath); - } - - // Clear download queue. - DownloadService downloadService = DownloadServiceImpl.getInstance(); - downloadService.clear(); + PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() { + @Override + public void onPermissionRequestFinished() { + String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath()); + cacheLocation.setSummary(currentPath); + cacheLocation.setText(currentPath); + } + }); } + + // Clear download queue. + DownloadService downloadService = DownloadServiceImpl.getInstance(); + downloadService.clear(); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index 8a93dec0..ea15f151 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -339,7 +339,7 @@ public class OfflineMusicService extends RESTMusicService { try { - Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality); + Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality); return Util.scaleBitmap(bitmap, size); } catch (Exception e) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index e090caa2..c45c7fce 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -1013,7 +1013,7 @@ public class RESTMusicService implements MusicService { synchronized (username) { // Use cached file, if existing. - Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality); + Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality); if (bitmap == null) { InputStream in = null; @@ -1031,7 +1031,7 @@ public class RESTMusicService implements MusicService { OutputStream out = null; try { - out = new FileOutputStream(FileUtil.getAvatarFile(username)); + out = new FileOutputStream(FileUtil.getAvatarFile(context, username)); out.write(bytes); } finally { Util.close(out); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index debc66c3..c3248aa3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -47,7 +47,7 @@ public class CacheCleaner catch (Exception ex) { // If an exception is thrown, assume we execute correctly the next time - Log.w("Exception in CacheCleaner.clean", ex); + Log.w(TAG, "Exception in CacheCleaner.clean", ex); } } @@ -60,7 +60,7 @@ public class CacheCleaner catch (Exception ex) { // If an exception is thrown, assume we execute correctly the next time - Log.w("Exception in CacheCleaner.cleanSpace", ex); + Log.w(TAG,"Exception in CacheCleaner.cleanSpace", ex); } } @@ -73,11 +73,11 @@ public class CacheCleaner catch (Exception ex) { // If an exception is thrown, assume we execute correctly the next time - Log.w("Exception in CacheCleaner.cleanPlaylists", ex); + Log.w(TAG, "Exception in CacheCleaner.cleanPlaylists", ex); } } - private static void deleteEmptyDirs(Iterable dirs, Collection doNotDelete) + private void deleteEmptyDirs(Iterable dirs, Collection doNotDelete) { for (File dir : dirs) { @@ -91,9 +91,9 @@ public class CacheCleaner if (children != null) { // No songs left in the folder - if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) + if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(context, dir).getPath())) { - Util.delete(FileUtil.getAlbumArtFile(dir)); + Util.delete(FileUtil.getAlbumArtFile(context, dir)); children = dir.listFiles(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index fdcf9abd..57c7af18 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -21,10 +21,12 @@ package org.moire.ultrasonic.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Build; import android.os.Environment; import android.text.TextUtils; import android.util.Log; +import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.domain.MusicDirectory; @@ -106,12 +108,12 @@ public class FileUtil public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { File albumDir = getAlbumDirectory(context, entry); - return getAlbumArtFile(albumDir); + return getAlbumArtFile(context, albumDir); } - public static File getAvatarFile(String username) + public static File getAvatarFile(Context context, String username) { - File albumArtDir = getAlbumArtDirectory(); + File albumArtDir = getAlbumArtDirectory(context); if (albumArtDir == null || username == null) { @@ -122,9 +124,9 @@ public class FileUtil return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); } - public static File getAlbumArtFile(File albumDir) + public static File getAlbumArtFile(Context context, File albumDir) { - File albumArtDir = getAlbumArtDirectory(); + File albumArtDir = getAlbumArtDirectory(context); if (albumArtDir == null || albumDir == null) { @@ -135,11 +137,11 @@ public class FileUtil return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); } - public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality) + public static Bitmap getAvatarBitmap(Context context, String username, int size, boolean highQuality) { if (username == null) return null; - File avatarFile = getAvatarFile(username); + File avatarFile = getAvatarFile(context, username); SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance(); Bitmap bitmap = null; @@ -299,7 +301,7 @@ public class FileUtil return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); } - public static File getAlbumArtDirectory() + public static File getAlbumArtDirectory(Context context) { File albumArtDir = new File(getUltraSonicDirectory(), "artwork"); ensureDirectoryExistsAndIsReadWritable(albumArtDir); @@ -316,10 +318,13 @@ public class FileUtil File dir; - if (!TextUtils.isEmpty(entry.getPath())) { + if (!TextUtils.isEmpty(entry.getPath())) + { File f = new File(fileSystemSafeDir(entry.getPath())); dir = new File(String.format("%s/%s", getMusicDirectory(context).getPath(), entry.isDirectory() ? f.getPath() : f.getParent())); - } else { + } + else + { String artist = fileSystemSafe(entry.getArtist()); String album = fileSystemSafe(entry.getAlbum()); @@ -360,7 +365,11 @@ public class FileUtil public static File getUltraSonicDirectory() { - return new File(Environment.getExternalStorageDirectory(), "Android/data/org.moire.ultrasonic"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return new File(Environment.getExternalStorageDirectory(), "Android/data/org.moire.ultrasonic"); + + // After Android M, the location of the files must be queried differently. GetExternalFilesDir will always return a directory which Ultrasonic can access without any extra privileges. + return MainActivity.getInstance().getExternalFilesDir(null); } public static File getDefaultMusicDirectory() @@ -372,7 +381,11 @@ public class FileUtil { String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath()); File dir = new File(path); - return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : DEFAULT_MUSIC_DIR; + + boolean hasAccess = ensureDirectoryExistsAndIsReadWritable(dir); + if (hasAccess == false) PermissionUtil.handlePermissionFailed(context, null); + + return hasAccess ? dir : DEFAULT_MUSIC_DIR; } public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) @@ -406,13 +419,13 @@ public class FileUtil if (!dir.canRead()) { Log.w(TAG, String.format("No read permission for directory %s", dir)); - return false; + return false; } if (!dir.canWrite()) { Log.w(TAG, String.format("No write permission for directory %s", dir)); - return false; + return false; } return true; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java index ae1fc517..ebc3867d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java @@ -427,6 +427,11 @@ public class LegacyImageLoader implements Runnable, ImageLoader { ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); + if (bitmap == null) { + Log.d(TAG, "Found empty album art."); + return; + } + if (isAvatar) addImageToCache(bitmap, username, size); else diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java new file mode 100644 index 00000000..00a04e82 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java @@ -0,0 +1,167 @@ +package org.moire.ultrasonic.util; + +import android.Manifest; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.PermissionChecker; + +import com.karumi.dexter.Dexter; +import com.karumi.dexter.MultiplePermissionsReport; +import com.karumi.dexter.PermissionToken; +import com.karumi.dexter.listener.DexterError; +import com.karumi.dexter.listener.PermissionRequest; +import com.karumi.dexter.listener.PermissionRequestErrorListener; +import com.karumi.dexter.listener.multi.MultiplePermissionsListener; + +import org.moire.ultrasonic.R; +import org.moire.ultrasonic.activity.MainActivity; + +import java.util.List; + +import static androidx.core.content.PermissionChecker.PERMISSION_DENIED; + + +/** + * Contains static functions for Permission handling + */ +public class PermissionUtil { + + private static final String TAG = FileUtil.class.getSimpleName(); + + public interface PermissionRequestFinishedCallback { + void onPermissionRequestFinished(); + } + + /** + * This function can be used to handle file access permission failures. + * + * It will check if the failure is because the necessary permissions aren't available, + * and it will request them, if necessary. + * + * @param context context for the operation + * @param callback callback function to execute after the permission request is finished + */ + public static void handlePermissionFailed(final Context context, final PermissionRequestFinishedCallback callback) { + + String currentCachePath = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath()); + String defaultCachePath = FileUtil.getDefaultMusicDirectory().getPath(); + + // Ultrasonic can do nothing about this error when the Music Directory is already set to the default. + if (currentCachePath.compareTo(defaultCachePath) == 0) return; + + // We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available + Context mainContext = MainActivity.getInstance(); + + if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) || + (PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) { + // While we request permission, the Music Directory is temporarily reset to its default location + setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); + requestPermission(mainContext, currentCachePath, callback); + } else { + setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); + showWarning(mainContext,"Warning", context.getString(R.string.permissions_access_error), null); + callback.onPermissionRequestFinished(); + } + } + + private static void setCacheLocation(Context context, String cacheLocation) { + Util.getPreferences(context).edit() + .putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, cacheLocation) + .apply(); + } + + private static void requestPermission(final Context context, final String cacheLocation, final PermissionRequestFinishedCallback callback) { + Dexter.withContext(context) + .withPermissions( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE) + .withListener(new MultiplePermissionsListener() { + @Override + public void onPermissionsChecked(MultiplePermissionsReport report) { + if (report.areAllPermissionsGranted()) { + Log.i(TAG, String.format("Permission granted to use cache directory %s", cacheLocation)); + setCacheLocation(context, cacheLocation); + if (callback != null) callback.onPermissionRequestFinished(); + return; + } + + if (report.isAnyPermissionPermanentlyDenied()) { + Log.i(TAG, String.format("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation)); + showSettingsDialog(context); + if (callback != null) callback.onPermissionRequestFinished(); + return; + } + + Log.i(TAG, String.format("At least one permission is missing to use directory %s ", cacheLocation)); + setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); + showWarning(context, context.getString(R.string.permissions_message_box_title), + context.getString(R.string.permissions_permission_missing), null); + if (callback != null) callback.onPermissionRequestFinished(); + } + + @Override + public void onPermissionRationaleShouldBeShown(List permissions, PermissionToken token) { + showWarning(context, context.getString(R.string.permissions_rationale_title), + context.getString(R.string.permissions_rationale_description), token); + } + }).withErrorListener(new PermissionRequestErrorListener() { + @Override + public void onError(DexterError error) { + Log.e(TAG, String.format("An error has occurred during checking permissions with Dexter: %s", error.toString())); + } + }) + .check(); + } + + private static void showSettingsDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setTitle(context.getString(R.string.permissions_permanent_denial_title)); + builder.setMessage(context.getString(R.string.permissions_permanent_denial_description)); + + builder.setPositiveButton(context.getString(R.string.permissions_open_settings), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + openSettings(context); + } + }); + + builder.setNegativeButton(context.getString(R.string.common_cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); + dialog.cancel(); + } + }); + + builder.show(); + } + + private static void openSettings(Context context) { + Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.setData(Uri.parse("package:" + context.getPackageName())); + context.startActivity(i); + } + + private static void showWarning(Context context, String title, String text, final PermissionToken token) { + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setTitle(title); + builder.setMessage(text); + builder.setPositiveButton(context.getString(R.string.common_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + if (token != null) token.continuePermissionRequest(); + } + }); + builder.show(); + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 5259037d..578ae72e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -882,6 +882,7 @@ public class Util extends DownloadActivity public static Bitmap scaleBitmap(Bitmap bitmap, int size) { + if (bitmap == null) return null; return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true); } diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index b750ea01..fbb9332a 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -395,6 +395,16 @@ 12 Album Cover Mehrere Jahre + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 Titel %d Titel diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 5adb4aed..ca46dbdd 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -396,6 +396,16 @@ 12 Caratula del Álbum Múltiples años + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 canción %d canciones diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 19dcdc5b..c82ff2ad 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -396,6 +396,16 @@ 12 albumArt Multiple Years + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + Un titre %d titres diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index cfd1ae60..b2568d39 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -396,6 +396,16 @@ 12 albumArt Multiple Years + + Az Ultrasonic nem éri el a zenei fájl gyorsítótárat. A gyorsítótár helye visszaállítva az alapbeállításra. + Figyelem + Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz. A gyorsítótár helye visszaállítva az alapbeállításra. + Jogosultság kérés + Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz.\nKérjük, engedélyezd a hozzáférést a fájlrendszerhez. + A jogosultság visszautasítva + Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz. Ez a beállítás az alkalmazásbeállítások között módosítható. Ha elutasítod ezt a kérést, a gyorsítótár helye visszaáll az alapbeállításra. + Beállítások megnyitása + 1 dal %d dal diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 0b416312..b9fa2a51 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -396,6 +396,16 @@ 12 Albumhoes Meerdere jaren + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 nummer %d nummers diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 9148a556..2fdd5466 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -396,6 +396,16 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników 12 Okładka Z różnych lat + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 utwór %d utwory diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index f210d9b1..df2f05d8 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -396,6 +396,16 @@ 12 albumArt Múltiplos Anos + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 música %d músicas diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 90082b7c..5ed2e4bb 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -396,6 +396,16 @@ 12 albumArt Múltiplos Anos + + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + %d música %d músicas diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index e4d9e28c..4a2b453e 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -400,6 +400,15 @@ Multiple Years http://example.com + Ultrasonic can\'t access the music file cache. Cache location was reset to the default path. + Warning + Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value. + Permission request + Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem. + Permissions permanently denied + Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value. + Open settings + 1 song %d songs