1
0
mirror of https://github.com/ultrasonic/ultrasonic synced 2025-02-17 04:00:39 +01:00

Merge pull request #501 from tzugen/impl

Migrate MediaPlayerController to Kotlin
This commit is contained in:
Nite 2021-05-24 09:23:53 +02:00 committed by GitHub
commit 04b4b154e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 800 additions and 1172 deletions

View File

@ -25,7 +25,7 @@
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.&lt;no name provided&gt;$String.format("%s\n\n%s", Util.getShareGreeting(context), result.url)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.&lt;no name provided&gt;$String.format("%s\n\n%s", Util.getShareGreeting(), result.url)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
@ -46,20 +46,15 @@
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$1000L</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$1000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$1000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1000L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$1000</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>

View File

@ -63,7 +63,8 @@ style:
excludePackageStatements: false
excludeImportStatements: false
MagicNumber:
ignoreNumbers: ['-1', '0', '1', '2', '100']
# 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '100', '1000']
ignoreEnums: true
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:

View File

@ -56,6 +56,7 @@ android {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xopt-in=org.koin.core.component.KoinApiExtension"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -26,45 +26,45 @@
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String lhs = lhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
errorLine1=" String lhs = lhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="103"
column="38"/>
line="97"
column="37"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String rhs = rhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
errorLine1=" String rhs = rhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="104"
column="38"/>
line="98"
column="37"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" int index = lhs.indexOf(String.format(&quot;%s &quot;, article.toLowerCase()));"
errorLine2=" ~~~~~~~~~~~">
errorLine1=" int index = lhs.indexOf(String.format(&quot;%s &quot;, article.toLowerCase()));"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="121"
column="59"/>
line="115"
column="58"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" index = rhs.indexOf(String.format(&quot;%s &quot;, article.toLowerCase()));"
errorLine2=" ~~~~~~~~~~~">
errorLine1=" index = rhs.indexOf(String.format(&quot;%s &quot;, article.toLowerCase()));"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="128"
column="55"/>
line="122"
column="54"/>
</issue>
<issue
@ -74,7 +74,7 @@
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="486"
line="466"
column="38"/>
</issue>
@ -85,7 +85,7 @@
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="488"
line="468"
column="43"/>
</issue>
@ -238,7 +238,7 @@
errorLine2=" ~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java"
line="483"
line="476"
column="58"/>
</issue>
@ -491,7 +491,7 @@
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="653"
line="633"
column="37"/>
</issue>
@ -579,39 +579,6 @@
file="src/main/res/drawable-xhdpi-v14"/>
</issue>
<issue
id="StaticFieldLeak"
message="This `AsyncTask` class should be static or leaks might occur (org.moire.ultrasonic.util.CacheCleaner.BackgroundCleanup)"
errorLine1=" private class BackgroundCleanup extends AsyncTask&lt;Void, Void, Void>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/util/CacheCleaner.java"
line="225"
column="16"/>
</issue>
<issue
id="StaticFieldLeak"
message="This `AsyncTask` class should be static or leaks might occur (org.moire.ultrasonic.util.CacheCleaner.BackgroundSpaceCleanup)"
errorLine1=" private class BackgroundSpaceCleanup extends AsyncTask&lt;Void, Void, Void>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/util/CacheCleaner.java"
line="253"
column="16"/>
</issue>
<issue
id="StaticFieldLeak"
message="This `AsyncTask` class should be static or leaks might occur (org.moire.ultrasonic.util.CacheCleaner.BackgroundPlaylistsCleanup)"
errorLine1=" private class BackgroundPlaylistsCleanup extends AsyncTask&lt;List&lt;Playlist>, Void, Void>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/util/CacheCleaner.java"
line="282"
column="16"/>
</issue>
<issue
id="StaticFieldLeak"
message="This `AsyncTask` class should be static or leaks might occur (org.moire.ultrasonic.service.DownloadQueueSerializer.SerializeTask)"
@ -2117,13 +2084,6 @@
file="src/main/res/drawable-hdpi/list_pressed_holo_dark.9.png"/>
</issue>
<issue
id="IconMissingDensityFolder"
message="Missing density variation folders in `/home/xyz/08-Tech/02-Android/ultrasonic/ultrasonic/src/main/res`: drawable-mdpi, drawable-xhdpi, drawable-xxhdpi">
<location
file="src/main/res"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"

View File

@ -235,10 +235,11 @@ public class MainFragment extends Fragment {
Navigation.findNavController(getView()).navigate(R.id.mainToArtistList, bundle);
}
private void showAlbumList(final String type, final int title) {
private void showAlbumList(final String type, final int titleIndex) {
Bundle bundle = new Bundle();
String title = getContext().getResources().getString(titleIndex, "");
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.mainToAlbumList, bundle);

View File

@ -131,7 +131,7 @@ public class PlaylistsFragment extends Fragment {
List<Playlist> playlists = musicService.getPlaylists(refresh);
if (!ActiveServerProvider.Companion.isOffline())
new CacheCleaner(getContext()).cleanPlaylists(playlists);
new CacheCleaner().cleanPlaylists(playlists);
return playlists;
}

View File

@ -139,7 +139,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
setupServersCategory();
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
sharingDefaultGreeting.setText(Util.getShareGreeting());
setupClearSearchPreference();
setupGaplessControlSettingsV14();
setupFeatureFlagsPreferences();

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.service;
import android.content.Context;
import timber.log.Timber;
import org.moire.ultrasonic.domain.MusicDirectory;
@ -35,20 +34,18 @@ public class Downloader
private final ShufflePlayBuffer shufflePlayBuffer;
private final ExternalStorageMonitor externalStorageMonitor;
private final LocalMediaPlayer localMediaPlayer;
private final Context context;
// TODO: This is a circular reference, try to remove
private Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final List<DownloadFile> cleanupCandidates = new ArrayList<>();
private final LRUCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LRUCache<>(100);
private ScheduledExecutorService executorService;
private long revision;
public Downloader(Context context, ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor,
public Downloader(ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor,
LocalMediaPlayer localMediaPlayer)
{
this.context = context;
this.shufflePlayBuffer = shufflePlayBuffer;
this.externalStorageMonitor = externalStorageMonitor;
this.localMediaPlayer = localMediaPlayer;
@ -56,19 +53,14 @@ public class Downloader
public void onCreate()
{
Runnable downloadChecker = new Runnable()
{
@Override
public void run()
Runnable downloadChecker = () -> {
try
{
try
{
checkDownloads();
}
catch (Throwable x)
{
Timber.e(x,"checkDownloads() failed.");
}
checkDownloads();
}
catch (Throwable x)
{
Timber.e(x,"checkDownloads() failed.");
}
};
@ -100,10 +92,10 @@ public class Downloader
if (shufflePlayBuffer.isEnabled)
{
checkShufflePlay(context);
checkShufflePlay();
}
if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected(context))
if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected())
{
return;
}
@ -188,7 +180,7 @@ public class Downloader
DownloadFile downloadFile = backgroundDownloadList.get(i);
if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()))
{
Util.scanMedia(context, downloadFile.getCompleteFile());
Util.scanMedia(downloadFile.getCompleteFile());
// Don't need to keep list like active song list
backgroundDownloadList.remove(i);
@ -316,7 +308,7 @@ public class Downloader
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(context, song, save);
DownloadFile downloadFile = new DownloadFile(song, save);
downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
offset++;
}
@ -325,7 +317,7 @@ public class Downloader
{
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(context, song, save);
DownloadFile downloadFile = new DownloadFile(song, save);
downloadList.add(downloadFile);
}
}
@ -336,7 +328,7 @@ public class Downloader
{
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(context, song, save);
DownloadFile downloadFile = new DownloadFile(song, save);
backgroundDownloadList.add(downloadFile);
}
@ -376,7 +368,7 @@ public class Downloader
DownloadFile downloadFile = downloadFileCache.get(song);
if (downloadFile == null)
{
downloadFile = new DownloadFile(context, song, false);
downloadFile = new DownloadFile(song, false);
downloadFileCache.put(song, downloadFile);
}
return downloadFile;
@ -398,7 +390,7 @@ public class Downloader
}
}
private synchronized void checkShufflePlay(Context context)
private synchronized void checkShufflePlay()
{
// Get users desired random playlist size
int listSize = Util.getMaxSongs();
@ -412,7 +404,7 @@ public class Downloader
{
for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size))
{
DownloadFile downloadFile = new DownloadFile(context, song, false);
DownloadFile downloadFile = new DownloadFile(song, false);
downloadList.add(downloadFile);
revision++;
}
@ -426,7 +418,7 @@ public class Downloader
int songsToShift = currIndex - 2;
for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift))
{
downloadList.add(new DownloadFile(context, song, false));
downloadList.add(new DownloadFile(song, false));
downloadList.get(0).cancelDownload();
downloadList.remove(0);
revision++;

View File

@ -4,6 +4,9 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.moire.ultrasonic.app.UApp;
import timber.log.Timber;
/**
@ -11,15 +14,9 @@ import timber.log.Timber;
*/
public class ExternalStorageMonitor
{
private Context context;
private BroadcastReceiver ejectEventReceiver;
private boolean externalStorageAvailable = true;
public ExternalStorageMonitor(Context context)
{
this.context = context;
}
public void onCreate(final Runnable ejectedCallback)
{
// Stop when SD card is ejected.
@ -44,12 +41,12 @@ public class ExternalStorageMonitor
IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
ejectFilter.addDataScheme("file");
context.registerReceiver(ejectEventReceiver, ejectFilter);
UApp.Companion.applicationContext().registerReceiver(ejectEventReceiver, ejectFilter);
}
public void onDestroy()
{
context.unregisterReceiver(ejectEventReceiver);
UApp.Companion.applicationContext().unregisterReceiver(ejectEventReceiver);
}
public boolean isExternalStorageAvailable() { return externalStorageAvailable; }

View File

@ -30,6 +30,7 @@ import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.JukeboxStatus;
import org.moire.ultrasonic.domain.PlayerState;
@ -70,10 +71,9 @@ public class JukeboxMediaPlayer
private final AtomicBoolean running = new AtomicBoolean();
private Thread serviceThread;
private boolean enabled = false;
private final Context context;
// TODO: These create circular references, try to refactor
private final Lazy<MediaPlayerControllerImpl> mediaPlayerControllerLazy = inject(MediaPlayerControllerImpl.class);
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Downloader downloader;
// TODO: Report warning if queue fills up.
@ -82,9 +82,8 @@ public class JukeboxMediaPlayer
// TODO: Persist RC state?
// TODO: Minimize status updates.
public JukeboxMediaPlayer(Context context, Downloader downloader)
public JukeboxMediaPlayer(Downloader downloader)
{
this.context = context;
this.downloader = downloader;
}
@ -217,15 +216,8 @@ public class JukeboxMediaPlayer
private void disableJukeboxOnError(Throwable x, final int resourceId)
{
Timber.w(x.toString());
new Handler().post(new Runnable()
{
@Override
public void run()
{
Util.toast(context, resourceId, false);
}
});
Context context = UApp.Companion.applicationContext();
new Handler().post(() -> Util.toast(context, resourceId, false));
mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false);
}
@ -293,6 +285,7 @@ public class JukeboxMediaPlayer
tasks.remove(SetGain.class);
tasks.add(new SetGain(gain));
Context context = UApp.Companion.applicationContext();
if (volumeToast == null) volumeToast = new VolumeToast(context);
volumeToast.setVolume(gain);

View File

@ -1,129 +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 <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.service;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import java.util.List;
/**
* This interface contains all functions which are necessary for the Application UI
* to control the Media Player implementation.
*
* @author Sindre Mehus
* @version $Id$
*/
public interface MediaPlayerController
{
void download(List<Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist);
void downloadBackground(List<Entry> songs, boolean save);
void setShufflePlayEnabled(boolean enabled);
boolean isShufflePlayEnabled();
void shuffle();
RepeatMode getRepeatMode();
void setRepeatMode(RepeatMode repeatMode);
boolean getKeepScreenOn();
void setKeepScreenOn(boolean screenOn);
boolean getShowVisualization();
void setShowVisualization(boolean showVisualization);
void clear();
void clearIncomplete();
void remove(DownloadFile downloadFile);
void play(int index);
void seekTo(int position);
void previous();
void next();
void pause();
void stop();
void start();
void reset();
PlayerState getPlayerState();
int getPlayerPosition();
int getPlayerDuration();
void delete(List<Entry> songs);
void unpin(List<Entry> songs);
void setSuggestedPlaylistName(String name);
String getSuggestedPlaylistName();
boolean isJukeboxEnabled();
boolean isJukeboxAvailable();
void setJukeboxEnabled(boolean b);
void adjustJukeboxVolume(boolean up);
void togglePlayPause();
void setVolume(float volume);
void restore(List<Entry> songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist);
void stopJukeboxService();
void updateNotification();
void setSongRating(final int rating);
DownloadFile getCurrentPlaying();
int getPlaylistSize();
int getCurrentPlayingNumberOnPlaylist();
DownloadFile getCurrentDownloading();
List<DownloadFile> getPlayList();
long getPlayListUpdateRevision();
long getPlayListDuration();
DownloadFile getDownloadFileForSong(Entry song);
}

View File

@ -1,661 +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 <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.service;
import android.content.Context;
import android.content.Intent;
import timber.log.Timber;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.util.ShufflePlayBuffer;
import org.moire.ultrasonic.util.Util;
import java.util.Iterator;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* The implementation of the Media Player Controller.
* This class contains everything that is necessary for the Application UI
* to control the Media Player implementation.
*
* @author Sindre Mehus, Joshua Bahnsen
* @version $Id$
*/
public class MediaPlayerControllerImpl implements MediaPlayerController
{
private boolean created = false;
private String suggestedPlaylistName;
private boolean keepScreenOn;
private boolean showVisualization;
private boolean autoPlayStart;
private final Context context;
private final Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final DownloadQueueSerializer downloadQueueSerializer;
private final ExternalStorageMonitor externalStorageMonitor;
private final Downloader downloader;
private final ShufflePlayBuffer shufflePlayBuffer;
private final LocalMediaPlayer localMediaPlayer;
public MediaPlayerControllerImpl(Context context, DownloadQueueSerializer downloadQueueSerializer,
ExternalStorageMonitor externalStorageMonitor, Downloader downloader,
ShufflePlayBuffer shufflePlayBuffer, LocalMediaPlayer localMediaPlayer)
{
this.context = context;
this.downloadQueueSerializer = downloadQueueSerializer;
this.externalStorageMonitor = externalStorageMonitor;
this.downloader = downloader;
this.shufflePlayBuffer = shufflePlayBuffer;
this.localMediaPlayer = localMediaPlayer;
Timber.i("MediaPlayerControllerImpl constructed");
}
public void onCreate()
{
if (created) return;
this.externalStorageMonitor.onCreate(this::reset);
setJukeboxEnabled(activeServerProvider.getValue().getActiveServer().getJukeboxByDefault());
created = true;
Timber.i("MediaPlayerControllerImpl created");
}
public void onDestroy()
{
if (!created) return;
externalStorageMonitor.onDestroy();
context.stopService(new Intent(context, MediaPlayerService.class));
downloader.onDestroy();
created = false;
Timber.i("MediaPlayerControllerImpl destroyed");
}
@Override
public synchronized void restore(List<MusicDirectory.Entry> songs, final int currentPlayingIndex, final int currentPlayingPosition, final boolean autoPlay, boolean newPlaylist)
{
download(songs, false, false, false, false, newPlaylist);
if (currentPlayingIndex != -1)
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) ->
{
mediaPlayerService.play(currentPlayingIndex, autoPlayStart);
if (localMediaPlayer.currentPlaying != null)
{
if (autoPlay && jukeboxMediaPlayer.getValue().isEnabled())
{
jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), currentPlayingPosition / 1000);
}
else
{
if (localMediaPlayer.currentPlaying.isCompleteFileAvailable())
{
localMediaPlayer.play(localMediaPlayer.currentPlaying, currentPlayingPosition, autoPlay);
}
}
}
autoPlayStart = false;
return null;
}
);
}
}
public synchronized void preload()
{
MediaPlayerService.getInstance(context);
}
@Override
public synchronized void play(final int index)
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> {
mediaPlayerService.play(index, true);
return null;
}
);
}
public synchronized void play()
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> {
mediaPlayerService.play();
return null;
}
);
}
public synchronized void resumeOrPlay()
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> {
mediaPlayerService.resumeOrPlay();
return null;
}
);
}
@Override
public synchronized void togglePlayPause()
{
if (localMediaPlayer.playerState == PlayerState.IDLE) autoPlayStart = true;
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> {
mediaPlayerService.togglePlayPause();
return null;
}
);
}
@Override
public synchronized void start()
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> {
mediaPlayerService.start();
return null;
}
);
}
@Override
public synchronized void seekTo(final int position)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.seekTo(position);
}
@Override
public synchronized void pause()
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.pause();
}
@Override
public synchronized void stop()
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.stop();
}
@Override
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoPlay, boolean playNext, boolean shuffle, boolean newPlaylist)
{
downloader.download(songs, save, autoPlay, playNext, newPlaylist);
jukeboxMediaPlayer.getValue().updatePlaylist();
if (shuffle) shuffle();
if (!playNext && !autoPlay && (downloader.downloadList.size() - 1) == downloader.getCurrentPlayingIndex())
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.setNextPlaying();
}
if (autoPlay)
{
play(0);
}
else
{
if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size() > 0)
{
localMediaPlayer.currentPlaying = downloader.downloadList.get(0);
localMediaPlayer.currentPlaying.setPlaying(true);
}
downloader.checkDownloads();
}
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition());
}
@Override
public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save)
{
downloader.downloadBackground(songs, save);
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition());
}
public synchronized void setCurrentPlaying(DownloadFile currentPlaying)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) localMediaPlayer.setCurrentPlaying(currentPlaying);
}
public synchronized void setCurrentPlaying(int index)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(index);
}
public synchronized void setPlayerState(PlayerState state)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state);
}
@Override
public void stopJukeboxService()
{
jukeboxMediaPlayer.getValue().stopJukeboxService();
}
@Override
public synchronized void setShufflePlayEnabled(boolean enabled)
{
shufflePlayBuffer.isEnabled = enabled;
if (enabled)
{
clear();
downloader.checkDownloads();
}
}
@Override
public boolean isShufflePlayEnabled()
{
return shufflePlayBuffer.isEnabled;
}
@Override
public synchronized void shuffle()
{
downloader.shuffle();
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition());
jukeboxMediaPlayer.getValue().updatePlaylist();
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.setNextPlaying();
}
@Override
public RepeatMode getRepeatMode()
{
return Util.getRepeatMode();
}
@Override
public synchronized void setRepeatMode(RepeatMode repeatMode)
{
Util.setRepeatMode(repeatMode);
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.setNextPlaying();
}
@Override
public boolean getKeepScreenOn()
{
return keepScreenOn;
}
@Override
public void setKeepScreenOn(boolean keepScreenOn)
{
this.keepScreenOn = keepScreenOn;
}
@Override
public boolean getShowVisualization()
{
return showVisualization;
}
@Override
public void setShowVisualization(boolean showVisualization)
{
this.showVisualization = showVisualization;
}
@Override
public synchronized void clear()
{
clear(true);
}
public synchronized void clear(boolean serialize)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) {
mediaPlayerService.clear(serialize);
} else {
// If no MediaPlayerService is available, just empty the playlist
downloader.clear();
if (serialize) {
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList,
downloader.getCurrentPlayingIndex(), getPlayerPosition());
}
}
jukeboxMediaPlayer.getValue().updatePlaylist();
}
@Override
public synchronized void clearIncomplete()
{
reset();
Iterator<DownloadFile> iterator = downloader.downloadList.iterator();
while (iterator.hasNext())
{
DownloadFile downloadFile = iterator.next();
if (!downloadFile.isCompleteFileAvailable())
{
iterator.remove();
}
}
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition());
jukeboxMediaPlayer.getValue().updatePlaylist();
}
@Override
public synchronized void remove(DownloadFile downloadFile)
{
if (downloadFile == localMediaPlayer.currentPlaying)
{
reset();
setCurrentPlaying(null);
}
downloader.removeDownloadFile(downloadFile);
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition());
jukeboxMediaPlayer.getValue().updatePlaylist();
if (downloadFile == localMediaPlayer.nextPlaying)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.setNextPlaying();
}
}
@Override
public synchronized void delete(List<MusicDirectory.Entry> songs)
{
for (MusicDirectory.Entry song : songs)
{
downloader.getDownloadFileForSong(song).delete();
}
}
@Override
public synchronized void unpin(List<MusicDirectory.Entry> songs)
{
for (MusicDirectory.Entry song : songs)
{
downloader.getDownloadFileForSong(song).unpin();
}
}
@Override
public synchronized void previous()
{
int index = downloader.getCurrentPlayingIndex();
if (index == -1)
{
return;
}
// Restart song if played more than five seconds.
if (getPlayerPosition() > 5000 || index == 0)
{
play(index);
}
else
{
play(index - 1);
}
}
@Override
public synchronized void next()
{
int index = downloader.getCurrentPlayingIndex();
if (index != -1)
{
switch (getRepeatMode())
{
case SINGLE:
case OFF:
if (index + 1 >= 0 && index + 1 < downloader.downloadList.size()) {
play(index + 1);
}
break;
case ALL:
play((index + 1) % downloader.downloadList.size());
break;
default:
break;
}
}
}
@Override
public synchronized void reset()
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) localMediaPlayer.reset();
}
@Override
public synchronized int getPlayerPosition()
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService == null) return 0;
return mediaPlayerService.getPlayerPosition();
}
@Override
public synchronized int getPlayerDuration()
{
if (localMediaPlayer.currentPlaying != null)
{
Integer duration = localMediaPlayer.currentPlaying.getSong().getDuration();
if (duration != null)
{
return duration * 1000;
}
}
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService == null) return 0;
return mediaPlayerService.getPlayerDuration();
}
@Override
public PlayerState getPlayerState() { return localMediaPlayer.playerState; }
@Override
public void setSuggestedPlaylistName(String name)
{
this.suggestedPlaylistName = name;
}
@Override
public String getSuggestedPlaylistName()
{
return suggestedPlaylistName;
}
@Override
public boolean isJukeboxEnabled()
{
return jukeboxMediaPlayer.getValue().isEnabled();
}
@Override
public boolean isJukeboxAvailable()
{
try
{
String username = activeServerProvider.getValue().getActiveServer().getUserName();
UserInfo user = MusicServiceFactory.getMusicService().getUser(username);
return user.getJukeboxRole();
}
catch (Exception e)
{
Timber.w(e, "Error getting user information");
}
return false;
}
@Override
public void setJukeboxEnabled(boolean jukeboxEnabled)
{
jukeboxMediaPlayer.getValue().setEnabled(jukeboxEnabled);
setPlayerState(PlayerState.IDLE);
if (jukeboxEnabled)
{
jukeboxMediaPlayer.getValue().startJukeboxService();
reset();
// Cancel current download, if necessary.
if (downloader.currentDownloading != null)
{
downloader.currentDownloading.cancelDownload();
}
}
else
{
jukeboxMediaPlayer.getValue().stopJukeboxService();
}
}
@Override
public void adjustJukeboxVolume(boolean up)
{
jukeboxMediaPlayer.getValue().adjustVolume(up);
}
@Override
public void setVolume(float volume)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) localMediaPlayer.setVolume(volume);
}
@Override
public void updateNotification()
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.updateNotification(localMediaPlayer.playerState, localMediaPlayer.currentPlaying);
}
public void toggleSongStarred() {
if (localMediaPlayer.currentPlaying == null)
return;
final Entry song = localMediaPlayer.currentPlaying.getSong();
// Trigger an update
localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying);
song.setStarred(!song.getStarred());
}
public void setSongRating(final int rating)
{
if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING))
return;
if (localMediaPlayer.currentPlaying == null)
return;
final Entry song = localMediaPlayer.currentPlaying.getSong();
song.setUserRating(rating);
new Thread(() -> {
try
{
MusicServiceFactory.getMusicService().setRating(song.getId(), rating);
}
catch (Exception e)
{
Timber.e(e);
}
}).start();
updateNotification();
}
@Override
public DownloadFile getCurrentPlaying() {
return localMediaPlayer.currentPlaying;
}
@Override
public int getPlaylistSize() {
return downloader.downloadList.size();
}
@Override
public int getCurrentPlayingNumberOnPlaylist() {
return downloader.getCurrentPlayingIndex();
}
@Override
public DownloadFile getCurrentDownloading() {
return downloader.currentDownloading;
}
@Override
public List<DownloadFile> getPlayList() {
return downloader.downloadList;
}
@Override
public long getPlayListUpdateRevision() {
return downloader.getDownloadListUpdateRevision();
}
@Override
public long getPlayListDuration() {
return downloader.getDownloadListDuration();
}
@Override
public DownloadFile getDownloadFileForSong(Entry song) {
return downloader.getDownloadFileForSong(song);
}
}

View File

@ -30,6 +30,7 @@ import timber.log.Timber;
import android.view.KeyEvent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.Constants;
@ -43,19 +44,17 @@ import org.moire.ultrasonic.util.Util;
public class MediaPlayerLifecycleSupport
{
private boolean created = false;
private DownloadQueueSerializer downloadQueueSerializer; // From DI
private final MediaPlayerControllerImpl mediaPlayerController; // From DI
private final DownloadQueueSerializer downloadQueueSerializer; // From DI
private final MediaPlayerController mediaPlayerController; // From DI
private final Downloader downloader; // From DI
private Context context;
private BroadcastReceiver headsetEventReceiver;
public MediaPlayerLifecycleSupport(Context context, DownloadQueueSerializer downloadQueueSerializer,
final MediaPlayerControllerImpl mediaPlayerController, final Downloader downloader)
public MediaPlayerLifecycleSupport(DownloadQueueSerializer downloadQueueSerializer,
final MediaPlayerController mediaPlayerController, final Downloader downloader)
{
this.downloadQueueSerializer = downloadQueueSerializer;
this.mediaPlayerController = mediaPlayerController;
this.context = context;
this.downloader = downloader;
Timber.i("LifecycleSupport constructed");
@ -92,7 +91,7 @@ public class MediaPlayerLifecycleSupport
}
});
new CacheCleaner(context).clean();
new CacheCleaner().clean();
created = true;
Timber.i("LifecycleSupport created");
}
@ -103,7 +102,7 @@ public class MediaPlayerLifecycleSupport
downloadQueueSerializer.serializeDownloadQueueNow(downloader.downloadList,
downloader.getCurrentPlayingIndex(), mediaPlayerController.getPlayerPosition());
mediaPlayerController.clear(false);
context.unregisterReceiver(headsetEventReceiver);
UApp.Companion.applicationContext().unregisterReceiver(headsetEventReceiver);
mediaPlayerController.onDestroy();
created = false;
Timber.i("LifecycleSupport destroyed");
@ -139,6 +138,7 @@ public class MediaPlayerLifecycleSupport
*/
private void registerHeadsetReceiver() {
final SharedPreferences sp = Util.getPreferences();
final Context context = UApp.Companion.applicationContext();
final String spKey = context
.getString(R.string.settings_playback_resume_play_on_headphones_plug);
@ -177,7 +177,7 @@ public class MediaPlayerLifecycleSupport
{
headsetIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
}
context.registerReceiver(headsetEventReceiver, headsetIntentFilter);
UApp.Companion.applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter);
}
public void handleKeyEvent(KeyEvent event)
@ -205,58 +205,55 @@ public class MediaPlayerLifecycleSupport
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT);
// We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start
onCreate(autoStart, new Runnable() {
@Override
public void run() {
switch (keyCode)
{
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
mediaPlayerController.togglePlayPause();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
mediaPlayerController.previous();
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
mediaPlayerController.next();
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
mediaPlayerController.stop();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
if (mediaPlayerController.getPlayerState() == PlayerState.IDLE)
{
mediaPlayerController.play();
}
else if (mediaPlayerController.getPlayerState() != PlayerState.STARTED)
{
mediaPlayerController.start();
}
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
mediaPlayerController.pause();
break;
case KeyEvent.KEYCODE_1:
mediaPlayerController.setSongRating(1);
break;
case KeyEvent.KEYCODE_2:
mediaPlayerController.setSongRating(2);
break;
case KeyEvent.KEYCODE_3:
mediaPlayerController.setSongRating(3);
break;
case KeyEvent.KEYCODE_4:
mediaPlayerController.setSongRating(4);
break;
case KeyEvent.KEYCODE_5:
mediaPlayerController.setSongRating(5);
break;
case KeyEvent.KEYCODE_STAR:
mediaPlayerController.toggleSongStarred();
break;
default:
break;
}
onCreate(autoStart, () -> {
switch (keyCode)
{
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
mediaPlayerController.togglePlayPause();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
mediaPlayerController.previous();
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
mediaPlayerController.next();
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
mediaPlayerController.stop();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
if (mediaPlayerController.getPlayerState() == PlayerState.IDLE)
{
mediaPlayerController.play();
}
else if (mediaPlayerController.getPlayerState() != PlayerState.STARTED)
{
mediaPlayerController.start();
}
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
mediaPlayerController.pause();
break;
case KeyEvent.KEYCODE_1:
mediaPlayerController.setSongRating(1);
break;
case KeyEvent.KEYCODE_2:
mediaPlayerController.setSongRating(2);
break;
case KeyEvent.KEYCODE_3:
mediaPlayerController.setSongRating(3);
break;
case KeyEvent.KEYCODE_4:
mediaPlayerController.setSongRating(4);
break;
case KeyEvent.KEYCODE_5:
mediaPlayerController.setSongRating(5);
break;
case KeyEvent.KEYCODE_STAR:
mediaPlayerController.toggleSongStarred();
break;
default:
break;
}
});
}
@ -278,36 +275,33 @@ public class MediaPlayerLifecycleSupport
intentAction.equals(Constants.CMD_NEXT));
// We can receive intents when everything is stopped, so we need to start
onCreate(autoStart, new Runnable() {
@Override
public void run() {
switch(intentAction)
{
case Constants.CMD_PLAY:
mediaPlayerController.play();
break;
case Constants.CMD_RESUME_OR_PLAY:
// If Ultrasonic wasn't running, the autoStart is enough to resume, no need to call anything
if (isRunning) mediaPlayerController.resumeOrPlay();
break;
case Constants.CMD_NEXT:
mediaPlayerController.next();
break;
case Constants.CMD_PREVIOUS:
mediaPlayerController.previous();
break;
case Constants.CMD_TOGGLEPAUSE:
mediaPlayerController.togglePlayPause();
break;
case Constants.CMD_STOP:
// TODO: There is a stop() function, shouldn't we use that?
mediaPlayerController.pause();
mediaPlayerController.seekTo(0);
break;
case Constants.CMD_PAUSE:
mediaPlayerController.pause();
break;
}
onCreate(autoStart, () -> {
switch(intentAction)
{
case Constants.CMD_PLAY:
mediaPlayerController.play();
break;
case Constants.CMD_RESUME_OR_PLAY:
// If Ultrasonic wasn't running, the autoStart is enough to resume, no need to call anything
if (isRunning) mediaPlayerController.resumeOrPlay();
break;
case Constants.CMD_NEXT:
mediaPlayerController.next();
break;
case Constants.CMD_PREVIOUS:
mediaPlayerController.previous();
break;
case Constants.CMD_TOGGLEPAUSE:
mediaPlayerController.togglePlayPause();
break;
case Constants.CMD_STOP:
// TODO: There is a stop() function, shouldn't we use that?
mediaPlayerController.pause();
mediaPlayerController.seekTo(0);
break;
case Constants.CMD_PAUSE:
mediaPlayerController.pause();
break;
}
});
}

View File

@ -21,9 +21,6 @@ package org.moire.ultrasonic.service;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import kotlin.Pair;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.Bookmark;
@ -54,7 +51,6 @@ import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@ -64,6 +60,8 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import kotlin.Lazy;
import kotlin.Pair;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
@ -95,49 +93,44 @@ public class OfflineMusicService implements MusicService
String ignoredArticlesString = "The El La Los Las Le Les";
final String[] ignoredArticles = COMPILE.split(ignoredArticlesString);
Collections.sort(artists, new Comparator<Artist>()
{
@Override
public int compare(Artist lhsArtist, Artist rhsArtist)
Collections.sort(artists, (lhsArtist, rhsArtist) -> {
String lhs = lhsArtist.getName().toLowerCase();
String rhs = rhsArtist.getName().toLowerCase();
char lhs1 = lhs.charAt(0);
char rhs1 = rhs.charAt(0);
if (Character.isDigit(lhs1) && !Character.isDigit(rhs1))
{
String lhs = lhsArtist.getName().toLowerCase();
String rhs = rhsArtist.getName().toLowerCase();
char lhs1 = lhs.charAt(0);
char rhs1 = rhs.charAt(0);
if (Character.isDigit(lhs1) && !Character.isDigit(rhs1))
{
return 1;
}
if (Character.isDigit(rhs1) && !Character.isDigit(lhs1))
{
return -1;
}
for (String article : ignoredArticles)
{
int index = lhs.indexOf(String.format("%s ", article.toLowerCase()));
if (index == 0)
{
lhs = lhs.substring(article.length() + 1);
}
index = rhs.indexOf(String.format("%s ", article.toLowerCase()));
if (index == 0)
{
rhs = rhs.substring(article.length() + 1);
}
}
return lhs.compareTo(rhs);
return 1;
}
if (Character.isDigit(rhs1) && !Character.isDigit(lhs1))
{
return -1;
}
for (String article : ignoredArticles)
{
int index = lhs.indexOf(String.format("%s ", article.toLowerCase()));
if (index == 0)
{
lhs = lhs.substring(article.length() + 1);
}
index = rhs.indexOf(String.format("%s ", article.toLowerCase()));
if (index == 0)
{
rhs = rhs.substring(article.length() + 1);
}
}
return lhs.compareTo(rhs);
});
return new Indexes(0L, ignoredArticlesString, Collections.<Artist>emptyList(), artists);
return new Indexes(0L, ignoredArticlesString, Collections.emptyList(), artists);
}
@Override
@ -387,44 +380,31 @@ public class OfflineMusicService implements MusicService
}
}
Collections.sort(artists, new Comparator<Artist>()
{
@Override
public int compare(Artist lhs, Artist rhs)
Collections.sort(artists, (lhs, rhs) -> {
if (lhs.getCloseness() == rhs.getCloseness())
{
if (lhs.getCloseness() == rhs.getCloseness())
{
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
});
Collections.sort(albums, new Comparator<MusicDirectory.Entry>()
{
@Override
public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs)
{
if (lhs.getCloseness() == rhs.getCloseness())
{
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
Collections.sort(albums, (lhs, rhs) -> {
if (lhs.getCloseness() == rhs.getCloseness())
{
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
});
Collections.sort(songs, new Comparator<MusicDirectory.Entry>()
{
@Override
public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs)
{
if (lhs.getCloseness() == rhs.getCloseness())
{
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
Collections.sort(songs, (lhs, rhs) -> {
if (lhs.getCloseness() == rhs.getCloseness())
{
return 0;
}
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
});
return new SearchResult(artists, albums, songs);

View File

@ -1,8 +1,6 @@
package org.moire.ultrasonic.service;
import android.content.Context;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider;
/**
@ -16,7 +14,7 @@ public class Scrobbler
private String lastSubmission;
private String lastNowPlaying;
public void scrobble(final Context context, final DownloadFile song, final boolean submission)
public void scrobble(final DownloadFile song, final boolean submission)
{
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.os.AsyncTask;
import android.os.StatFs;
import timber.log.Timber;
@ -14,7 +13,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -31,13 +29,8 @@ public class CacheCleaner
{
private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
private final Context context;
private Lazy<Downloader> downloader = inject(Downloader.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
public CacheCleaner(Context context)
public CacheCleaner()
{
this.context = context;
}
public void clean()
@ -79,7 +72,7 @@ public class CacheCleaner
}
}
private void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete)
private static void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete)
{
for (File dir : dirs)
{
@ -108,7 +101,7 @@ public class CacheCleaner
}
}
private long getMinimumDelete(List<File> files)
private static long getMinimumDelete(List<File> files)
{
if (files.isEmpty())
{
@ -168,7 +161,7 @@ public class CacheCleaner
}
}
Timber.i("Deleted : %s", Util.formatBytes(bytesDeleted));
Timber.i("Deleted: %s", Util.formatBytes(bytesDeleted));
}
private static void findCandidatesForDeletion(File file, List<File> files, List<File> dirs)
@ -197,20 +190,14 @@ public class CacheCleaner
private static void sortByAscendingModificationTime(List<File> files)
{
Collections.sort(files, new Comparator<File>()
{
@Override
public int compare(File a, File b)
{
return Long.compare(a.lastModified(), b.lastModified());
}
});
Collections.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
}
private Set<File> findFilesToNotDelete()
private static Set<File> findFilesToNotDelete()
{
Set<File> filesToNotDelete = new HashSet<File>(5);
Set<File> filesToNotDelete = new HashSet<>(5);
Lazy<Downloader> downloader = inject(Downloader.class);
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
{
@ -222,7 +209,7 @@ public class CacheCleaner
return filesToNotDelete;
}
private class BackgroundCleanup extends AsyncTask<Void, Void, Void>
private static class BackgroundCleanup extends AsyncTask<Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params)
@ -230,8 +217,8 @@ public class CacheCleaner
try
{
Thread.currentThread().setName("BackgroundCleanup");
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
List<File> files = new ArrayList<>();
List<File> dirs = new ArrayList<>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
sortByAscendingModificationTime(files);
@ -250,7 +237,7 @@ public class CacheCleaner
}
}
private class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void>
private static class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params)
@ -258,8 +245,8 @@ public class CacheCleaner
try
{
Thread.currentThread().setName("BackgroundSpaceCleanup");
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
List<File> files = new ArrayList<>();
List<File> dirs = new ArrayList<>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
long bytesToDelete = getMinimumDelete(files);
@ -279,13 +266,14 @@ public class CacheCleaner
}
}
private class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void>
private static class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void>
{
@Override
protected Void doInBackground(List<Playlist>... params)
{
try
{
Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
Thread.currentThread().setName("BackgroundPlaylistsCleanup");
String server = activeServerProvider.getValue().getActiveServer().getName();
SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server));

View File

@ -79,7 +79,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
imageSizeDefault = drawable.getIntrinsicHeight();
}
imageSizeLarge = Util.getMaxDisplayMetric(context);
imageSizeLarge = Util.getMaxDisplayMetric();
createLargeUnknownImage(context);
createUnknownAvatarImage(context);
}

View File

@ -18,9 +18,6 @@
*/
package org.moire.ultrasonic.util;
import android.content.Context;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
@ -32,6 +29,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
/**
* @author Sindre Mehus
* @version $Id$
@ -42,28 +41,19 @@ public class ShufflePlayBuffer
private static final int REFILL_THRESHOLD = 40;
private final List<MusicDirectory.Entry> buffer = new ArrayList<>();
private final Context context;
private ScheduledExecutorService executorService;
private int currentServer;
public boolean isEnabled = false;
public ShufflePlayBuffer(Context context)
public ShufflePlayBuffer()
{
this.context = context;
}
public void onCreate()
{
executorService = Executors.newSingleThreadScheduledExecutor();
Runnable runnable = new Runnable()
{
@Override
public void run()
{
refill();
}
};
Runnable runnable = this::refill;
executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
Timber.i("ShufflePlayBuffer created");
}
@ -97,7 +87,7 @@ public class ShufflePlayBuffer
// Check if active server has changed.
clearBufferIfNecessary();
if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !ActiveServerProvider.Companion.isOffline()))
if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected() && !ActiveServerProvider.Companion.isOffline()))
{
return;
}

View File

@ -167,10 +167,14 @@ public class Util
}
}
public static int getMaxBitRate(Context context)
{
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
public static ConnectivityManager getConnectivityManager() {
Context context = appContext();
return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
public static int getMaxBitRate()
{
ConnectivityManager manager = getConnectivityManager();
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
if (networkInfo == null)
@ -548,9 +552,9 @@ public class Util
return null;
}
public static boolean isNetworkConnected(Context context)
public static boolean isNetworkConnected()
{
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
ConnectivityManager manager = getConnectivityManager();
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
boolean connected = networkInfo != null && networkInfo.isConnected();
@ -648,9 +652,9 @@ public class Util
return bitmap;
}
public static WifiManager.WifiLock createWifiLock(Context context, String tag)
public static WifiManager.WifiLock createWifiLock(String tag)
{
WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
WifiManager wm = (WifiManager) appContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
return wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, tag);
}
@ -945,15 +949,15 @@ public class Util
return size;
}
public static int getMinDisplayMetric(Context context)
public static int getMinDisplayMetric()
{
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
DisplayMetrics metrics = appContext().getResources().getDisplayMetrics();
return Math.min(metrics.widthPixels, metrics.heightPixels);
}
public static int getMaxDisplayMetric(Context context)
public static int getMaxDisplayMetric()
{
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
DisplayMetrics metrics = appContext().getResources().getDisplayMetrics();
return Math.max(metrics.widthPixels, metrics.heightPixels);
}
@ -1251,10 +1255,12 @@ public class Util
return preferences.getString(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION, "");
}
public static String getShareGreeting(Context context)
public static String getShareGreeting()
{
SharedPreferences preferences = getPreferences();
return preferences.getString(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING, String.format(context.getResources().getString(R.string.share_default_greeting), context.getResources().getString(R.string.common_appname)));
Context context = appContext();
String defaultVal = String.format(context.getResources().getString(R.string.share_default_greeting), context.getResources().getString(R.string.common_appname));
return preferences.getString(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING, defaultVal);
}
public static String getDefaultShareExpiration()
@ -1313,11 +1319,11 @@ public class Util
return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST, false);
}
public static void scanMedia(Context context, File file)
public static void scanMedia(File file)
{
Uri uri = Uri.fromFile(file);
Intent scanFileIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri);
context.sendBroadcast(scanFileIntent);
appContext().sendBroadcast(scanFileIntent);
}
public static int getImageLoaderConcurrency()

View File

@ -9,7 +9,6 @@ import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.JukeboxMediaPlayer
import org.moire.ultrasonic.service.LocalMediaPlayer
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerControllerImpl
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.util.ShufflePlayBuffer
@ -17,19 +16,15 @@ import org.moire.ultrasonic.util.ShufflePlayBuffer
* This Koin module contains the registration of classes related to the media player
*/
val mediaPlayerModule = module {
single<MediaPlayerController> {
MediaPlayerControllerImpl(androidContext(), get(), get(), get(), get(), get())
}
single { JukeboxMediaPlayer(androidContext(), get()) }
single { MediaPlayerLifecycleSupport(androidContext(), get(), get(), get()) }
single { JukeboxMediaPlayer(get()) }
single { MediaPlayerLifecycleSupport(get(), get(), get()) }
single { DownloadQueueSerializer(androidContext()) }
single { ExternalStorageMonitor(androidContext()) }
single { ShufflePlayBuffer(androidContext()) }
single { Downloader(androidContext(), get(), get(), get()) }
single { ExternalStorageMonitor() }
single { ShufflePlayBuffer() }
single { Downloader(get(), get(), get()) }
single { LocalMediaPlayer(get(), androidContext()) }
single { AudioFocusHandler(get()) }
// TODO Ideally this can be cleaned up when all circular references are removed.
single { MediaPlayerControllerImpl(androidContext(), get(), get(), get(), get(), get()) }
single { MediaPlayerController(get(), get(), get(), get(), get()) }
}

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.net.ConnectException
import java.net.UnknownHostException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -91,12 +92,18 @@ open class GenericListModel(application: Application) :
try {
load(isOffline, useId3Tags, musicService, refresh, bundle)
} catch (exception: ConnectException) {
Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, swipe.context)
}
handleException(exception, swipe.context)
} catch (exception: UnknownHostException) {
handleException(exception, swipe.context)
}
}
private fun handleException(exception: Exception, context: Context) {
Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, context)
}
}
/**
* This is the central function you need to implement if you want to extend this class
*/

View File

@ -51,7 +51,7 @@ class CommunicationErrorHandler {
}
fun getErrorMessage(error: Throwable, context: Context): String {
if (error is IOException && !Util.isNetworkConnected(context)) {
if (error is IOException && !Util.isNetworkConnected()) {
return context.resources.getString(R.string.background_task_no_network)
} else if (error is FileNotFoundException) {
return context.resources.getString(R.string.background_task_not_found)

View File

@ -21,6 +21,7 @@ import java.io.OutputStream
import java.io.RandomAccessFile
import org.koin.core.component.KoinApiExtension
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.CacheCleaner
@ -37,7 +38,6 @@ import timber.log.Timber
*/
@KoinApiExtension
class DownloadFile(
private val context: Context,
val song: MusicDirectory.Entry,
private val save: Boolean
) {
@ -48,7 +48,7 @@ class DownloadFile(
var isFailed = false
private var retryCount = MAX_RETRIES
private val desiredBitRate: Int = Util.getMaxBitRate(context)
private val desiredBitRate: Int = Util.getMaxBitRate()
@Volatile
private var isPlaying = false
@ -138,7 +138,7 @@ class DownloadFile(
Util.delete(completeFile)
Util.delete(saveFile)
Util.scanMedia(context, saveFile)
Util.scanMedia(saveFile)
}
fun unpin() {
@ -186,7 +186,7 @@ class DownloadFile(
} else if (completeWhenDone) {
if (save) {
Util.renameFile(partialFile, saveFile)
Util.scanMedia(context, saveFile)
Util.scanMedia(saveFile)
} else {
Util.renameFile(partialFile, completeFile)
}
@ -211,7 +211,7 @@ class DownloadFile(
var wifiLock: WifiLock? = null
try {
wakeLock = acquireWakeLock(wakeLock)
wifiLock = Util.createWifiLock(context, toString())
wifiLock = Util.createWifiLock(toString())
wifiLock.acquire()
if (saveFile.exists()) {
@ -285,7 +285,7 @@ class DownloadFile(
} else {
if (save) {
Util.renameFile(partialFile, saveFile)
Util.scanMedia(context, saveFile)
Util.scanMedia(saveFile)
} else {
Util.renameFile(partialFile, completeFile)
}
@ -309,7 +309,7 @@ class DownloadFile(
Timber.i("Released wake lock %s", wakeLock)
}
wifiLock?.release()
CacheCleaner(context).cleanSpace()
CacheCleaner().cleanSpace()
downloader.value.checkDownloads()
}
}
@ -317,6 +317,7 @@ class DownloadFile(
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())
@ -333,7 +334,7 @@ class DownloadFile(
private fun downloadAndSaveCoverArt(musicService: MusicService) {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
val size = Util.getMinDisplayMetric(context)
val size = Util.getMinDisplayMetric()
musicService.getCoverArt(song, size, true, true)
}
} catch (e: Exception) {

View File

@ -25,6 +25,7 @@ import java.net.URLEncoder
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
@ -39,6 +40,7 @@ import timber.log.Timber
/**
* Represents a Media Player which uses the mobile's resources for playback
*/
@KoinApiExtension
class LocalMediaPlayer(
private val audioFocusHandler: AudioFocusHandler,
private val context: Context
@ -397,7 +399,7 @@ class LocalMediaPlayer(
secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt()
if (song.transcodedContentType == null && Util.getMaxBitRate(context) == 0) {
if (song.transcodedContentType == null && Util.getMaxBitRate() == 0) {
progressBar?.secondaryProgress = secondaryProgress
}
}

View File

@ -0,0 +1,514 @@
/*
* MediaPlayerController.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.content.Intent
import org.koin.core.component.KoinApiExtension
import org.koin.java.KoinJavaComponent.get
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.service.MediaPlayerService.Companion.executeOnStartedMediaPlayerService
import org.moire.ultrasonic.service.MediaPlayerService.Companion.getInstance
import org.moire.ultrasonic.service.MediaPlayerService.Companion.runningInstance
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* The implementation of the Media Player Controller.
* This class contains everything that is necessary for the Application UI
* to control the Media Player implementation.
*/
@KoinApiExtension
@Suppress("TooManyFunctions")
class MediaPlayerController(
private val downloadQueueSerializer: DownloadQueueSerializer,
private val externalStorageMonitor: ExternalStorageMonitor,
private val downloader: Downloader,
private val shufflePlayBuffer: ShufflePlayBuffer,
private val localMediaPlayer: LocalMediaPlayer
) {
private var created = false
var suggestedPlaylistName: String? = null
var keepScreenOn = false
var showVisualization = false
private var autoPlayStart = false
private val jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java).value
private val activeServerProvider = inject(ActiveServerProvider::class.java).value
fun onCreate() {
if (created) return
externalStorageMonitor.onCreate { reset() }
isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault
created = true
Timber.i("MediaPlayerController created")
}
fun onDestroy() {
if (!created) return
val context = UApp.applicationContext()
externalStorageMonitor.onDestroy()
context.stopService(Intent(context, MediaPlayerService::class.java))
downloader.onDestroy()
created = false
Timber.i("MediaPlayerController destroyed")
}
@Synchronized
fun restore(
songs: List<MusicDirectory.Entry?>?,
currentPlayingIndex: Int,
currentPlayingPosition: Int,
autoPlay: Boolean,
newPlaylist: Boolean
) {
download(
songs,
save = false,
autoPlay = false,
playNext = false,
shuffle = false,
newPlaylist = newPlaylist
)
if (currentPlayingIndex != -1) {
executeOnStartedMediaPlayerService { mediaPlayerService: MediaPlayerService ->
mediaPlayerService.play(currentPlayingIndex, autoPlayStart)
if (localMediaPlayer.currentPlaying != null) {
if (autoPlay && jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(
downloader.currentPlayingIndex,
currentPlayingPosition / 1000
)
} else {
if (localMediaPlayer.currentPlaying!!.isCompleteFileAvailable) {
localMediaPlayer.play(
localMediaPlayer.currentPlaying,
currentPlayingPosition,
autoPlay
)
}
}
}
autoPlayStart = false
}
}
}
@Synchronized
fun preload() {
getInstance()
}
@Synchronized
fun play(index: Int) {
executeOnStartedMediaPlayerService { service: MediaPlayerService ->
service.play(index, true)
}
}
@Synchronized
fun play() {
executeOnStartedMediaPlayerService { service: MediaPlayerService ->
service.play()
}
}
@Synchronized
fun resumeOrPlay() {
executeOnStartedMediaPlayerService { service: MediaPlayerService ->
service.resumeOrPlay()
}
}
@Synchronized
fun togglePlayPause() {
if (localMediaPlayer.playerState === PlayerState.IDLE) autoPlayStart = true
executeOnStartedMediaPlayerService { service: MediaPlayerService ->
service.togglePlayPause()
}
}
@Synchronized
fun start() {
executeOnStartedMediaPlayerService { service: MediaPlayerService ->
service.start()
}
}
@Synchronized
fun seekTo(position: Int) {
val mediaPlayerService = runningInstance
mediaPlayerService?.seekTo(position)
}
@Synchronized
fun pause() {
val mediaPlayerService = runningInstance
mediaPlayerService?.pause()
}
@Synchronized
fun stop() {
val mediaPlayerService = runningInstance
mediaPlayerService?.stop()
}
@Synchronized
@Suppress("LongParameterList")
fun download(
songs: List<MusicDirectory.Entry?>?,
save: Boolean,
autoPlay: Boolean,
playNext: Boolean,
shuffle: Boolean,
newPlaylist: Boolean
) {
downloader.download(songs, save, autoPlay, playNext, newPlaylist)
jukeboxMediaPlayer.updatePlaylist()
if (shuffle) shuffle()
val isLastTrack = (downloader.downloadList.size - 1 == downloader.currentPlayingIndex)
if (!playNext && !autoPlay && isLastTrack) {
val mediaPlayerService = runningInstance
mediaPlayerService?.setNextPlaying()
}
if (autoPlay) {
play(0)
} else {
if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size > 0) {
localMediaPlayer.currentPlaying = downloader.downloadList[0]
downloader.downloadList[0].setPlaying(true)
}
downloader.checkDownloads()
}
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex,
playerPosition
)
}
@Synchronized
fun downloadBackground(songs: List<MusicDirectory.Entry?>?, save: Boolean) {
downloader.downloadBackground(songs, save)
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex,
playerPosition
)
}
@Synchronized
fun setCurrentPlaying(index: Int) {
val mediaPlayerService = runningInstance
mediaPlayerService?.setCurrentPlaying(index)
}
fun stopJukeboxService() {
jukeboxMediaPlayer.stopJukeboxService()
}
@set:Synchronized
var isShufflePlayEnabled: Boolean
get() = shufflePlayBuffer.isEnabled
set(enabled) {
shufflePlayBuffer.isEnabled = enabled
if (enabled) {
clear()
downloader.checkDownloads()
}
}
@Synchronized
fun shuffle() {
downloader.shuffle()
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex,
playerPosition
)
jukeboxMediaPlayer.updatePlaylist()
val mediaPlayerService = runningInstance
mediaPlayerService?.setNextPlaying()
}
@set:Synchronized
var repeatMode: RepeatMode?
get() = Util.getRepeatMode()
set(repeatMode) {
Util.setRepeatMode(repeatMode)
val mediaPlayerService = runningInstance
mediaPlayerService?.setNextPlaying()
}
@Synchronized
fun clear() {
clear(true)
}
@Synchronized
fun clear(serialize: Boolean) {
val mediaPlayerService = runningInstance
if (mediaPlayerService != null) {
mediaPlayerService.clear(serialize)
} else {
// If no MediaPlayerService is available, just empty the playlist
downloader.clear()
if (serialize) {
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex, playerPosition
)
}
}
jukeboxMediaPlayer.updatePlaylist()
}
@Synchronized
fun clearIncomplete() {
reset()
val iterator = downloader.downloadList.iterator()
while (iterator.hasNext()) {
val downloadFile = iterator.next()
if (!downloadFile.isCompleteFileAvailable) {
iterator.remove()
}
}
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex,
playerPosition
)
jukeboxMediaPlayer.updatePlaylist()
}
@Synchronized
fun remove(downloadFile: DownloadFile) {
if (downloadFile == localMediaPlayer.currentPlaying) {
reset()
currentPlaying = null
}
downloader.removeDownloadFile(downloadFile)
downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList,
downloader.currentPlayingIndex,
playerPosition
)
jukeboxMediaPlayer.updatePlaylist()
if (downloadFile == localMediaPlayer.nextPlaying) {
val mediaPlayerService = runningInstance
mediaPlayerService?.setNextPlaying()
}
}
@Synchronized
fun delete(songs: List<MusicDirectory.Entry?>) {
for (song in songs) {
downloader.getDownloadFileForSong(song).delete()
}
}
@Synchronized
fun unpin(songs: List<MusicDirectory.Entry?>) {
for (song in songs) {
downloader.getDownloadFileForSong(song).unpin()
}
}
@Synchronized
fun previous() {
val index = downloader.currentPlayingIndex
if (index == -1) {
return
}
// Restart song if played more than five seconds.
@Suppress("MagicNumber")
if (playerPosition > 5000 || index == 0) {
play(index)
} else {
play(index - 1)
}
}
@Synchronized
operator fun next() {
val index = downloader.currentPlayingIndex
if (index != -1) {
when (repeatMode) {
RepeatMode.SINGLE, RepeatMode.OFF -> {
// Play next if exists
if (index + 1 >= 0 && index + 1 < downloader.downloadList.size) {
play(index + 1)
}
}
RepeatMode.ALL -> {
play((index + 1) % downloader.downloadList.size)
}
else -> {
}
}
}
}
@Synchronized
fun reset() {
val mediaPlayerService = runningInstance
if (mediaPlayerService != null) localMediaPlayer.reset()
}
@get:Synchronized
val playerPosition: Int
get() {
val mediaPlayerService = runningInstance ?: return 0
return mediaPlayerService.playerPosition
}
@get:Synchronized
val playerDuration: Int
get() {
if (localMediaPlayer.currentPlaying != null) {
val duration = localMediaPlayer.currentPlaying!!.song.duration
if (duration != null) {
return duration * 1000
}
}
val mediaPlayerService = runningInstance ?: return 0
return mediaPlayerService.playerDuration
}
@set:Synchronized
var playerState: PlayerState
get() = localMediaPlayer.playerState
set(state) {
val mediaPlayerService = runningInstance
if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state)
}
@set:Synchronized
var isJukeboxEnabled: Boolean
get() = jukeboxMediaPlayer.isEnabled
set(jukeboxEnabled) {
jukeboxMediaPlayer.isEnabled = jukeboxEnabled
playerState = PlayerState.IDLE
if (jukeboxEnabled) {
jukeboxMediaPlayer.startJukeboxService()
reset()
// Cancel current download, if necessary.
if (downloader.currentDownloading != null) {
downloader.currentDownloading.cancelDownload()
}
} else {
jukeboxMediaPlayer.stopJukeboxService()
}
}
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
val isJukeboxAvailable: Boolean
get() {
try {
val username = activeServerProvider.getActiveServer().userName
val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser(
username
)
return jukeboxRole
} catch (e: Exception) {
Timber.w(e, "Error getting user information")
}
return false
}
fun adjustJukeboxVolume(up: Boolean) {
jukeboxMediaPlayer.adjustVolume(up)
}
fun setVolume(volume: Float) {
if (runningInstance != null) localMediaPlayer.setVolume(volume)
}
private fun updateNotification() {
runningInstance?.updateNotification(
localMediaPlayer.playerState,
localMediaPlayer.currentPlaying
)
}
fun toggleSongStarred() {
if (localMediaPlayer.currentPlaying == null) return
val song = localMediaPlayer.currentPlaying!!.song
// Trigger an update
localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
song.starred = !song.starred
}
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
fun setSongRating(rating: Int) {
if (!get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
if (localMediaPlayer.currentPlaying == null) return
val song = localMediaPlayer.currentPlaying!!.song
song.userRating = rating
Thread {
try {
getMusicService().setRating(song.id, rating)
} catch (e: Exception) {
Timber.e(e)
}
}.start()
updateNotification()
}
@set:Synchronized
var currentPlaying: DownloadFile?
get() = localMediaPlayer.currentPlaying
set(currentPlaying) {
if (runningInstance != null) localMediaPlayer.setCurrentPlaying(currentPlaying)
}
val playlistSize: Int
get() = downloader.downloadList.size
val currentPlayingNumberOnPlaylist: Int
get() = downloader.currentPlayingIndex
val currentDownloading: DownloadFile
get() = downloader.currentDownloading
val playList: List<DownloadFile>
get() = downloader.downloadList
val playListUpdateRevision: Long
get() = downloader.downloadListUpdateRevision
val playListDuration: Long
get() = downloader.downloadListDuration
fun getDownloadFileForSong(song: MusicDirectory.Entry?): DownloadFile {
return downloader.getDownloadFileForSong(song)
}
init {
Timber.i("MediaPlayerController constructed")
}
}

View File

@ -24,8 +24,10 @@ import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.koin.android.ext.android.inject
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode
@ -47,6 +49,7 @@ import timber.log.Timber
* Android Foreground Service for playing music
* while the rest of the Ultrasonic App is in the background.
*/
@KoinApiExtension
@Suppress("LargeClass")
class MediaPlayerService : Service() {
private val binder: IBinder = SimpleServiceBinder(this)
@ -406,9 +409,9 @@ class MediaPlayerService : Service() {
}
if (playerState === PlayerState.STARTED) {
scrobbler.scrobble(context, currentPlaying, false)
scrobbler.scrobble(currentPlaying, false)
} else if (playerState === PlayerState.COMPLETED) {
scrobbler.scrobble(context, currentPlaying, true)
scrobbler.scrobble(currentPlaying, true)
}
null
@ -475,12 +478,11 @@ class MediaPlayerService : Service() {
// Set Metadata
val metadata = MediaMetadataCompat.Builder()
val context = applicationContext
if (currentPlaying != null) {
try {
val song = currentPlaying.song
val cover = FileUtil.getAlbumArtBitmap(
song, Util.getMinDisplayMetric(context),
song, Util.getMinDisplayMetric(),
true
)
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L)
@ -906,7 +908,8 @@ class MediaPlayerService : Service() {
private val instanceLock = Any()
@JvmStatic
fun getInstance(context: Context): MediaPlayerService? {
fun getInstance(): MediaPlayerService? {
val context = UApp.applicationContext()
synchronized(instanceLock) {
for (i in 0..19) {
if (instance != null) return instance
@ -931,18 +934,18 @@ class MediaPlayerService : Service() {
@JvmStatic
fun executeOnStartedMediaPlayerService(
context: Context,
taskToExecute: (MediaPlayerService?) -> Unit
taskToExecute: (MediaPlayerService) -> Unit
) {
val t: Thread = object : Thread() {
override fun run() {
val instance = getInstance(context)
val instance = getInstance()
if (instance == null) {
Timber.e("ExecuteOnStarted.. failed to get a MediaPlayerService instance!")
return
} else {
taskToExecute(instance)
}
taskToExecute(instance)
}
}
t.start()

View File

@ -12,7 +12,7 @@ class NetworkAndStorageChecker(val context: Context) {
fun warnIfNetworkOrStorageUnavailable() {
if (!Util.isExternalStoragePresent()) {
Util.toast(context, R.string.select_album_no_sdcard)
} else if (!isOffline() && !Util.isNetworkConnected(context)) {
} else if (!isOffline() && !Util.isNetworkConnected()) {
Util.toast(context, R.string.select_album_no_network)
}
}

View File

@ -91,7 +91,7 @@ class ShareHandler(val context: Context) {
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TEXT,
String.format("%s\n\n%s", Util.getShareGreeting(context), result.url)
String.format("%s\n\n%s", Util.getShareGreeting(), result.url)
)
fragment.activity?.startActivity(
Intent.createChooser(

View File

@ -10,7 +10,7 @@ import org.moire.ultrasonic.util.Util
*/
class VideoPlayer() {
fun playVideo(context: Context, entry: MusicDirectory.Entry?) {
if (!Util.isNetworkConnected(context)) {
if (!Util.isNetworkConnected()) {
Util.toast(context, R.string.select_album_no_network)
return
}