diff --git a/detekt-config.yml b/detekt-config.yml
index eb76d7f7..c9117d88 100644
--- a/detekt-config.yml
+++ b/detekt-config.yml
@@ -70,7 +70,7 @@ style:
excludeImportStatements: false
MagicNumber:
# 100 common in percentage, 1000 in milliseconds
- ignoreNumbers: ['-1', '0', '1', '2', '100', '1000']
+ ignoreNumbers: ['-1', '0', '1', '2', '10', '100', '256', '512', '1000', '1024']
ignoreEnums: true
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CancellableTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CancellableTask.java
index da7b6bb9..6c70339d 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CancellableTask.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CancellableTask.java
@@ -18,11 +18,11 @@
*/
package org.moire.ultrasonic.util;
-import timber.log.Timber;
-
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import timber.log.Timber;
+
/**
* @author Sindre Mehus
* @version $Id$
@@ -93,7 +93,7 @@ public abstract class CancellableTask
thread.get().start();
}
- public static interface OnCancelListener
+ public interface OnCancelListener
{
void onCancel();
}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java
deleted file mode 100644
index 65d80be3..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see .
-
- Copyright 2009 (C) Sindre Mehus
- */
-package org.moire.ultrasonic.util;
-
-/**
- * @author Sindre Mehus
- * @version $Id$
- */
-public final class Constants
-{
-
- // Character encoding used throughout.
- public static final String UTF_8 = "UTF-8";
-
- // REST protocol version and client ID.
- // Note: Keep it as low as possible to maintain compatibility with older servers.
- public static final String REST_PROTOCOL_VERSION = "1.7.0";
- public static final String REST_CLIENT_ID = "Ultrasonic";
-
- // Names for intent extras.
- public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
- public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
- public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist";
- public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title";
- public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall";
- public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query";
- public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id";
- public static final String INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id";
- public static final String INTENT_EXTRA_NAME_PARENT_ID = "subsonic.parent.id";
- public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name";
- public static final String INTENT_EXTRA_NAME_SHARE_ID = "subsonic.share.id";
- public static final String INTENT_EXTRA_NAME_SHARE_NAME = "subsonic.share.name";
- public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
- public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TITLE = "subsonic.albumlisttitle";
- public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
- public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
- public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
- public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh";
- public static final String INTENT_EXTRA_NAME_STARRED = "subsonic.starred";
- public static final String INTENT_EXTRA_NAME_RANDOM = "subsonic.random";
- public static final String INTENT_EXTRA_NAME_GENRE_NAME = "subsonic.genre";
- public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum";
- public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos";
- public static final String INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer";
- public static final String INTENT_EXTRA_NAME_APPEND = "subsonic.append";
-
- // Names for Intent Actions
- public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE";
- public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY";
- public static final String CMD_RESUME_OR_PLAY = "org.moire.ultrasonic.CMD_RESUME_OR_PLAY";
- public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE";
- public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE";
- public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP";
- public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS";
- public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT";
-
- // Preferences keys.
- public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
- public static final String PREFERENCES_KEY_SERVERS_EDIT = "editServers";
- public static final String PREFERENCES_KEY_THEME = "theme";
- public static final String PREFERENCES_KEY_THEME_LIGHT = "light";
- public static final String PREFERENCES_KEY_THEME_DARK = "dark";
- public static final String PREFERENCES_KEY_THEME_BLACK = "black";
- public static final String PREFERENCES_KEY_DISPLAY_BITRATE_WITH_ARTIST = "displayBitrateWithArtist";
- public static final String PREFERENCES_KEY_USE_FOLDER_FOR_ALBUM_ARTIST = "useFolderForAlbumArtist";
- public static final String PREFERENCES_KEY_SHOW_TRACK_NUMBER = "showTrackNumber";
- public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
- public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
- public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
- public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
- public static final String PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount";
- public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
- public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
- public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
- public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
- public static final String PREFERENCES_KEY_SERVER_SCALING = "serverScaling";
- public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
- public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
- public static final String PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength";
- public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
- public static final String PREFERENCES_KEY_SHOW_NOTIFICATION = "showNotification";
- public static final String PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION = "alwaysShowNotification";
- public static final String PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS = "showLockScreen";
- public static final String PREFERENCES_KEY_MAX_ALBUMS = "maxAlbums";
- public static final String PREFERENCES_KEY_MAX_SONGS = "maxSongs";
- public static final String PREFERENCES_KEY_MAX_ARTISTS = "maxArtists";
- public static final String PREFERENCES_KEY_DEFAULT_ALBUMS = "defaultAlbums";
- public static final String PREFERENCES_KEY_DEFAULT_SONGS = "defaultSongs";
- public static final String PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists";
- public static final String PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying";
- public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback";
- public static final String PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS = "playbackControlSettings";
- public static final String PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory";
- public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay";
- public static final String PREFERENCES_KEY_INCREMENT_TIME = "incrementTime";
- public static final String PREFERENCES_KEY_ID3_TAGS = "useId3Tags";
- public static final String PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture";
- public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss";
- public static final String PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval";
- public static final String PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime";
- public static final String PREFERENCES_KEY_CLEAR_PLAYLIST = "clearPlaylist";
- public static final String PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark";
- public static final String PREFERENCES_KEY_DISC_SORT = "discAndTrackSort";
- public static final String PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications";
- public static final String PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt";
- public static final String PREFERENCES_KEY_DISABLE_SEND_NOW_PLAYING_LIST = "disableNowPlayingListSending";
- public static final String PREFERENCES_KEY_VIEW_REFRESH = "viewRefresh";
- public static final String PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS = "sharingAlwaysAskForDetails";
- public static final String PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION = "sharingDefaultDescription";
- public static final String PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting";
- public static final String PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration";
- public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist";
- public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating";
- public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory";
- public static final String PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted";
- public static final String PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE = "resumeOnBluetoothDevice";
- public static final String PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice";
- public static final String PREFERENCES_KEY_SINGLE_BUTTON_PLAY_PAUSE = "singleButtonPlayPause";
- public static final String PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile";
-
- public static final int PREFERENCE_VALUE_ALL = 0;
- public static final int PREFERENCE_VALUE_A2DP = 1;
- public static final int PREFERENCE_VALUE_DISABLED = 2;
-
- public static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser";
-
- public static final String ALBUM_ART_FILE = "folder.jpeg";
- public static final String STARRED = "starred";
- public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName";
- public static final int RESULT_CLOSE_ALL = 1337;
-
- private Constants()
- {
- }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java
deleted file mode 100644
index cb35f20e..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java
+++ /dev/null
@@ -1,536 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see .
-
- Copyright 2009 (C) Sindre Mehus
- */
-package org.moire.ultrasonic.util;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Environment;
-import android.text.TextUtils;
-
-import org.moire.ultrasonic.app.UApp;
-import org.moire.ultrasonic.domain.MusicDirectory;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.regex.Pattern;
-
-import kotlin.Lazy;
-import timber.log.Timber;
-
-import static org.koin.java.KoinJavaComponent.inject;
-
-/**
- * @author Sindre Mehus
- */
-public class FileUtil
-{
- private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
- private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
- private static final List MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus");
- private static final List VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
- private static final List PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u");
- private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*");
- public static final String SUFFIX_LARGE = ".jpeg";
- public static final String SUFFIX_SMALL = ".jpeg-small";
-
- private static final Lazy permissionUtil = inject(PermissionUtil.class);
-
- public static File getSongFile(MusicDirectory.Entry song)
- {
- File dir = getAlbumDirectory(song);
-
- // Do not generate new name for offline files. Offline files will have their Path as their Id.
- if (!TextUtils.isEmpty(song.getId()))
- {
- if (song.getId().startsWith(dir.getAbsolutePath())) return new File(song.getId());
- }
-
- // Generate a file name for the song
- StringBuilder fileName = new StringBuilder(256);
- Integer track = song.getTrack();
-
- //check if filename already had track number
- if (!TITLE_WITH_TRACK.matcher(song.getTitle()).matches()) {
- if (track != null) {
- if (track < 10) {
- fileName.append('0');
- }
-
- fileName.append(track).append('-');
- }
- }
- fileName.append(fileSystemSafe(song.getTitle())).append('.');
-
- if (!TextUtils.isEmpty(song.getTranscodedSuffix())) {
- fileName.append(song.getTranscodedSuffix());
- } else {
- fileName.append(song.getSuffix());
- }
-
- return new File(dir, fileName.toString());
- }
-
- public static File getPlaylistFile(String server, String name)
- {
- File playlistDir = getPlaylistDirectory(server);
- return new File(playlistDir, String.format("%s.m3u", fileSystemSafe(name)));
- }
-
- public static File getPlaylistDirectory()
- {
- File playlistDir = new File(getUltrasonicDirectory(), "playlists");
- ensureDirectoryExistsAndIsReadWritable(playlistDir);
- return playlistDir;
- }
-
- public static File getPlaylistDirectory(String server)
- {
- File playlistDir = new File(getPlaylistDirectory(), server);
- ensureDirectoryExistsAndIsReadWritable(playlistDir);
- return playlistDir;
- }
-
- /**
- * Get the album art file for a given album entry
- * @param entry The album entry
- * @return File object. Not guaranteed that it exists
- */
- public static File getAlbumArtFile(MusicDirectory.Entry entry)
- {
- File albumDir = getAlbumDirectory(entry);
- return getAlbumArtFile(albumDir);
- }
-
- /**
- * Get the cache key for a given album entry
- * @param entry The album entry
- * @param large Whether to get the key for the large or the default image
- * @return String The hash key
- */
- public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large)
- {
- File albumDir = getAlbumDirectory(entry);
-
- return getAlbumArtKey(albumDir, large);
- }
-
- /**
- * Get the cache key for a given album entry
- * @param albumDir The album directory
- * @param large Whether to get the key for the large or the default image
- * @return String The hash key
- */
- public static String getAlbumArtKey(File albumDir, boolean large)
- {
- if (albumDir == null) {
- return null;
- }
-
- String suffix = (large) ? SUFFIX_LARGE : SUFFIX_SMALL;
-
- return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.getPath()), suffix);
- }
-
-
-
- public static File getAvatarFile(String username)
- {
- File albumArtDir = getAlbumArtDirectory();
-
- if (albumArtDir == null || username == null)
- {
- return null;
- }
-
- String md5Hex = Util.md5Hex(username);
- return new File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE));
- }
-
- /**
- * Get the album art file for a given album directory
- * @param albumDir The album directory
- * @return File object. Not guaranteed that it exists
- */
- public static File getAlbumArtFile(File albumDir)
- {
- File albumArtDir = getAlbumArtDirectory();
- String key = getAlbumArtKey(albumDir, true);
-
- if (key == null || albumArtDir == null)
- {
- return null;
- }
-
- return new File(albumArtDir, key);
- }
-
-
- /**
- * Get the album art file for a given cache key
- * @param cacheKey The key (== the filename)
- * @return File object. Not guaranteed that it exists
- */
- public static File getAlbumArtFile(String cacheKey)
- {
- File albumArtDir = getAlbumArtDirectory();
-
- if (albumArtDir == null || cacheKey == null)
- {
- return null;
- }
-
- return new File(albumArtDir, cacheKey);
- }
-
-
- public static File getAlbumArtDirectory()
- {
- File albumArtDir = new File(getUltrasonicDirectory(), "artwork");
- ensureDirectoryExistsAndIsReadWritable(albumArtDir);
- ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
- return albumArtDir;
- }
-
- public static File getAlbumDirectory(MusicDirectory.Entry entry)
- {
- if (entry == null)
- {
- return null;
- }
-
- File dir;
-
- if (!TextUtils.isEmpty(entry.getPath()))
- {
- File f = new File(fileSystemSafeDir(entry.getPath()));
- dir = new File(String.format("%s/%s", getMusicDirectory().getPath(), entry.isDirectory() ? f.getPath() : f.getParent()));
- }
- else
- {
- String artist = fileSystemSafe(entry.getArtist());
- String album = fileSystemSafe(entry.getAlbum());
-
- if ("unnamed".equals(album))
- {
- album = fileSystemSafe(entry.getTitle());
- }
-
- dir = new File(String.format("%s/%s/%s", getMusicDirectory().getPath(), artist, album));
- }
-
- return dir;
- }
-
- public static void createDirectoryForParent(File file)
- {
- File dir = file.getParentFile();
- if (!dir.exists())
- {
- if (!dir.mkdirs())
- {
- Timber.e("Failed to create directory %s", dir);
- }
- }
- }
-
- private static File getOrCreateDirectory(String name)
- {
- File dir = new File(getUltrasonicDirectory(), name);
-
- if (!dir.exists() && !dir.mkdirs())
- {
- Timber.e("Failed to create %s", name);
- }
-
- return dir;
- }
-
- public static File getUltrasonicDirectory()
- {
- 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 UApp.Companion.applicationContext().getExternalFilesDir(null);
- }
-
- public static File getDefaultMusicDirectory()
- {
- return getOrCreateDirectory("music");
- }
-
- public static File getMusicDirectory()
- {
- File defaultMusicDirectory = getDefaultMusicDirectory();
- String path = Settings.getPreferences().getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultMusicDirectory.getPath());
- File dir = new File(path);
-
- boolean hasAccess = ensureDirectoryExistsAndIsReadWritable(dir);
- if (!hasAccess) permissionUtil.getValue().handlePermissionFailed(null);
-
- return hasAccess ? dir : defaultMusicDirectory;
- }
-
- public static boolean ensureDirectoryExistsAndIsReadWritable(File dir)
- {
- if (dir == null)
- {
- return false;
- }
-
- if (dir.exists())
- {
- if (!dir.isDirectory())
- {
- Timber.w("%s exists but is not a directory.", dir);
- return false;
- }
- }
- else
- {
- if (dir.mkdirs())
- {
- Timber.i("Created directory %s", dir);
- }
- else
- {
- Timber.w("Failed to create directory %s", dir);
- return false;
- }
- }
-
- if (!dir.canRead())
- {
- Timber.w("No read permission for directory %s", dir);
- return false;
- }
-
- if (!dir.canWrite())
- {
- Timber.w("No write permission for directory %s", dir);
- return false;
- }
-
- return true;
- }
-
- /**
- * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
- * with dashes ("-").
- *
- * @param filename The filename in question.
- * @return The filename with special characters replaced by hyphens.
- */
- private static String fileSystemSafe(String filename)
- {
- if (filename == null || filename.trim().isEmpty())
- {
- return "unnamed";
- }
-
- for (String s : FILE_SYSTEM_UNSAFE)
- {
- filename = filename.replace(s, "-");
- }
-
- return filename;
- }
-
- /**
- * Makes a given filename safe by replacing special characters like colons (":")
- * with dashes ("-").
- *
- * @param path The path of the directory in question.
- * @return The the directory name with special characters replaced by hyphens.
- */
- private static String fileSystemSafeDir(String path)
- {
- if (path == null || path.trim().isEmpty())
- {
- return "";
- }
-
- for (String s : FILE_SYSTEM_UNSAFE_DIR)
- {
- path = path.replace(s, "-");
- }
-
- return path;
- }
-
- /**
- * Similar to {@link File#listFiles()}, but returns a sorted set.
- * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
- */
- public static SortedSet listFiles(File dir)
- {
- File[] files = dir.listFiles();
-
- if (files == null)
- {
- Timber.w("Failed to list children for %s", dir.getPath());
- return new TreeSet<>();
- }
-
- return new TreeSet<>(Arrays.asList(files));
- }
-
- public static SortedSet listMediaFiles(File dir)
- {
- SortedSet files = listFiles(dir);
- Iterator iterator = files.iterator();
-
- while (iterator.hasNext())
- {
- File file = iterator.next();
-
- if (!file.isDirectory() && !isMediaFile(file))
- {
- iterator.remove();
- }
- }
-
- return files;
- }
-
- private static boolean isMediaFile(File file)
- {
- String extension = getExtension(file.getName());
- return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension);
- }
-
- public static boolean isPlaylistFile(File file)
- {
- String extension = getExtension(file.getName());
- return PLAYLIST_FILE_EXTENSIONS.contains(extension);
- }
-
- /**
- * Returns the extension (the substring after the last dot) of the given file. The dot
- * is not included in the returned extension.
- *
- * @param name The filename in question.
- * @return The extension, or an empty string if no extension is found.
- */
- public static String getExtension(String name)
- {
- int index = name.lastIndexOf('.');
- return index == -1 ? "" : name.substring(index + 1).toLowerCase();
- }
-
- /**
- * Returns the base name (the substring before the last dot) of the given file. The dot
- * is not included in the returned basename.
- *
- * @param name The filename in question.
- * @return The base name, or an empty string if no basename is found.
- */
- public static String getBaseName(String name)
- {
- int index = name.lastIndexOf('.');
- return index == -1 ? name : name.substring(0, index);
- }
-
- /**
- * Returns the file name of a .partial file of the given file.
- *
- * @param name The filename in question.
- * @return The .partial file name
- */
- public static String getPartialFile(String name)
- {
- return String.format("%s.partial.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
- }
-
- /**
- * Returns the file name of a .complete file of the given file.
- *
- * @param name The filename in question.
- * @return The .complete file name
- */
- public static String getCompleteFile(String name)
- {
- return String.format("%s.complete.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
- }
-
- public static boolean serialize(Context context, T obj, String fileName)
- {
- File file = new File(context.getCacheDir(), fileName);
- ObjectOutputStream out = null;
-
- try
- {
- out = new ObjectOutputStream(new FileOutputStream(file));
- out.writeObject(obj);
- Timber.i("Serialized object to %s", file);
- return true;
- }
- catch (Throwable x)
- {
- Timber.w("Failed to serialize object to %s", file);
- return false;
- }
- finally
- {
- Util.close(out);
- }
- }
-
- @SuppressWarnings({"unchecked"})
- public static T deserialize(Context context, String fileName)
- {
- File file = new File(context.getCacheDir(), fileName);
-
- if (!file.exists() || !file.isFile())
- {
- return null;
- }
-
- ObjectInputStream in = null;
-
- try
- {
- in = new ObjectInputStream(new FileInputStream(file));
- Object object = in.readObject();
- T result = (T) object;
- Timber.i("Deserialized object from %s", file);
- return result;
- }
- catch (Throwable x)
- {
- Timber.w(x,"Failed to deserialize object from %s", file);
- return null;
- }
- finally
- {
- Util.close(in);
- }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
index b58075b0..f2e9a626 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
@@ -307,7 +307,7 @@ class NavigationActivity : AppCompatActivity() {
val editor = preferences.edit()
editor.putString(
Constants.PREFERENCES_KEY_CACHE_LOCATION,
- FileUtil.getDefaultMusicDirectory().path
+ FileUtil.defaultMusicDirectory.path
)
editor.apply()
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
index 2e8b4859..6b7d6d5f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
@@ -18,6 +18,8 @@ import timber.log.Timber
/**
* This class can be used to retrieve the properties of the Active Server
* It caches the settings read up from the DB to improve performance.
+ *
+ * TODO: There seems to be some confusion whether offline id is 0 or -1. Clean this up (carefully!)
*/
class ActiveServerProvider(
private val repository: ServerSettingDao
@@ -30,9 +32,8 @@ class ActiveServerProvider(
* Get the settings of the current Active Server
* @return The Active Server Settings
*/
- fun getActiveServer(): ServerSetting {
- val serverId = getActiveServerId()
-
+ @JvmOverloads
+ fun getActiveServer(serverId: Int = getActiveServerId()): ServerSetting {
if (serverId > 0) {
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
@@ -95,16 +96,29 @@ class ActiveServerProvider(
return cachedDatabase!!
}
+ if (activeServer < 1) {
+ return offlineMetaDatabase
+ }
+
Timber.i("Switching to new database, id:$activeServer")
cachedServerId = activeServer
- val db = Room.databaseBuilder(
+ return Room.databaseBuilder(
UApp.applicationContext(),
MetaDatabase::class.java,
METADATA_DB + cachedServerId
)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
- return db
+ }
+
+ val offlineMetaDatabase: MetaDatabase by lazy {
+ Room.databaseBuilder(
+ UApp.applicationContext(),
+ MetaDatabase::class.java,
+ METADATA_DB + 0
+ )
+ .fallbackToDestructiveMigrationOnDowngrade()
+ .build()
}
@Synchronized
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt
index 2a86d496..a039c1f7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt
@@ -17,6 +17,16 @@ interface ArtistsDao {
@JvmSuppressWildcards
fun set(objects: List)
+ /**
+ * Insert an object in the database.
+ *
+ * @param obj the object to be inserted.
+ * @return The SQLite row id
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ @JvmSuppressWildcards
+ fun insert(obj: Artist): Long
+
/**
* Clear the whole database
*/
@@ -28,4 +38,10 @@ interface ArtistsDao {
*/
@Query("SELECT * FROM artists")
fun get(): List
+
+ /**
+ * Get artist by id
+ */
+ @Query("SELECT * FROM artists WHERE id LIKE :id")
+ fun get(id: String): Artist
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt
index ab69089f..1f3e77f5 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt
@@ -97,6 +97,16 @@ interface GenericDao {
@JvmSuppressWildcards
fun set(objects: List)
+ /**
+ * Insert an object in the database.
+ *
+ * @param obj the object to be inserted.
+ * @return The SQLite row id
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ @JvmSuppressWildcards
+ fun insert(obj: T): Long
+
/**
* Insert an object in the database.
*
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt
index 6abdc85a..638a591b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt
@@ -6,6 +6,10 @@ import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.MusicFolder
+/**
+ * This database is used to store and cache the ID3 metadata
+ */
+
@Database(
entities = [Artist::class, Index::class, MusicFolder::class],
version = 1
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt
index 1dfd564f..607eed30 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt
@@ -14,8 +14,8 @@ import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.S
import java.text.Collator
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.ArtistOrIndex
-import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.imageloader.ImageLoader
+import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Settings
/**
@@ -59,13 +59,14 @@ class ArtistRowAdapter(
if (Settings.shouldShowArtistPicture) {
holder.coverArt.visibility = View.VISIBLE
+ val key = FileUtil.getArtistArtKey(itemList[listPosition].name, false)
imageLoader.loadImage(
- holder.coverArt,
- MusicDirectory.Entry("-1").apply {
- coverArt = holder.coverArtId
- artist = itemList[listPosition].name
- },
- false, 0, R.drawable.ic_contact_picture
+ view = holder.coverArt,
+ id = holder.coverArtId,
+ key = key,
+ large = false,
+ size = 0,
+ defaultResourceId = R.drawable.ic_contact_picture
)
} else {
holder.coverArt.visibility = View.GONE
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt
similarity index 100%
rename from ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.kt
rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt
index 73862c20..4b28e82c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt
@@ -93,10 +93,27 @@ class ImageLoader(
defaultResourceId: Int = R.drawable.unknown_album
) {
val id = entry?.coverArt
+ val key = FileUtil.getAlbumArtKey(entry, large)
+
+ loadImage(view, id, key, large, size, defaultResourceId)
+ }
+
+ /**
+ * Load the cover of a given entry into an ImageView
+ */
+ @JvmOverloads
+ @Suppress("LongParameterList", "ComplexCondition")
+ fun loadImage(
+ view: View?,
+ id: String?,
+ key: String?,
+ large: Boolean,
+ size: Int,
+ defaultResourceId: Int = R.drawable.unknown_album
+ ) {
val requestedSize = resolveSize(size, large)
- if (id != null && id.isNotEmpty() && view is ImageView) {
- val key = FileUtil.getAlbumArtKey(entry, large)
+ if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
val request = ImageRequest.CoverArt(
id, key, view, requestedSize,
placeHolderDrawableRes = defaultResourceId,
@@ -162,8 +179,7 @@ class ImageLoader(
var inputStream: InputStream? = null
try {
inputStream = response.stream
- val bytes = Util.toByteArray(inputStream)
-
+ val bytes = inputStream!!.readBytes()
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(file)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt
index 28be7d2a..0b8d894b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt
@@ -94,7 +94,7 @@ class FileLoggerTree : Timber.DebugTree() {
if (next) fileNum++
file = File(
- FileUtil.getUltrasonicDirectory(),
+ FileUtil.ultrasonicDirectory,
FILENAME.replace("*", fileNum.toString())
)
}
@@ -162,7 +162,7 @@ class FileLoggerTree : Timber.DebugTree() {
}
private fun getLogFileList(): Array? {
- val directory = FileUtil.getUltrasonicDirectory()
+ val directory = FileUtil.ultrasonicDirectory
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt
index 5586103c..2ddf1bc4 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt
@@ -7,10 +7,6 @@
package org.moire.ultrasonic.service
-import android.content.Context
-import android.net.wifi.WifiManager.WifiLock
-import android.os.PowerManager
-import android.os.PowerManager.WakeLock
import android.text.TextUtils
import androidx.lifecycle.MutableLiveData
import java.io.File
@@ -21,7 +17,8 @@ import java.io.OutputStream
import java.io.RandomAccessFile
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
-import org.moire.ultrasonic.app.UApp
+import org.moire.ultrasonic.data.ActiveServerProvider
+import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@@ -33,10 +30,7 @@ import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
- * This class represents a singe Song or Video that can be downloaded.
- *
- * @author Sindre Mehus
- * @version $Id$
+ * This class represents a single Song or Video that can be downloaded.
*/
class DownloadFile(
val song: MusicDirectory.Entry,
@@ -64,6 +58,7 @@ class DownloadFile(
private val downloader: Downloader by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
+ private val activeServerProvider: ActiveServerProvider by inject()
val progress: MutableLiveData = MutableLiveData(0)
@@ -206,16 +201,12 @@ class DownloadFile(
}
private inner class DownloadTask : CancellableTask() {
+ val musicService = getMusicService()
+
override fun execute() {
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
- var wakeLock: WakeLock? = null
- var wifiLock: WifiLock? = null
try {
- wakeLock = acquireWakeLock(wakeLock)
- wifiLock = Util.createWifiLock(toString())
- wifiLock.acquire()
-
if (saveFile.exists()) {
Timber.i("%s already exists. Skipping.", saveFile)
return
@@ -234,8 +225,6 @@ class DownloadFile(
return
}
- val musicService = getMusicService()
-
// Some devices seem to throw error on partial file which doesn't exist
val needsDownloading: Boolean
val duration = song.duration
@@ -280,6 +269,11 @@ class DownloadFile(
if (isCancelled) {
throw Exception(String.format("Download of '%s' was cancelled", song))
}
+
+ if (song.artistId != null) {
+ cacheMetadata(song.artistId!!)
+ }
+
downloadAndSaveCoverArt()
}
@@ -307,33 +301,38 @@ class DownloadFile(
} finally {
Util.close(inputStream)
Util.close(outputStream)
- if (wakeLock != null) {
- wakeLock.release()
- Timber.i("Released wake lock %s", wakeLock)
- }
- wifiLock?.release()
CacheCleaner().cleanSpace()
downloader.checkDownloads()
}
}
- private fun acquireWakeLock(wakeLock: WakeLock?): WakeLock? {
- var wakeLock1 = wakeLock
- if (Settings.isScreenLitOnDownload) {
- val context = UApp.applicationContext()
- val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
- val flags = PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ON_AFTER_RELEASE
- wakeLock1 = pm.newWakeLock(flags, toString())
- wakeLock1.acquire(10 * 60 * 1000L /*10 minutes*/)
- Timber.i("Acquired wake lock %s", wakeLock1)
- }
- return wakeLock1
- }
-
override fun toString(): String {
return String.format("DownloadTask (%s)", song)
}
+ private fun cacheMetadata(artistId: String) {
+ // TODO: Right now it's caching the track artist.
+ // Once the albums are cached in db, we should retrieve the album,
+ // and then cache the album artist.
+ if (artistId.isEmpty()) return
+ var artist: Artist? =
+ activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId)
+
+ // If we are downloading a new album, and the user has not visited the Artists list
+ // recently, then the artist won't be in the database.
+ if (artist == null) {
+ val artists: List = musicService.getArtists(true)
+ artist = artists.find {
+ it.id == artistId
+ }
+ }
+
+ // If we have found an artist, catch it.
+ if (artist != null) {
+ activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist)
+ }
+ }
+
private fun downloadAndSaveCoverArt() {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
similarity index 88%
rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt
rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
index c8bb2d69..6d1527dc 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
@@ -1,5 +1,6 @@
package org.moire.ultrasonic.service
+import android.net.wifi.WifiManager
import java.util.ArrayList
import java.util.PriorityQueue
import java.util.concurrent.Executors
@@ -29,6 +30,8 @@ class Downloader(
private val localMediaPlayer: LocalMediaPlayer
) : KoinComponent {
val playlist: MutableList = ArrayList()
+ var started: Boolean = false
+
private val downloadQueue: PriorityQueue = PriorityQueue()
private val activelyDownloading: MutableList = ArrayList()
@@ -37,27 +40,20 @@ class Downloader(
private val downloadFileCache = LRUCache(100)
private var executorService: ScheduledExecutorService? = null
+ private var wifiLock: WifiManager.WifiLock? = null
var playlistUpdateRevision: Long = 0
private set
val downloadChecker = Runnable {
try {
- Timber.w("checking Downloads")
+ Timber.w("Checking Downloads")
checkDownloadsInternal()
} catch (all: Exception) {
Timber.e(all, "checkDownloads() failed.")
}
}
- fun onCreate() {
- executorService = Executors.newSingleThreadScheduledExecutor()
- executorService!!.scheduleWithFixedDelay(
- downloadChecker, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.SECONDS
- )
- Timber.i("Downloader created")
- }
-
fun onDestroy() {
stop()
clearPlaylist()
@@ -65,16 +61,42 @@ class Downloader(
Timber.i("Downloader destroyed")
}
+ fun start() {
+ started = true
+ if (executorService == null) {
+ executorService = Executors.newSingleThreadScheduledExecutor()
+ executorService!!.scheduleWithFixedDelay(
+ downloadChecker, 0L, CHECK_INTERVAL, TimeUnit.SECONDS
+ )
+ Timber.i("Downloader started")
+ }
+
+ if (wifiLock == null) {
+ wifiLock = Util.createWifiLock(toString())
+ wifiLock?.acquire()
+ }
+ }
+
fun stop() {
- if (executorService != null) executorService!!.shutdown()
+ started = false
+ executorService?.shutdown()
+ executorService = null
+ wifiLock?.release()
+ wifiLock = null
+ MediaPlayerService.runningInstance?.notifyDownloaderStopped()
Timber.i("Downloader stopped")
}
fun checkDownloads() {
- executorService?.execute(downloadChecker)
+ if (executorService == null || executorService!!.isTerminated) {
+ start()
+ } else {
+ executorService?.execute(downloadChecker)
+ }
}
@Synchronized
+ @Suppress("ComplexMethod")
fun checkDownloadsInternal() {
if (
!Util.isExternalStoragePresent() ||
@@ -120,13 +142,24 @@ class Downloader(
while (activelyDownloading.size < PARALLEL_DOWNLOADS && downloadQueue.size > 0) {
val task = downloadQueue.remove()
activelyDownloading.add(task)
- task.download()
+ startDownloadOnService(task)
// The next file on the playlist is currently downloading
if (playlist.indexOf(task) == 1) {
localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING)
}
}
+
+ // Stop Executor service when done downloading
+ if (activelyDownloading.size == 0) {
+ stop()
+ }
+ }
+
+ private fun startDownloadOnService(task: DownloadFile) {
+ MediaPlayerService.executeOnStartedMediaPlayerService {
+ task.download()
+ }
}
private fun cleanupActiveDownloads() {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
index 6e7b4087..f86ce5b2 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
@@ -48,6 +48,9 @@ import timber.log.Timber
/**
* Android Foreground Service for playing music
* while the rest of the Ultrasonic App is in the background.
+ *
+ * "A foreground service is a service that the user is
+ * actively aware of and isn’t a candidate for the system to kill when low on memory."
*/
@Suppress("LargeClass")
class MediaPlayerService : Service() {
@@ -79,7 +82,6 @@ class MediaPlayerService : Service() {
override fun onCreate() {
super.onCreate()
- downloader.onCreate()
shufflePlayBuffer.onCreate()
localMediaPlayer.init()
@@ -156,6 +158,10 @@ class MediaPlayerService : Service() {
}
}
+ fun notifyDownloaderStopped() {
+ stopIfIdle()
+ }
+
@Synchronized
fun seekTo(position: Int) {
if (jukeboxMediaPlayer.isEnabled) {
@@ -581,22 +587,27 @@ class MediaPlayerService : Service() {
// Clear old actions
notificationBuilder!!.clearActions()
- // Add actions
- val compactActions = addActions(context, notificationBuilder!!, playerState, song)
-
- // Configure shortcut actions
- style.setShowActionsInCompactView(*compactActions)
- notificationBuilder!!.setStyle(style)
-
- // Set song title, artist and cover if possible
if (song != null) {
+ // Add actions
+ val compactActions = addActions(context, notificationBuilder!!, playerState, song)
+ // Configure shortcut actions
+ style.setShowActionsInCompactView(*compactActions)
+ notificationBuilder!!.setStyle(style)
+
+ // Set song title, artist and cover
val iconSize = (256 * context.resources.displayMetrics.density).toInt()
val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize)
notificationBuilder!!.setContentTitle(song.title)
notificationBuilder!!.setContentText(song.artist)
notificationBuilder!!.setLargeIcon(bitmap)
notificationBuilder!!.setSubText(song.album)
+ } else if (downloader.started) {
+ // No song is playing, but Ultrasonic is downloading files
+ notificationBuilder!!.setContentTitle(
+ getString(R.string.notification_downloading_title)
+ )
}
+
return notificationBuilder!!.build()
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
index 874138ce..d705119a 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
@@ -52,7 +52,7 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List {
val indexes: MutableList = ArrayList()
- val root = FileUtil.getMusicDirectory()
+ val root = FileUtil.musicDirectory
for (file in FileUtil.listFiles(root)) {
if (file.isDirectory) {
val index = Index(file.path)
@@ -121,7 +121,7 @@ class OfflineMusicService : MusicService, KoinComponent {
val artists: MutableList = ArrayList()
val albums: MutableList = ArrayList()
val songs: MutableList = ArrayList()
- val root = FileUtil.getMusicDirectory()
+ val root = FileUtil.musicDirectory
var closeness: Int
for (artistFile in FileUtil.listFiles(root)) {
val artistName = artistFile.name
@@ -250,7 +250,7 @@ class OfflineMusicService : MusicService, KoinComponent {
}
override fun getRandomSongs(size: Int): MusicDirectory {
- val root = FileUtil.getMusicDirectory()
+ val root = FileUtil.musicDirectory
val children: MutableList = LinkedList()
listFilesRecursively(root, children)
val result = MusicDirectory()
@@ -503,7 +503,7 @@ class OfflineMusicService : MusicService, KoinComponent {
entry.isDirectory = file.isDirectory
entry.parent = file.parent
entry.size = file.length()
- val root = FileUtil.getMusicDirectory().path
+ val root = FileUtil.musicDirectory.path
entry.path = file.path.replaceFirst(
String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
index 067a2e4a..3121fbe6 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
@@ -42,7 +42,6 @@ import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toIndexList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.util.FileUtil
-import org.moire.ultrasonic.util.FileUtilKt
import org.moire.ultrasonic.util.Settings
import timber.log.Timber
@@ -242,7 +241,7 @@ open class RESTMusicService(
activeServerProvider.getActiveServer().name, name
)
- FileUtilKt.savePlaylist(playlistFile, playlist, name)
+ FileUtil.savePlaylist(playlistFile, playlist, name)
}
@Throws(Exception::class)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt
index 6156b781..84a89986 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt
@@ -51,7 +51,7 @@ class ImageLoaderProvider(val context: Context) : KoinComponent {
ImageLoaderConfig(
Util.getMaxDisplayMetric(),
defaultSize,
- FileUtil.getAlbumArtDirectory()
+ FileUtil.albumArtDirectory
)
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt
index ea84668f..0318f70c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt
@@ -1,20 +1,8 @@
/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see .
-
- Copyright 2021 (C) Jozsef Varga
+ * CancellationToken.kt
+ * Copyright (C) 2009-2021 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
new file mode 100644
index 00000000..908974c4
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
@@ -0,0 +1,125 @@
+/*
+ * Constants.kt
+ * Copyright (C) 2009-2021 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+package org.moire.ultrasonic.util
+
+object Constants {
+ // Character encoding used throughout.
+ const val UTF_8 = "UTF-8"
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ const val REST_PROTOCOL_VERSION = "1.7.0"
+ const val REST_CLIENT_ID = "Ultrasonic"
+
+ // Names for intent extras.
+ const val INTENT_EXTRA_NAME_ID = "subsonic.id"
+ const val INTENT_EXTRA_NAME_NAME = "subsonic.name"
+ const val INTENT_EXTRA_NAME_ARTIST = "subsonic.artist"
+ const val INTENT_EXTRA_NAME_TITLE = "subsonic.title"
+ const val INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall"
+ const val INTENT_EXTRA_NAME_QUERY = "subsonic.query"
+ const val INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id"
+ const val INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id"
+ const val INTENT_EXTRA_NAME_PARENT_ID = "subsonic.parent.id"
+ const val INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name"
+ const val INTENT_EXTRA_NAME_SHARE_ID = "subsonic.share.id"
+ const val INTENT_EXTRA_NAME_SHARE_NAME = "subsonic.share.name"
+ const val INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype"
+ const val INTENT_EXTRA_NAME_ALBUM_LIST_TITLE = "subsonic.albumlisttitle"
+ const val INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize"
+ const val INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset"
+ const val INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle"
+ const val INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh"
+ const val INTENT_EXTRA_NAME_STARRED = "subsonic.starred"
+ const val INTENT_EXTRA_NAME_RANDOM = "subsonic.random"
+ const val INTENT_EXTRA_NAME_GENRE_NAME = "subsonic.genre"
+ const val INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum"
+ const val INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos"
+ const val INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer"
+ const val INTENT_EXTRA_NAME_APPEND = "subsonic.append"
+
+ // Names for Intent Actions
+ const val CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE"
+ const val CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY"
+ const val CMD_RESUME_OR_PLAY = "org.moire.ultrasonic.CMD_RESUME_OR_PLAY"
+ const val CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE"
+ const val CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE"
+ const val CMD_STOP = "org.moire.ultrasonic.CMD_STOP"
+ const val CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"
+ const val CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"
+
+ // Preferences keys.
+ const val PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId"
+ const val PREFERENCES_KEY_SERVERS_EDIT = "editServers"
+ const val PREFERENCES_KEY_THEME = "theme"
+ const val PREFERENCES_KEY_THEME_LIGHT = "light"
+ const val PREFERENCES_KEY_THEME_DARK = "dark"
+ const val PREFERENCES_KEY_THEME_BLACK = "black"
+ const val PREFERENCES_KEY_DISPLAY_BITRATE_WITH_ARTIST = "displayBitrateWithArtist"
+ const val PREFERENCES_KEY_USE_FOLDER_FOR_ALBUM_ARTIST = "useFolderForAlbumArtist"
+ const val PREFERENCES_KEY_SHOW_TRACK_NUMBER = "showTrackNumber"
+ const val PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi"
+ const val PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile"
+ const val PREFERENCES_KEY_CACHE_SIZE = "cacheSize"
+ const val PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation"
+ const val PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount"
+ const val PREFERENCES_KEY_HIDE_MEDIA = "hideMedia"
+ const val PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons"
+ const val PREFERENCES_KEY_SCROBBLE = "scrobble"
+ const val PREFERENCES_KEY_SERVER_SCALING = "serverScaling"
+ const val PREFERENCES_KEY_REPEAT_MODE = "repeatMode"
+ const val PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload"
+ const val PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength"
+ const val PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout"
+ const val PREFERENCES_KEY_SHOW_NOTIFICATION = "showNotification"
+ const val PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION = "alwaysShowNotification"
+ const val PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS = "showLockScreen"
+ const val PREFERENCES_KEY_MAX_ALBUMS = "maxAlbums"
+ const val PREFERENCES_KEY_MAX_SONGS = "maxSongs"
+ const val PREFERENCES_KEY_MAX_ARTISTS = "maxArtists"
+ const val PREFERENCES_KEY_DEFAULT_ALBUMS = "defaultAlbums"
+ const val PREFERENCES_KEY_DEFAULT_SONGS = "defaultSongs"
+ const val PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists"
+ const val PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying"
+ const val PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback"
+ const val PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS = "playbackControlSettings"
+ const val PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory"
+ const val PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay"
+ const val PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"
+ const val PREFERENCES_KEY_ID3_TAGS = "useId3Tags"
+ const val PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture"
+ const val PREFERENCES_KEY_TEMP_LOSS = "tempLoss"
+ const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"
+ const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"
+ const val PREFERENCES_KEY_CLEAR_PLAYLIST = "clearPlaylist"
+ const val PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark"
+ const val PREFERENCES_KEY_DISC_SORT = "discAndTrackSort"
+ const val PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications"
+ const val PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt"
+ const val PREFERENCES_KEY_DISABLE_SEND_NOW_PLAYING_LIST = "disableNowPlayingListSending"
+ const val PREFERENCES_KEY_VIEW_REFRESH = "viewRefresh"
+ const val PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS = "sharingAlwaysAskForDetails"
+ const val PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION = "sharingDefaultDescription"
+ const val PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting"
+ const val PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration"
+ const val PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist"
+ const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating"
+ const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory"
+ const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted"
+ const val PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE = "resumeOnBluetoothDevice"
+ const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice"
+ const val PREFERENCES_KEY_SINGLE_BUTTON_PLAY_PAUSE = "singleButtonPlayPause"
+ const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile"
+ const val PREFERENCE_VALUE_ALL = 0
+ const val PREFERENCE_VALUE_A2DP = 1
+ const val PREFERENCE_VALUE_DISABLED = 2
+ const val FILENAME_DOWNLOADS_SER = "downloadstate.ser"
+ const val ALBUM_ART_FILE = "folder.jpeg"
+ const val STARRED = "starred"
+ const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
+ const val RESULT_CLOSE_ALL = 1337
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt
new file mode 100644
index 00000000..af5690e6
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt
@@ -0,0 +1,479 @@
+/*
+ * FileUtil.kt
+ * Copyright (C) 2009-2021 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
+package org.moire.ultrasonic.util
+
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import android.text.TextUtils
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.FileWriter
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
+import java.util.Locale
+import java.util.SortedSet
+import java.util.TreeSet
+import java.util.regex.Pattern
+import org.koin.java.KoinJavaComponent
+import org.moire.ultrasonic.app.UApp
+import org.moire.ultrasonic.domain.MusicDirectory
+import timber.log.Timber
+
+object FileUtil {
+
+ private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|")
+ private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|")
+ private val MUSIC_FILE_EXTENSIONS =
+ listOf("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus")
+ private val VIDEO_FILE_EXTENSIONS =
+ listOf("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv")
+ private val PLAYLIST_FILE_EXTENSIONS = listOf("m3u")
+ private val TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*")
+ const val SUFFIX_LARGE = ".jpeg"
+ const val SUFFIX_SMALL = ".jpeg-small"
+ private const val UNNAMED = "unnamed"
+
+ private val permissionUtil = KoinJavaComponent.inject(
+ PermissionUtil::class.java
+ )
+
+ fun getSongFile(song: MusicDirectory.Entry): File {
+ val dir = getAlbumDirectory(song)
+
+ // Do not generate new name for offline files. Offline files will have their Path as their Id.
+ if (!TextUtils.isEmpty(song.id)) {
+ if (song.id.startsWith(dir.absolutePath)) return File(song.id)
+ }
+
+ // Generate a file name for the song
+ val fileName = StringBuilder(256)
+ val track = song.track
+
+ // check if filename already had track number
+ if (song.title != null && !TITLE_WITH_TRACK.matcher(song.title!!).matches()) {
+ if (track != null) {
+ if (track < 10) {
+ fileName.append('0')
+ }
+ fileName.append(track).append('-')
+ }
+ }
+ fileName.append(fileSystemSafe(song.title)).append('.')
+ if (!TextUtils.isEmpty(song.transcodedSuffix)) {
+ fileName.append(song.transcodedSuffix)
+ } else {
+ fileName.append(song.suffix)
+ }
+ return File(dir, fileName.toString())
+ }
+
+ @JvmStatic
+ fun getPlaylistFile(server: String?, name: String?): File {
+ val playlistDir = getPlaylistDirectory(server)
+ return File(playlistDir, String.format(Locale.ROOT, "%s.m3u", fileSystemSafe(name)))
+ }
+
+ @JvmStatic
+ val playlistDirectory: File
+ get() {
+ val playlistDir = File(ultrasonicDirectory, "playlists")
+ ensureDirectoryExistsAndIsReadWritable(playlistDir)
+ return playlistDir
+ }
+
+ @JvmStatic
+ fun getPlaylistDirectory(server: String? = null): File {
+ val playlistDir: File
+ if (server != null) {
+ playlistDir = File(playlistDirectory, server)
+ } else {
+ playlistDir = playlistDirectory
+ }
+ ensureDirectoryExistsAndIsReadWritable(playlistDir)
+ return playlistDir
+ }
+
+ /**
+ * Get the album art file for a given album entry
+ * @param entry The album entry
+ * @return File object. Not guaranteed that it exists
+ */
+ fun getAlbumArtFile(entry: MusicDirectory.Entry): File {
+ val albumDir = getAlbumDirectory(entry)
+ return getAlbumArtFile(albumDir)
+ }
+
+ /**
+ * Get the cache key for a given album entry
+ * @param entry The album entry
+ * @param large Whether to get the key for the large or the default image
+ * @return String The hash key
+ */
+ fun getAlbumArtKey(entry: MusicDirectory.Entry?, large: Boolean): String? {
+ if (entry == null) return null
+ val albumDir = getAlbumDirectory(entry)
+ return getAlbumArtKey(albumDir, large)
+ }
+
+ /**
+ * Get the cache key for a given artist
+ * @param name The artist name
+ * @param large Whether to get the key for the large or the default image
+ * @return String The hash key
+ */
+ fun getArtistArtKey(name: String?, large: Boolean): String {
+ val artist = fileSystemSafe(name)
+ val dir = File(String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, UNNAMED))
+ return getAlbumArtKey(dir, large)
+ }
+
+ /**
+ * Get the cache key for a given album entry
+ * @param albumDir The album directory
+ * @param large Whether to get the key for the large or the default image
+ * @return String The hash key
+ */
+ private fun getAlbumArtKey(albumDir: File, large: Boolean): String {
+ val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL
+ return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.path), suffix)
+ }
+
+ fun getAvatarFile(username: String?): File? {
+ if (username == null) {
+ return null
+ }
+ val albumArtDir = albumArtDirectory
+ val md5Hex = Util.md5Hex(username)
+ return File(albumArtDir, String.format(Locale.ROOT, "%s%s", md5Hex, SUFFIX_LARGE))
+ }
+
+ /**
+ * Get the album art file for a given album directory
+ * @param albumDir The album directory
+ * @return File object. Not guaranteed that it exists
+ */
+ @JvmStatic
+ fun getAlbumArtFile(albumDir: File): File {
+ val albumArtDir = albumArtDirectory
+ val key = getAlbumArtKey(albumDir, true)
+ return File(albumArtDir, key)
+ }
+
+ /**
+ * Get the album art file for a given cache key
+ * @param cacheKey The key (== the filename)
+ * @return File object. Not guaranteed that it exists
+ */
+ @JvmStatic
+ fun getAlbumArtFile(cacheKey: String?): File? {
+ val albumArtDir = albumArtDirectory
+ return if (cacheKey == null) {
+ null
+ } else File(albumArtDir, cacheKey)
+ }
+
+ val albumArtDirectory: File
+ get() {
+ val albumArtDir = File(ultrasonicDirectory, "artwork")
+ ensureDirectoryExistsAndIsReadWritable(albumArtDir)
+ ensureDirectoryExistsAndIsReadWritable(File(albumArtDir, ".nomedia"))
+ return albumArtDir
+ }
+
+ fun getAlbumDirectory(entry: MusicDirectory.Entry): File {
+ val dir: File
+ if (!TextUtils.isEmpty(entry.path)) {
+ val f = File(fileSystemSafeDir(entry.path))
+ dir = File(
+ String.format(
+ Locale.ROOT,
+ "%s/%s",
+ musicDirectory.path,
+ if (entry.isDirectory) f.path else f.parent ?: ""
+ )
+ )
+ } else {
+ val artist = fileSystemSafe(entry.artist)
+ var album = fileSystemSafe(entry.album)
+ if (UNNAMED == album) {
+ album = fileSystemSafe(entry.title)
+ }
+ dir = File(String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, album))
+ }
+ return dir
+ }
+
+ fun createDirectoryForParent(file: File) {
+ val dir = file.parentFile
+ if (dir != null && !dir.exists()) {
+ if (!dir.mkdirs()) {
+ Timber.e("Failed to create directory %s", dir)
+ }
+ }
+ }
+
+ @Suppress("SameParameterValue")
+ private fun getOrCreateDirectory(name: String): File {
+ val dir = File(ultrasonicDirectory, name)
+ if (!dir.exists() && !dir.mkdirs()) {
+ Timber.e("Failed to create %s", name)
+ }
+ return dir
+ }
+
+ // 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.
+ @JvmStatic
+ val ultrasonicDirectory: File
+ get() {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
+ Environment.getExternalStorageDirectory(),
+ "Android/data/org.moire.ultrasonic"
+ ) else UApp.applicationContext().getExternalFilesDir(null)!!
+ }
+
+ // 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.
+ @JvmStatic
+ val defaultMusicDirectory: File
+ get() = getOrCreateDirectory("music")
+
+ @JvmStatic
+ val musicDirectory: File
+ get() {
+ val path = Settings.preferences
+ .getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultMusicDirectory.path)
+ val dir = File(path!!)
+ val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
+ if (!hasAccess) permissionUtil.value.handlePermissionFailed(null)
+ return if (hasAccess) dir else defaultMusicDirectory
+ }
+
+ @JvmStatic
+ @Suppress("ReturnCount")
+ fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
+ if (dir == null) {
+ return false
+ }
+ if (dir.exists()) {
+ if (!dir.isDirectory) {
+ Timber.w("%s exists but is not a directory.", dir)
+ return false
+ }
+ } else {
+ if (dir.mkdirs()) {
+ Timber.i("Created directory %s", dir)
+ } else {
+ Timber.w("Failed to create directory %s", dir)
+ return false
+ }
+ }
+ if (!dir.canRead()) {
+ Timber.w("No read permission for directory %s", dir)
+ return false
+ }
+ if (!dir.canWrite()) {
+ Timber.w("No write permission for directory %s", dir)
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param name The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private fun fileSystemSafe(name: String?): String {
+ if (name == null || name.trim { it <= ' ' }.isEmpty()) {
+ return UNNAMED
+ }
+ var filename: String = name
+
+ for (s in FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-")
+ }
+ return filename
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private fun fileSystemSafeDir(path: String?): String {
+ var filepath = path
+ if (filepath == null || filepath.trim { it <= ' ' }.isEmpty()) {
+ return ""
+ }
+ for (s in FILE_SYSTEM_UNSAFE_DIR) {
+ filepath = filepath!!.replace(s, "-")
+ }
+ return filepath!!
+ }
+
+ /**
+ * Similar to [File.listFiles], but returns a sorted set.
+ * Never returns `null`, instead a warning is logged, and an empty set is returned.
+ */
+ @JvmStatic
+ fun listFiles(dir: File): SortedSet {
+ val files = dir.listFiles()
+ if (files == null) {
+ Timber.w("Failed to list children for %s", dir.path)
+ return TreeSet()
+ }
+ return TreeSet(files.asList())
+ }
+
+ fun listMediaFiles(dir: File): SortedSet {
+ val files = listFiles(dir)
+ val iterator = files.iterator()
+ while (iterator.hasNext()) {
+ val file = iterator.next()
+ if (!file.isDirectory && !isMediaFile(file)) {
+ iterator.remove()
+ }
+ }
+ return files
+ }
+
+ private fun isMediaFile(file: File): Boolean {
+ val extension = getExtension(file.name)
+ return MUSIC_FILE_EXTENSIONS.contains(extension) ||
+ VIDEO_FILE_EXTENSIONS.contains(extension)
+ }
+
+ fun isPlaylistFile(file: File): Boolean {
+ val extension = getExtension(file.name)
+ return PLAYLIST_FILE_EXTENSIONS.contains(extension)
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ fun getExtension(name: String): String {
+ val index = name.lastIndexOf('.')
+ return if (index == -1) "" else name.substring(index + 1).lowercase(Locale.ROOT)
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ fun getBaseName(name: String): String {
+ val index = name.lastIndexOf('.')
+ return if (index == -1) name else name.substring(0, index)
+ }
+
+ /**
+ * Returns the file name of a .partial file of the given file.
+ *
+ * @param name The filename in question.
+ * @return The .partial file name
+ */
+ fun getPartialFile(name: String): String {
+ return String.format(Locale.ROOT, "%s.partial.%s", getBaseName(name), getExtension(name))
+ }
+
+ /**
+ * Returns the file name of a .complete file of the given file.
+ *
+ * @param name The filename in question.
+ * @return The .complete file name
+ */
+ fun getCompleteFile(name: String): String {
+ return String.format(Locale.ROOT, "%s.complete.%s", getBaseName(name), getExtension(name))
+ }
+
+ @JvmStatic
+ fun serialize(context: Context, obj: T, fileName: String): Boolean {
+ val file = File(context.cacheDir, fileName)
+ var out: ObjectOutputStream? = null
+ return try {
+ out = ObjectOutputStream(FileOutputStream(file))
+ out.writeObject(obj)
+ Timber.i("Serialized object to %s", file)
+ true
+ } catch (ignored: Exception) {
+ Timber.w("Failed to serialize object to %s", file)
+ false
+ } finally {
+ Util.close(out)
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @JvmStatic
+ fun deserialize(context: Context, fileName: String): T? {
+ val file = File(context.cacheDir, fileName)
+ if (!file.exists() || !file.isFile) {
+ return null
+ }
+ var inStream: ObjectInputStream? = null
+ return try {
+ inStream = ObjectInputStream(FileInputStream(file))
+ val readObject = inStream.readObject()
+ val result = readObject as T
+ Timber.i("Deserialized object from %s", file)
+ result
+ } catch (all: Throwable) {
+ Timber.w(all, "Failed to deserialize object from %s", file)
+ null
+ } finally {
+ Util.close(inStream)
+ }
+ }
+
+ fun savePlaylist(
+ playlistFile: File?,
+ playlist: MusicDirectory,
+ name: String
+ ) {
+ val fw = FileWriter(playlistFile)
+ val bw = BufferedWriter(fw)
+
+ try {
+ fw.write("#EXTM3U\n")
+ for (e in playlist.getChildren()) {
+ var filePath = getSongFile(e).absolutePath
+
+ if (!File(filePath).exists()) {
+ val ext = getExtension(filePath)
+ val base = getBaseName(filePath)
+ filePath = "$base.complete.$ext"
+ }
+ fw.write(filePath + "\n")
+ }
+ } catch (e: IOException) {
+ Timber.w("Failed to save playlist: %s", name)
+ throw e
+ } finally {
+ bw.close()
+ fw.close()
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt
deleted file mode 100644
index fc527c15..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * FileUtil.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
- *
- * Distributed under terms of the GNU GPLv3 license.
- */
-
-package org.moire.ultrasonic.util
-
-import java.io.BufferedWriter
-import java.io.File
-import java.io.FileWriter
-import java.io.IOException
-import org.moire.ultrasonic.domain.MusicDirectory
-import timber.log.Timber
-
-// TODO: Convert FileUtil.java and merge into here.
-object FileUtilKt {
- fun savePlaylist(
- playlistFile: File?,
- playlist: MusicDirectory,
- name: String
- ) {
- val fw = FileWriter(playlistFile)
- val bw = BufferedWriter(fw)
-
- try {
- fw.write("#EXTM3U\n")
- for (e in playlist.getChildren()) {
- var filePath = FileUtil.getSongFile(e).absolutePath
-
- if (!File(filePath).exists()) {
- val ext = FileUtil.getExtension(filePath)
- val base = FileUtil.getBaseName(filePath)
- filePath = "$base.complete.$ext"
- }
- fw.write(filePath + "\n")
- }
- } catch (e: IOException) {
- Timber.w("Failed to save playlist: %s", name)
- throw e
- } finally {
- bw.close()
- fw.close()
- }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt
index 961297f9..24f97fa9 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt
@@ -24,15 +24,6 @@ import org.moire.ultrasonic.domain.RepeatMode
object Settings {
private val PATTERN = Pattern.compile(":")
- val isScreenLitOnDownload: Boolean
- get() {
- val preferences = preferences
- return preferences.getBoolean(
- Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD,
- false
- )
- }
-
var repeatMode: RepeatMode
get() {
val preferences = preferences
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt
index 5a5d77cf..658d2943 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt
@@ -20,7 +20,7 @@ class SubsonicUncaughtExceptionHandler(
var printWriter: PrintWriter? = null
try {
- file = File(FileUtil.getUltrasonicDirectory(), STACKTRACE_NAME)
+ file = File(FileUtil.ultrasonicDirectory, STACKTRACE_NAME)
printWriter = PrintWriter(file)
val logMessage = String.format(
"Android API level: %s\nUltrasonic version name: %s\n" +
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
index 4a79d30d..52905159 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
@@ -37,14 +37,11 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants
-import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.text.DecimalFormat
@@ -115,38 +112,6 @@ object Util {
}
}
- /**
- * Get the contents of an `InputStream` as a `byte[]`.
- *
- *
- * This method buffers the input internally, so there is no need to use a
- * `BufferedInputStream`.
- *
- * @param input the `InputStream` to read from
- * @return the requested byte array
- * @throws NullPointerException if the input is null
- * @throws java.io.IOException if an I/O error occurs
- */
- @Throws(IOException::class)
- fun toByteArray(input: InputStream?): ByteArray {
- val output = ByteArrayOutputStream()
- copy(input!!, output)
- return output.toByteArray()
- }
-
- @Throws(IOException::class)
- @Suppress("MagicNumber")
- fun copy(input: InputStream, output: OutputStream): Long {
- val buffer = ByteArray(KBYTE * 4)
- var count: Long = 0
- var n: Int
- while (-1 != input.read(buffer).also { n = it }) {
- output.write(buffer, 0, n)
- count += n.toLong()
- }
- return count
- }
-
@Throws(IOException::class)
fun atomicCopy(from: File, to: File) {
val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml
index 17c802f2..934be6c7 100644
--- a/ultrasonic/src/main/res/values-cs/strings.xml
+++ b/ultrasonic/src/main/res/values-cs/strings.xml
@@ -248,8 +248,6 @@
Neomezeně
Pokračovat po připojení sluchátek
Aplikace spustí pozastavené přehrávání po připojení kabelu sluchátek do přístroje.
- Nezamčení displeje při stahování zlepšuje rychlost stahování.
- Nezamykat displej
1
10
100
diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml
index c581c89e..0ba6ac97 100644
--- a/ultrasonic/src/main/res/values-de/strings.xml
+++ b/ultrasonic/src/main/res/values-de/strings.xml
@@ -247,8 +247,6 @@
Unbegrenzt
Fortsetzen mit Kopfhörer
Die App setzt eine pausierte Wiedergabe beim Anschließen der Kopfhörer fort.
- Wenn der Bildschirm während des Herunterladens eingeschaltet bleibt, wird die Download-Geschwindigkeit verbessert.
- Bildschirm eingeschaltet lassen
1
10
100
diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml
index 19df0db3..07d30379 100644
--- a/ultrasonic/src/main/res/values-es/strings.xml
+++ b/ultrasonic/src/main/res/values-es/strings.xml
@@ -260,8 +260,6 @@
Ilimitado
Reanudar al insertar los auriculares
La aplicación reanudará la reproducción en pausa al insertar los auriculares en el dispositivo.
- Mantener la pantalla encendida mientras descarga mejora la velocidad de la misma.
- Mantener la pantalla encendida
Recuerda configurar tu nombre de usuario y contraseña en los servicios de Scrobble en el servidor
Hacer Scrobble de mis reproducciones
1
diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml
index 756c4e42..01f0baee 100644
--- a/ultrasonic/src/main/res/values-fr/strings.xml
+++ b/ultrasonic/src/main/res/values-fr/strings.xml
@@ -260,8 +260,6 @@
Illimité
Reprise à l\'insertion des écouteurs
L\'application reprendra la lecture lors de l\'insertion du casque dans l\'appareil.
- Garder l\'écran allumé pendant le téléchargement permet d\'améliorer la vitesse de téléchargement.
- Garder l\'écran allumé
Pensez à configurer votre nom d’utilisateur et votre mot de passe dans le(s) service(s) Scrobble sur le serveur.
Scrobbler mes lectures
1
diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml
index ef99d78e..e639c913 100644
--- a/ultrasonic/src/main/res/values-hu/strings.xml
+++ b/ultrasonic/src/main/res/values-hu/strings.xml
@@ -258,8 +258,6 @@
Korlátlan
Folytatás a fejhallgató behelyezésekor
Az alkalmazás folytatja a szüneteltetett lejátszást a fejhallgató behelyezésekor a készülékbe.
- Képernyő ébrentartása a letöltés alatt, a magasabb letöltési sebesség érdekében.
- Képernyő ébrentartása
Ne felejtsd el beállítani a Scrobble szolgáltatónál használt felhasználóneved és jelszavad a szervereden
Scrobble engedélyezése
1
diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml
index 2a92a780..2ad80868 100644
--- a/ultrasonic/src/main/res/values-it/strings.xml
+++ b/ultrasonic/src/main/res/values-it/strings.xml
@@ -242,8 +242,6 @@
5 canzoni
Illimitato
Riprendi all\'inserimento delle cuffie
- Mantenere lo schermo acceso durante il download migliora la sua velocità.
- Mantieni lo schemo acceso
1
10
100
diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml
index a6a29a19..8fd7f8f7 100644
--- a/ultrasonic/src/main/res/values-nl/strings.xml
+++ b/ultrasonic/src/main/res/values-nl/strings.xml
@@ -258,8 +258,6 @@
Ongelimiteerd
Hervatten bij aansluiten van hoofdtelefoon
Het afspelen wordt hervat zodra er een hoofdtelefoon wordt aangesloten.
- Door het scherm aan te houden tijdens het downloaden, wordt de downloadsnelheid verhoogd.
- Scherm aan houden
Let op: stel je gebruikersnaam en wachtwoord van je scrobble-dienst(en) in op je Subsonic-server
Scrobbelen
1
diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml
index a01adb07..09d38120 100644
--- a/ultrasonic/src/main/res/values-pl/strings.xml
+++ b/ultrasonic/src/main/res/values-pl/strings.xml
@@ -246,8 +246,6 @@
Bez limitu
Wznawiaj po podłączeniu słuchawek
Aplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek.
- Podtrzymywanie włączonego ekranu poprawia prędkość pobierania.
- Podtrzymuj ekran włączony
1
10
100
diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
index fda1630f..b8ef019d 100644
--- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml
+++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
@@ -260,8 +260,6 @@
Ilimitado
Retomar ao Inserir Fone de Ouvido
O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo.
- Manter a tela ligada enquanto baixando aumenta a velocidade de download.
- Manter a Tela Ligada
Lembre-se de configurar usuário e senha nos serviços Scrobble do servidor
Registre Minhas Músicas
1
diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml
index 2ff02ba4..68d09fd9 100644
--- a/ultrasonic/src/main/res/values-pt/strings.xml
+++ b/ultrasonic/src/main/res/values-pt/strings.xml
@@ -246,8 +246,6 @@
Ilimitado
Retomar ao inserir Auscultadores
O aplicativo retomará a reprodução em pausa na inserção dos auscultadores no dispositivo.
- Manter o ecrã ligado enquanto descarrega aumenta a velocidade de download.
- Manter o Ecrã Ligado
1
10
100
diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml
index e4fd5e36..44609be5 100644
--- a/ultrasonic/src/main/res/values-ru/strings.xml
+++ b/ultrasonic/src/main/res/values-ru/strings.xml
@@ -260,8 +260,6 @@
Неограниченный
Возобновить подключение наушников
Приложение возобновит приостановленное воспроизведение после того, как в устройство будут вставлены проводные наушники.
- Сохранение экрана во время загрузки повышает скорость загрузки.
- Оставьте экран включенным
Не забудьте установить своего пользователя и пароль в Скроббл сервисах на сервере.
Скробблить мои воспроизведения
1
diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml
index 961a3dbe..df8ba3c7 100644
--- a/ultrasonic/src/main/res/values/strings.xml
+++ b/ultrasonic/src/main/res/values/strings.xml
@@ -262,8 +262,6 @@
Unlimited
Resume on headphones insertion
App will resume paused playback on wired headphones insertion into device.
- Keeping the screen on while downloading improves download speed.
- Keep Screen On
Remember to set up your user and password in the Scrobble service(s) on the server
Scrobble my plays
1
@@ -404,6 +402,7 @@
Keep files
Delete files
Deleted log files.
+ Downloading media in the background…
Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.
Warning
diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml
index ba60cfab..e1b672e5 100644
--- a/ultrasonic/src/main/res/xml/settings.xml
+++ b/ultrasonic/src/main/res/xml/settings.xml
@@ -349,12 +349,6 @@
a:summary="@string/settings.hide_media_summary"
a:title="@string/settings.hide_media_title"
app:iconSpaceReserved="false"/>
-