From ba745a5f9a93484af95884906b21aad4c8440e8b Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 30 Aug 2021 10:08:27 +0200 Subject: [PATCH 01/12] Run download task inside service --- .../moire/ultrasonic/service/Downloader.kt | 8 +++- .../ultrasonic/util/CancellableTask.java | 6 +-- .../org/moire/ultrasonic/util/Constants.java | 1 - .../ultrasonic/imageloader/ImageLoader.kt | 3 +- .../moire/ultrasonic/service/DownloadFile.kt | 35 +++------------- .../ultrasonic/service/MediaPlayerService.kt | 3 ++ .../kotlin/org/moire/ultrasonic/util/Util.kt | 40 ------------------- ultrasonic/src/main/res/xml/settings.xml | 6 --- 8 files changed, 20 insertions(+), 82 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt index 56984123..3c8fbf73 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt @@ -119,7 +119,7 @@ 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) { @@ -128,6 +128,12 @@ class Downloader( } } + private fun startDownloadOnService(task: DownloadFile) { + MediaPlayerService.executeOnStartedMediaPlayerService { + task.download() + } + } + private fun cleanupActiveDownloads() { activelyDownloading.retainAll { when { 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 index 28404977..18d66684 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -87,7 +87,6 @@ public final class Constants 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"; 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..6d24a54f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -162,8 +162,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/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index bb486fa7..de341825 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -7,21 +7,11 @@ 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 -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -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.domain.MusicDirectory import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.subsonic.ImageLoaderProvider @@ -30,6 +20,12 @@ import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.io.RandomAccessFile /** * This class represents a singe Song or Video that can be downloaded. @@ -208,10 +204,8 @@ class DownloadFile( 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() @@ -306,29 +300,12 @@ 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 (Util.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) } 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 7f234a75..b76b0b93 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -47,6 +47,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() { 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 a1db4420..ed847ffd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -102,14 +102,6 @@ object Util { return applicationContext() } - fun isScreenLitOnDownload(): Boolean { - val preferences = getPreferences() - return preferences.getBoolean( - Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, - false - ) - } - var repeatMode: RepeatMode get() { val preferences = getPreferences() @@ -221,38 +213,6 @@ object Util { fun getPreferences(): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext()) - /** - * 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/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 46ad73b9..1947f681 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -343,12 +343,6 @@ a:summary="@string/settings.hide_media_summary" a:title="@string/settings.hide_media_title" app:iconSpaceReserved="false"/> - Date: Sun, 12 Sep 2021 11:37:40 +0200 Subject: [PATCH 02/12] Migrate Constants to Kotlin, update a copyright notice. --- .../org/moire/ultrasonic/util/Constants.java | 149 ------------------ .../ultrasonic/util/CancellationToken.kt | 20 +-- .../org/moire/ultrasonic/util/Constants.kt | 124 +++++++++++++++ 3 files changed, 128 insertions(+), 165 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt 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 18d66684..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ /dev/null @@ -1,149 +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_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_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/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..e6158db6 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -0,0 +1,124 @@ +/* + * 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_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 +} From ec49775d7e586979ef9b9f2f4e4ee65892cd3b4d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 12 Sep 2021 11:40:18 +0200 Subject: [PATCH 03/12] Convert FileUtil to Kotlin --- .../org/moire/ultrasonic/util/FileUtil.java | 536 ------------------ .../org/moire/ultrasonic/util/FileUtil.kt | 439 ++++++++++++++ .../ultrasonic/activity/NavigationActivity.kt | 2 +- .../moire/ultrasonic/log/FileLoggerTree.kt | 4 +- .../ultrasonic/service/OfflineMusicService.kt | 8 +- .../subsonic/ImageLoaderProvider.kt | 2 +- .../util/SubsonicUncaughtExceptionHandler.kt | 2 +- 7 files changed, 448 insertions(+), 545 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.kt 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 3d3f8b27..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 = Util.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/java/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.kt new file mode 100644 index 00000000..62cbcaa0 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.kt @@ -0,0 +1,439 @@ +/* + 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.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.util.Util.close +import org.moire.ultrasonic.util.Util.getPreferences +import org.moire.ultrasonic.util.Util.md5Hex +import timber.log.Timber +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.Locale +import java.util.SortedSet +import java.util.TreeSet +import java.util.regex.Pattern + +/** + * @author Sindre Mehus + */ +object FileUtil { + private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|") + private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|") + private val MUSIC_FILE_EXTENSIONS = + Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus") + private val VIDEO_FILE_EXTENSIONS = + Arrays.asList("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 val permissionUtil = 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 (!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("%s.m3u", fileSystemSafe(name))) + } + + @JvmStatic + val playlistDirectory: File + get() { + val playlistDir = File(ultrasonicDirectory, "playlists") + ensureDirectoryExistsAndIsReadWritable(playlistDir) + return playlistDir + } + + fun getPlaylistDirectory(server: String?): File { + val playlistDir = File(playlistDirectory, 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 + */ + 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? { + val 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 + */ + fun getAlbumArtKey(albumDir: File?, large: Boolean): String? { + if (albumDir == null) { + return null + } + val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL + return String.format(Locale.ROOT, "%s%s", md5Hex(albumDir.path), suffix) + } + + fun getAvatarFile(username: String?): File? { + val albumArtDir = albumArtDirectory + if (albumArtDir == null || username == null) { + return null + } + val md5Hex = md5Hex(username) + return 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 + */ + fun getAlbumArtFile(albumDir: File?): File? { + val albumArtDir = albumArtDirectory + val key = getAlbumArtKey(albumDir, true) + return if (key == null || albumArtDir == null) { + null + } else 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 + */ + fun getAlbumArtFile(cacheKey: String?): File? { + val albumArtDir = albumArtDirectory + return if (albumArtDir == null || 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? { + if (entry == null) { + return null + } + val dir: File + if (!TextUtils.isEmpty(entry.path)) { + val f = File(fileSystemSafeDir(entry.path)) + dir = File( + String.format( + "%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("%s/%s/%s", musicDirectory.path, artist, album)) + } + return dir + } + + fun createDirectoryForParent(file: File) { + val dir = file.parentFile + if (!dir.exists()) { + if (!dir.mkdirs()) { + Timber.e("Failed to create directory %s", dir) + } + } + } + + 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() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File( + Environment.getExternalStorageDirectory(), + "Android/data/org.moire.ultrasonic" + ) else 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 defaultMusicDirectory = defaultMusicDirectory + val path = getPreferences().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 + 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 filename The filename in question. + * @return The filename with special characters replaced by hyphens. + */ + private fun fileSystemSafe(filename: String): String { + var filename = filename + if (filename == null || filename.trim { it <= ' ' }.isEmpty()) { + return "unnamed" + } + 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 path = path + if (path == null || path.trim { it <= ' ' }.isEmpty()) { + return "" + } + for (s in FILE_SYSTEM_UNSAFE_DIR) { + path = path!!.replace(s, "-") + } + return path + } + + /** + * 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(Arrays.asList(*files)) + } + + 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).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. + */ + 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("%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("%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 (x: Throwable) { + Timber.w("Failed to serialize object to %s", file) + false + } finally { + close(out) + } + } + + @JvmStatic + fun deserialize(context: Context, fileName: String?): T? { + val file = File(context.cacheDir, fileName) + if (!file.exists() || !file.isFile) { + return null + } + var `in`: ObjectInputStream? = null + return try { + `in` = ObjectInputStream(FileInputStream(file)) + val `object` = `in`.readObject() + val result = `object` as T + Timber.i("Deserialized object from %s", file) + result + } catch (x: Throwable) { + Timber.w(x, "Failed to deserialize object from %s", file) + null + } finally { + close(`in`) + } + } +} \ No newline at end of file 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 c770efe4..6ca87dfa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -306,7 +306,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/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/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/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/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" + From 5ff4d21abcb78a8a7230271600ed9a011454902c Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 12 Sep 2021 12:01:24 +0200 Subject: [PATCH 04/12] Merge FileUtil functions into a single class. --- detekt-config.yml | 2 +- .../moire/ultrasonic/service/DownloadFile.kt | 12 +- .../ultrasonic/service/RESTMusicService.kt | 3 +- .../org/moire/ultrasonic/util/FileUtil.kt | 206 ++++++++++-------- .../org/moire/ultrasonic/util/FileUtilKt.kt | 47 ---- .../kotlin/org/moire/ultrasonic/util/Util.kt | 3 - 6 files changed, 124 insertions(+), 149 deletions(-) rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/util/FileUtil.kt (69%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt 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/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index de341825..c386ae2c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -10,6 +10,12 @@ package org.moire.ultrasonic.service import android.net.wifi.WifiManager.WifiLock import android.text.TextUtils import androidx.lifecycle.MutableLiveData +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.io.RandomAccessFile import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.domain.MusicDirectory @@ -20,12 +26,6 @@ import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.io.RandomAccessFile /** * This class represents a singe Song or Video that can be downloaded. 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 2e850f8a..4981992b 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.Util 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/java/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt similarity index 69% rename from ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index 62cbcaa0..57f7d7b5 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -1,61 +1,47 @@ /* - 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 + * 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 org.koin.java.KoinJavaComponent.inject -import org.moire.ultrasonic.app.UApp.Companion.applicationContext -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.util.Util.close -import org.moire.ultrasonic.util.Util.getPreferences -import org.moire.ultrasonic.util.Util.md5Hex -import timber.log.Timber +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.Arrays 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 -/** - * @author Sindre Mehus - */ object FileUtil { + private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|") private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|") private val MUSIC_FILE_EXTENSIONS = - Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus") + listOf("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus") private val VIDEO_FILE_EXTENSIONS = - Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv") + 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 val permissionUtil = inject( + private val permissionUtil = KoinJavaComponent.inject( PermissionUtil::class.java ) @@ -71,8 +57,8 @@ object FileUtil { val fileName = StringBuilder(256) val track = song.track - //check if filename already had track number - if (!TITLE_WITH_TRACK.matcher(song.title).matches()) { + // 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') @@ -90,13 +76,13 @@ object FileUtil { } @JvmStatic - fun getPlaylistFile(server: String?, name: String): File { + fun getPlaylistFile(server: String?, name: String): File { val playlistDir = getPlaylistDirectory(server) - return File(playlistDir, String.format("%s.m3u", fileSystemSafe(name))) + return File(playlistDir, String.format(Locale.ROOT, "%s.m3u", fileSystemSafe(name))) } @JvmStatic - val playlistDirectory: File + val playlistDirectory: File get() { val playlistDir = File(ultrasonicDirectory, "playlists") ensureDirectoryExistsAndIsReadWritable(playlistDir) @@ -104,7 +90,12 @@ object FileUtil { } fun getPlaylistDirectory(server: String?): File { - val playlistDir = File(playlistDirectory, server) + val playlistDir: File + if (server != null) { + playlistDir = File(playlistDirectory, server) + } else { + playlistDir = playlistDirectory + } ensureDirectoryExistsAndIsReadWritable(playlistDir) return playlistDir } @@ -136,21 +127,21 @@ object FileUtil { * @param large Whether to get the key for the large or the default image * @return String The hash key */ - fun getAlbumArtKey(albumDir: File?, large: Boolean): String? { + private fun getAlbumArtKey(albumDir: File?, large: Boolean): String? { if (albumDir == null) { return null } val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL - return String.format(Locale.ROOT, "%s%s", md5Hex(albumDir.path), suffix) + return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.path), suffix) } fun getAvatarFile(username: String?): File? { - val albumArtDir = albumArtDirectory - if (albumArtDir == null || username == null) { + if (username == null) { return null } - val md5Hex = md5Hex(username) - return File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE)) + val albumArtDir = albumArtDirectory + val md5Hex = Util.md5Hex(username) + return File(albumArtDir, String.format(Locale.ROOT, "%s%s", md5Hex, SUFFIX_LARGE)) } /** @@ -161,7 +152,7 @@ object FileUtil { fun getAlbumArtFile(albumDir: File?): File? { val albumArtDir = albumArtDirectory val key = getAlbumArtKey(albumDir, true) - return if (key == null || albumArtDir == null) { + return if (key == null) { null } else File(albumArtDir, key) } @@ -173,7 +164,7 @@ object FileUtil { */ fun getAlbumArtFile(cacheKey: String?): File? { val albumArtDir = albumArtDirectory - return if (albumArtDir == null || cacheKey == null) { + return if (cacheKey == null) { null } else File(albumArtDir, cacheKey) } @@ -195,9 +186,10 @@ object FileUtil { val f = File(fileSystemSafeDir(entry.path)) dir = File( String.format( + Locale.ROOT, "%s/%s", musicDirectory.path, - if (entry.isDirectory) f.path else f.parent + if (entry.isDirectory) f.path else f.parent!! ) ) } else { @@ -206,20 +198,21 @@ object FileUtil { if ("unnamed" == album) { album = fileSystemSafe(entry.title!!) } - dir = File(String.format("%s/%s/%s", musicDirectory.path, artist, album)) + 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.exists()) { + 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()) { @@ -228,34 +221,36 @@ object FileUtil { 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? + // 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() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File( Environment.getExternalStorageDirectory(), "Android/data/org.moire.ultrasonic" - ) else applicationContext().getExternalFilesDir(null) + ) 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 + // 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 + val musicDirectory: File get() { - val defaultMusicDirectory = defaultMusicDirectory - val path = getPreferences().getString( - Constants.PREFERENCES_KEY_CACHE_LOCATION, - defaultMusicDirectory.path - ) - val dir = File(path) + val path = Util.getPreferences() + .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 - fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean { + @Suppress("ReturnCount") + fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean { if (dir == null) { return false } @@ -287,12 +282,12 @@ object FileUtil { * Makes a given filename safe by replacing special characters like slashes ("/" and "\") * with dashes ("-"). * - * @param filename The filename in question. + * @param name The filename in question. * @return The filename with special characters replaced by hyphens. */ - private fun fileSystemSafe(filename: String): String { - var filename = filename - if (filename == null || filename.trim { it <= ' ' }.isEmpty()) { + private fun fileSystemSafe(name: String): String { + var filename = name + if (filename.trim { it <= ' ' }.isEmpty()) { return "unnamed" } for (s in FILE_SYSTEM_UNSAFE) { @@ -308,29 +303,29 @@ object FileUtil { * @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 path = path - if (path == null || path.trim { it <= ' ' }.isEmpty()) { + private fun fileSystemSafeDir(path: String?): String { + var filepath = path + if (filepath == null || filepath.trim { it <= ' ' }.isEmpty()) { return "" } for (s in FILE_SYSTEM_UNSAFE_DIR) { - path = path!!.replace(s, "-") + filepath = filepath!!.replace(s, "-") } - return path + 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 { + @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(Arrays.asList(*files)) + return TreeSet(files.asList()) } fun listMediaFiles(dir: File): SortedSet { @@ -347,7 +342,8 @@ object FileUtil { private fun isMediaFile(file: File): Boolean { val extension = getExtension(file.name) - return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension) + return MUSIC_FILE_EXTENSIONS.contains(extension) || + VIDEO_FILE_EXTENSIONS.contains(extension) } fun isPlaylistFile(file: File): Boolean { @@ -364,7 +360,7 @@ object FileUtil { */ fun getExtension(name: String): String { val index = name.lastIndexOf('.') - return if (index == -1) "" else name.substring(index + 1).toLowerCase() + return if (index == -1) "" else name.substring(index + 1).lowercase(Locale.ROOT) } /** @@ -386,7 +382,7 @@ object FileUtil { * @return The .partial file name */ fun getPartialFile(name: String): String { - return String.format("%s.partial.%s", getBaseName(name), getExtension(name)) + return String.format(Locale.ROOT, "%s.partial.%s", getBaseName(name), getExtension(name)) } /** @@ -396,11 +392,11 @@ object FileUtil { * @return The .complete file name */ fun getCompleteFile(name: String): String { - return String.format("%s.complete.%s", getBaseName(name), getExtension(name)) + return String.format(Locale.ROOT, "%s.complete.%s", getBaseName(name), getExtension(name)) } @JvmStatic - fun serialize(context: Context, obj: T, fileName: String?): Boolean { + fun serialize(context: Context, obj: T, fileName: String): Boolean { val file = File(context.cacheDir, fileName) var out: ObjectOutputStream? = null return try { @@ -408,32 +404,62 @@ object FileUtil { out.writeObject(obj) Timber.i("Serialized object to %s", file) true - } catch (x: Throwable) { + } catch (ignored: Exception) { Timber.w("Failed to serialize object to %s", file) false } finally { - close(out) + Util.close(out) } } + @Suppress("UNCHECKED_CAST") @JvmStatic - fun deserialize(context: Context, fileName: String?): T? { + fun deserialize(context: Context, fileName: String): T? { val file = File(context.cacheDir, fileName) if (!file.exists() || !file.isFile) { return null } - var `in`: ObjectInputStream? = null + var inStream: ObjectInputStream? = null return try { - `in` = ObjectInputStream(FileInputStream(file)) - val `object` = `in`.readObject() - val result = `object` as T + inStream = ObjectInputStream(FileInputStream(file)) + val readObject = inStream.readObject() + val result = readObject as T Timber.i("Deserialized object from %s", file) result - } catch (x: Throwable) { - Timber.w(x, "Failed to deserialize object from %s", file) + } catch (all: Throwable) { + Timber.w(all, "Failed to deserialize object from %s", file) null } finally { - close(`in`) + Util.close(inStream) } } -} \ No newline at end of file + + 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/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index ed847ffd..9105d461 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -40,14 +40,11 @@ import android.widget.Toast import androidx.annotation.AnyRes import androidx.media.utils.MediaConstants import androidx.preference.PreferenceManager -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 From 611539be556595bc1c9004d34123c0a659575c46 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 12 Sep 2021 13:45:00 +0200 Subject: [PATCH 05/12] Handle WifiLock in Download manager class, instead of creating an individual lock per task. Also only stop the executor when done. --- .../moire/ultrasonic/service/Downloader.kt | 46 ++++++++++++++----- .../moire/ultrasonic/service/DownloadFile.kt | 10 +--- .../ultrasonic/service/MediaPlayerService.kt | 1 - 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt index 3c8fbf73..3842be8d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/java/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 @@ -11,6 +12,7 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.util.LRUCache import org.moire.ultrasonic.util.ShufflePlayBuffer +import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.getMaxSongs import org.moire.ultrasonic.util.Util.getPreloadCount import org.moire.ultrasonic.util.Util.isExternalStoragePresent @@ -39,27 +41,21 @@ 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() @@ -67,16 +63,39 @@ class Downloader( Timber.i("Downloader destroyed") } + fun start() { + 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() + executorService?.shutdown() + executorService = null + wifiLock?.release() + wifiLock = null 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 (!isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable) { return @@ -126,6 +145,11 @@ class Downloader( localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING) } } + + // Stop Executor service when done downloading + if (activelyDownloading.size == 0) { + stop() + } } private fun startDownloadOnService(task: DownloadFile) { 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 c386ae2c..cccbc498 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -28,10 +28,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, @@ -204,11 +201,7 @@ class DownloadFile( override fun execute() { var inputStream: InputStream? = null var outputStream: FileOutputStream? = null - var wifiLock: WifiLock? = null try { - wifiLock = Util.createWifiLock(toString()) - wifiLock.acquire() - if (saveFile.exists()) { Timber.i("%s already exists. Skipping.", saveFile) return @@ -300,7 +293,6 @@ class DownloadFile( } finally { Util.close(inputStream) Util.close(outputStream) - wifiLock?.release() CacheCleaner().cleanSpace() downloader.checkDownloads() } 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 b76b0b93..5d1f5709 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -81,7 +81,6 @@ class MediaPlayerService : Service() { override fun onCreate() { super.onCreate() - downloader.onCreate() shufflePlayBuffer.onCreate() localMediaPlayer.init() From 28097bf32501dbff45b78efa7b884f66b86d3bad Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 12 Sep 2021 14:03:39 +0200 Subject: [PATCH 06/12] Optimize Imageloader to not create empty MusicDirectory.Entries --- .../ultrasonic/fragment/ArtistRowAdapter.kt | 15 ++-- .../ultrasonic/imageloader/ImageLoader.kt | 21 +++++- .../org/moire/ultrasonic/util/FileUtil.kt | 74 +++++++++++-------- 3 files changed, 71 insertions(+), 39 deletions(-) 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 0b375bfc..274af8cb 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.Util /** @@ -59,13 +59,14 @@ class ArtistRowAdapter( if (Util.getShouldShowArtistPicture()) { 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/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 6d24a54f..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, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index 57f7d7b5..573a785f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -41,6 +41,8 @@ object FileUtil { 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 ) @@ -50,7 +52,7 @@ object FileUtil { // 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) + if (song.id.startsWith(dir.absolutePath)) return File(song.id) } // Generate a file name for the song @@ -66,7 +68,7 @@ object FileUtil { fileName.append(track).append('-') } } - fileName.append(fileSystemSafe(song.title!!)).append('.') + fileName.append(fileSystemSafe(song.title)).append('.') if (!TextUtils.isEmpty(song.transcodedSuffix)) { fileName.append(song.transcodedSuffix) } else { @@ -76,7 +78,7 @@ object FileUtil { } @JvmStatic - fun getPlaylistFile(server: String?, name: String): File { + fun getPlaylistFile(server: String?, name: String?): File { val playlistDir = getPlaylistDirectory(server) return File(playlistDir, String.format(Locale.ROOT, "%s.m3u", fileSystemSafe(name))) } @@ -89,7 +91,8 @@ object FileUtil { return playlistDir } - fun getPlaylistDirectory(server: String?): File { + @JvmStatic + fun getPlaylistDirectory(server: String? = null): File { val playlistDir: File if (server != null) { playlistDir = File(playlistDirectory, server) @@ -105,7 +108,7 @@ object FileUtil { * @param entry The album entry * @return File object. Not guaranteed that it exists */ - fun getAlbumArtFile(entry: MusicDirectory.Entry?): File? { + fun getAlbumArtFile(entry: MusicDirectory.Entry): File { val albumDir = getAlbumDirectory(entry) return getAlbumArtFile(albumDir) } @@ -117,20 +120,30 @@ object FileUtil { * @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? { - if (albumDir == null) { - return null - } + 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) } @@ -149,12 +162,11 @@ object FileUtil { * @param albumDir The album directory * @return File object. Not guaranteed that it exists */ - fun getAlbumArtFile(albumDir: File?): File? { + @JvmStatic + fun getAlbumArtFile(albumDir: File): File { val albumArtDir = albumArtDirectory val key = getAlbumArtKey(albumDir, true) - return if (key == null) { - null - } else File(albumArtDir, key) + return File(albumArtDir, key) } /** @@ -162,6 +174,7 @@ object FileUtil { * @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) { @@ -177,10 +190,7 @@ object FileUtil { return albumArtDir } - fun getAlbumDirectory(entry: MusicDirectory.Entry?): File? { - if (entry == null) { - return null - } + fun getAlbumDirectory(entry: MusicDirectory.Entry): File { val dir: File if (!TextUtils.isEmpty(entry.path)) { val f = File(fileSystemSafeDir(entry.path)) @@ -193,10 +203,10 @@ object FileUtil { ) ) } else { - val artist = fileSystemSafe(entry.artist!!) - var album = fileSystemSafe(entry.album!!) - if ("unnamed" == album) { - album = fileSystemSafe(entry.title!!) + 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)) } @@ -225,11 +235,13 @@ object FileUtil { // GetExternalFilesDir will always return a directory which Ultrasonic // can access without any extra privileges. @JvmStatic - val ultrasonicDirectory: File? - get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File( - Environment.getExternalStorageDirectory(), - "Android/data/org.moire.ultrasonic" - ) else UApp.applicationContext().getExternalFilesDir(null) + 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 @@ -237,6 +249,7 @@ object FileUtil { @JvmStatic val defaultMusicDirectory: File get() = getOrCreateDirectory("music") + @JvmStatic val musicDirectory: File get() { @@ -285,11 +298,12 @@ object FileUtil { * @param name The filename in question. * @return The filename with special characters replaced by hyphens. */ - private fun fileSystemSafe(name: String): String { - var filename = name - if (filename.trim { it <= ' ' }.isEmpty()) { - return "unnamed" + 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, "-") } From ecc7e870f1e8c49ef553421a2e8bc600011fa236 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 12 Sep 2021 14:36:33 +0200 Subject: [PATCH 07/12] Save Artists in Offline database --- .../moire/ultrasonic/service/Downloader.kt | 1 - .../ultrasonic/data/ActiveServerProvider.kt | 24 +++++++++++++++---- .../org/moire/ultrasonic/data/ArtistsDao.kt | 16 +++++++++++++ .../org/moire/ultrasonic/data/BasicDaos.kt | 10 ++++++++ .../org/moire/ultrasonic/data/MetaDatabase.kt | 4 ++++ .../moire/ultrasonic/service/DownloadFile.kt | 17 ++++++++++++- 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt index 3842be8d..917493f0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt @@ -43,7 +43,6 @@ class Downloader( private var executorService: ScheduledExecutorService? = null private var wifiLock: WifiManager.WifiLock? = null - var playlistUpdateRevision: Long = 0 private set 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 d1a5c18b..758fa74d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -17,6 +17,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 @@ -29,9 +31,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!! @@ -94,16 +95,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/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index cccbc498..0c896d14 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -7,7 +7,6 @@ package org.moire.ultrasonic.service -import android.net.wifi.WifiManager.WifiLock import android.text.TextUtils import androidx.lifecycle.MutableLiveData import java.io.File @@ -18,6 +17,7 @@ import java.io.OutputStream import java.io.RandomAccessFile import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.subsonic.ImageLoaderProvider @@ -56,6 +56,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) @@ -266,6 +267,11 @@ class DownloadFile( if (isCancelled) { throw Exception(String.format("Download of '%s' was cancelled", song)) } + + if (song.artistId != null) { + cacheMetadata(song.artistId!!) + } + downloadAndSaveCoverArt() } @@ -302,6 +308,15 @@ class DownloadFile( 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 + val artist = activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId) + activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist) + } + private fun downloadAndSaveCoverArt() { try { if (!TextUtils.isEmpty(song.coverArt)) { From 02129a8fd084107044b57d99328287bde605c5e5 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 15 Sep 2021 21:34:48 +0200 Subject: [PATCH 08/12] Remove unused strings --- ultrasonic/src/main/res/values-cs/strings.xml | 2 -- ultrasonic/src/main/res/values-de/strings.xml | 2 -- ultrasonic/src/main/res/values-es/strings.xml | 2 -- ultrasonic/src/main/res/values-fr/strings.xml | 2 -- ultrasonic/src/main/res/values-hu/strings.xml | 2 -- ultrasonic/src/main/res/values-it/strings.xml | 2 -- ultrasonic/src/main/res/values-nl/strings.xml | 2 -- ultrasonic/src/main/res/values-pl/strings.xml | 2 -- ultrasonic/src/main/res/values-pt-rBR/strings.xml | 2 -- ultrasonic/src/main/res/values-pt/strings.xml | 2 -- ultrasonic/src/main/res/values-ru/strings.xml | 2 -- ultrasonic/src/main/res/values/strings.xml | 2 -- 12 files changed, 24 deletions(-) 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 e7be8a28..9d8d2630 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 From 01aa1fe88747fdb80d7a0798e27304e9efa36e8a Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 21 Sep 2021 15:20:56 +0200 Subject: [PATCH 09/12] Moved kotlin files to their place --- .../org/moire/ultrasonic/fragment/MainFragment.kt | 0 .../{java => kotlin}/org/moire/ultrasonic/service/Downloader.kt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/fragment/MainFragment.kt (100%) rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/service/Downloader.kt (100%) 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/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt similarity index 100% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt From be49145aa8a1e9aecac92773c4e464b609bee582 Mon Sep 17 00:00:00 2001 From: Nite Date: Thu, 23 Sep 2021 15:51:53 +0200 Subject: [PATCH 10/12] Created different notification when Ultrasonic is only downloading files Fixed null check --- .../moire/ultrasonic/service/Downloader.kt | 5 ++++ .../ultrasonic/service/MediaPlayerService.kt | 27 +++++++++++++------ .../org/moire/ultrasonic/util/FileUtil.kt | 2 +- ultrasonic/src/main/res/values/strings.xml | 1 + 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 917493f0..10f50699 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -33,6 +33,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() @@ -63,6 +65,7 @@ class Downloader( } fun start() { + started = true if (executorService == null) { executorService = Executors.newSingleThreadScheduledExecutor() executorService!!.scheduleWithFixedDelay( @@ -78,10 +81,12 @@ class Downloader( } fun stop() { + started = false executorService?.shutdown() executorService = null wifiLock?.release() wifiLock = null + MediaPlayerService.runningInstance?.notifyDownloaderStopped() Timber.i("Downloader stopped") } 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 5d1f5709..57874587 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -157,6 +157,10 @@ class MediaPlayerService : Service() { } } + fun notifyDownloaderStopped() { + stopIfIdle() + } + @Synchronized fun seekTo(position: Int) { if (jukeboxMediaPlayer.isEnabled) { @@ -582,15 +586,14 @@ 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) @@ -598,6 +601,14 @@ class MediaPlayerService : Service() { 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/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index 573a785f..043a2400 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -199,7 +199,7 @@ object FileUtil { Locale.ROOT, "%s/%s", musicDirectory.path, - if (entry.isDirectory) f.path else f.parent!! + if (entry.isDirectory) f.path else f.parent ?: "" ) ) } else { diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 9d8d2630..040a8c9d 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -400,6 +400,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 From d4ce10ebfae34d60449144dfe8c3dcdba47588fa Mon Sep 17 00:00:00 2001 From: Nite Date: Thu, 23 Sep 2021 16:00:20 +0200 Subject: [PATCH 11/12] fixed lint error --- .../kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 57874587..15092350 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -600,9 +600,7 @@ class MediaPlayerService : Service() { notificationBuilder!!.setContentText(song.artist) notificationBuilder!!.setLargeIcon(bitmap) notificationBuilder!!.setSubText(song.album) - } - else if (downloader.started) - { + } else if (downloader.started) { // No song is playing, but Ultrasonic is downloading files notificationBuilder!!.setContentTitle( getString(R.string.notification_downloading_title) From 35a0dd761da774c13fd0ec2d06f747b016e37f9f Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Oct 2021 11:14:49 +0200 Subject: [PATCH 12/12] Fix a bug where newly added Artists were not cached correctly --- .../moire/ultrasonic/service/DownloadFile.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 758496ed..2ddf1bc4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -18,6 +18,7 @@ import java.io.RandomAccessFile import org.koin.core.component.KoinComponent import org.koin.core.component.inject 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 @@ -200,6 +201,8 @@ class DownloadFile( } private inner class DownloadTask : CancellableTask() { + val musicService = getMusicService() + override fun execute() { var inputStream: InputStream? = null var outputStream: FileOutputStream? = null @@ -222,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 @@ -314,8 +315,22 @@ class DownloadFile( // Once the albums are cached in db, we should retrieve the album, // and then cache the album artist. if (artistId.isEmpty()) return - val artist = activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId) - activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist) + 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() {