Implemented runtime permission handling using Dexter library, minor fixes

This commit is contained in:
Nite 2020-06-02 15:35:36 +02:00
parent 915a659b5d
commit 5d18929258
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
19 changed files with 312 additions and 39 deletions

View File

@ -79,6 +79,7 @@ dependencies {
testImplementation testing.kotlinJunit
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
implementation 'com.karumi:dexter:6.1.2'
}
jacoco {

View File

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

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

@ -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

View File

@ -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();
}
}

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>