mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-17 04:00:39 +01:00
Implemented runtime permission handling using Dexter library, minor fixes
This commit is contained in:
parent
915a659b5d
commit
5d18929258
@ -79,6 +79,7 @@ dependencies {
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.kluent
|
||||
implementation 'com.karumi:dexter:6.1.2'
|
||||
}
|
||||
|
||||
jacoco {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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<File> dirs, Collection<File> doNotDelete)
|
||||
private void deleteEmptyDirs(Iterable<File> dirs, Collection<File> 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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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<PermissionRequest> 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -395,6 +395,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">Album Cover</string>
|
||||
<string name="common_multiple_years">Mehrere Jahre</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 Titel</item>
|
||||
<item quantity="other">%d Titel</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">Caratula del Álbum</string>
|
||||
<string name="common_multiple_years">Múltiples años</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 canción</item>
|
||||
<item quantity="other">%d canciones</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">albumArt</string>
|
||||
<string name="common_multiple_years">Multiple Years</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">Un titre</item>
|
||||
<item quantity="other">%d titres</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">albumArt</string>
|
||||
<string name="common_multiple_years">Multiple Years</string>
|
||||
|
||||
<string name="permissions.access_error">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.</string>
|
||||
<string name="permissions.message_box_title">Figyelem</string>
|
||||
<string name="permissions.permission_missing">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.</string>
|
||||
<string name="permissions.rationale_title">Jogosultság kérés</string>
|
||||
<string name="permissions.rationale_description">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.</string>
|
||||
<string name="permissions.permanent_denial_title">A jogosultság visszautasítva</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Beállítások megnyitása</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 dal</item>
|
||||
<item quantity="other">%d dal</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">Albumhoes</string>
|
||||
<string name="common_multiple_years">Meerdere jaren</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 nummer</item>
|
||||
<item quantity="other">%d nummers</item>
|
||||
|
@ -396,6 +396,16 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">Okładka</string>
|
||||
<string name="common_multiple_years">Z różnych lat</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 utwór</item>
|
||||
<item quantity="few">%d utwory</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">albumArt</string>
|
||||
<string name="common_multiple_years">Múltiplos Anos</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 música</item>
|
||||
<item quantity="other">%d músicas</item>
|
||||
|
@ -396,6 +396,16 @@
|
||||
<string name="settings.image_loader_concurrency_12">12</string>
|
||||
<string name="albumArt">albumArt</string>
|
||||
<string name="common_multiple_years">Múltiplos Anos</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">%d música</item>
|
||||
<item quantity="other">%d músicas</item>
|
||||
|
@ -400,6 +400,15 @@
|
||||
<string name="common_multiple_years">Multiple Years</string>
|
||||
<string name="settings.server_address_unset" translatable="false">http://example.com</string>
|
||||
|
||||
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
|
||||
<string name="permissions.message_box_title">Warning</string>
|
||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||
<string name="permissions.rationale_title">Permission request</string>
|
||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||
<string name="permissions.permanent_denial_description">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.</string>
|
||||
<string name="permissions.open_settings">Open settings</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 song</item>
|
||||
<item quantity="other">%d songs</item>
|
||||
|
Loading…
x
Reference in New Issue
Block a user