From 442f622b35473aed71f936a83ec556f688887784 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Wed, 18 Oct 2023 10:19:10 +0000 Subject: [PATCH] Migrate remaining Java Code and modernize it --- gradle/libs.versions.toml | 2 + ultrasonic/detekt-baseline.xml | 16 +- ultrasonic/src/main/AndroidManifest.xml | 25 +- .../ultrasonic/fragment/ChatFragment.java | 297 ------------------ .../moire/ultrasonic/util/BackgroundTask.java | 72 ----- .../util/FragmentBackgroundTask.java | 89 ------ .../moire/ultrasonic/util/LoadingTask.java | 66 ---- .../ultrasonic/util/ProgressListener.java | 29 -- .../moire/ultrasonic/util/ShareDetails.java | 16 - .../ultrasonic/util/TimeSpanPreference.java | 38 --- .../ultrasonic/fragment/AboutFragment.kt | 3 +- .../ultrasonic/fragment/AlbumListFragment.kt | 16 +- .../ultrasonic/fragment/ArtistListFragment.kt | 2 +- .../ultrasonic/fragment/BookmarksFragment.kt | 11 +- .../ultrasonic/fragment/EntryListFragment.kt | 4 +- .../ultrasonic/fragment/EqualizerFragment.kt | 2 +- .../ultrasonic/fragment/FragmentTitle.kt | 34 +- .../ultrasonic/fragment/MultiListFragment.kt | 27 +- .../ultrasonic/fragment/PlayerFragment.kt | 14 +- .../ultrasonic/fragment/SearchFragment.kt | 35 +-- .../ultrasonic/fragment/SettingsFragment.kt | 2 +- .../fragment/TrackCollectionFragment.kt | 49 +-- .../fragment/legacy/ChatFragment.kt | 214 +++++++++++++ .../fragment/legacy/LyricsFragment.kt | 55 ++-- .../fragment/legacy/PlaylistsFragment.kt | 180 +++++------ .../fragment/legacy/PodcastFragment.kt | 72 ++--- .../fragment/legacy/SelectGenreFragment.kt | 73 ++--- .../fragment/legacy/SharesFragment.kt | 196 +++++------- .../ultrasonic/imageloader/ImageLoader.kt | 1 + .../moire/ultrasonic/model/ChatViewModel.kt | 31 ++ .../moire/ultrasonic/model/SearchListModel.kt | 5 +- .../provider/AlbumArtContentProvider.kt | 1 + .../ultrasonic/service/DownloadService.kt | 16 +- .../moire/ultrasonic/service/DownloadTask.kt | 2 +- .../ultrasonic/service/MediaPlayerManager.kt | 4 +- .../subsonic/ImageLoaderProvider.kt | 3 +- .../moire/ultrasonic/subsonic/ShareHandler.kt | 60 ++-- .../ultrasonic/util/CancellationToken.kt | 4 +- .../moire/ultrasonic/util/ContextMenuUtil.kt | 3 +- .../ultrasonic/util/CoroutinePatterns.kt | 36 ++- .../org/moire/ultrasonic/util/DownloadUtil.kt | 5 +- .../ultrasonic/util/RefreshableFragment.kt | 14 + .../org/moire/ultrasonic/util/ShareDetails.kt | 12 + .../moire/ultrasonic/util/TimeSpanPicker.kt | 6 +- .../ultrasonic/util/TimeSpanPreference.kt | 27 ++ .../kotlin/org/moire/ultrasonic/util/Util.kt | 36 +-- .../src/main/res/layout/filter_button_bar.xml | 2 +- .../src/main/res/layout/navigation_header.xml | 1 + .../src/main/res/layout/share_details.xml | 71 +++-- .../src/main/res/layout/time_span_dialog.xml | 11 +- .../main/res/navigation/navigation_graph.xml | 2 +- ultrasonic/src/main/res/values/strings.xml | 1 + 52 files changed, 787 insertions(+), 1206 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/FragmentBackgroundTask.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/ProgressListener.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeSpanPreference.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/ChatFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ChatViewModel.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/RefreshableFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShareDetails.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPreference.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb4af1c9..0cab93bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ media3 = "1.1.1" androidSupport = "1.7.0" materialDesign = "1.9.0" constraintLayout = "2.1.4" +activity = "1.8.0" multidex = "2.0.1" room = "2.5.2" kotlin = "1.9.10" @@ -66,6 +67,7 @@ navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-kt navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" } navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" } navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"} +activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } preferences = { module = "androidx.preference:preference", version.ref = "preferences" } media3common = { module = "androidx.media3:media3-common", version.ref = "media3" } media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } diff --git a/ultrasonic/detekt-baseline.xml b/ultrasonic/detekt-baseline.xml index a8789e05..cae63ce3 100644 --- a/ultrasonic/detekt-baseline.xml +++ b/ultrasonic/detekt-baseline.xml @@ -1,9 +1,7 @@ - - TooManyFunctions:PlaybackService.kt$PlaybackService : MediaLibraryServiceKoinComponentCoroutineScope - UnusedPrivateMember:UApp.kt$private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder - ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) ) + + ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name) @@ -11,19 +9,11 @@ LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? ) LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 - MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50 MagicNumber:RESTMusicService.kt$RESTMusicService$206 - NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean ) - TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable - TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService - UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle - - + diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index d8134933..f8870756 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -22,20 +22,22 @@ android:xlargeScreens="true"/> + android:preserveLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="false" + android:theme="@style/Theme.Material3.DynamicColors.Dark" + android:usesCleartextTraffic="true" + tools:ignore="UnusedAttribute" + tools:targetApi="q"> - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java deleted file mode 100644 index 6a091f72..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java +++ /dev/null @@ -1,297 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.ListAdapter; -import android.widget.ListView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.ChatMessage; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.ChatAdapter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -import com.google.android.material.button.MaterialButton; - -/** - * Provides online chat functionality - */ -public class ChatFragment extends Fragment { - - private ListView chatListView; - private EditText messageEditText; - private MaterialButton sendButton; - private Timer timer; - private volatile static Long lastChatMessageTime = (long) 0; - private static final ArrayList messageList = new ArrayList<>(); - private CancellationToken cancellationToken; - private SwipeRefreshLayout swipeRefresh; - - private final Lazy activeServerProvider = inject(ActiveServerProvider.class); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - Util.applyTheme(this.getContext()); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.chat, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - swipeRefresh = view.findViewById(R.id.chat_refresh); - swipeRefresh.setEnabled(false); - - cancellationToken = new CancellationToken(); - messageEditText = view.findViewById(R.id.chat_edittext); - sendButton = view.findViewById(R.id.chat_send); - - sendButton.setOnClickListener(view1 -> sendMessage()); - - chatListView = view.findViewById(R.id.chat_entries_list); - chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); - chatListView.setStackFromBottom(true); - - String serverName = activeServerProvider.getValue().getActiveServer().getName(); - String userName = activeServerProvider.getValue().getActiveServer().getUserName(); - String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName); - - FragmentTitle.Companion.setTitle(this, title); - setHasOptionsMenu(true); - - messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER); - - messageEditText.addTextChangedListener(new TextWatcher() - { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) - { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) - { - } - - @Override - public void afterTextChanged(Editable editable) - { - sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString())); - } - }); - - messageEditText.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) - { - sendMessage(); - return true; - } - - return false; - }); - - load(); - timerMethod(); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.chat, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - /* - * Listen for option item selections so that we receive a notification - * when the user requests a refresh by selecting the refresh action bar item. - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Check if user triggered a refresh: - if (item.getItemId() == R.id.menu_refresh) { - // Start the refresh background task. - load(); - return true; - } - // User didn't trigger a refresh, let the superclass handle this action - return super.onOptionsItemSelected(item); - } - - @Override - public void onResume() - { - super.onResume(); - - if (!messageList.isEmpty()) - { - ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList); - chatListView.setAdapter(chatAdapter); - } - - if (timer == null) - { - timerMethod(); - } - } - - @Override - public void onPause() - { - super.onPause(); - - if (timer != null) - { - timer.cancel(); - timer = null; - } - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void timerMethod() - { - int refreshInterval = Settings.getChatRefreshInterval(); - - if (refreshInterval > 0) - { - timer = new Timer(); - - timer.schedule(new TimerTask() - { - @Override - public void run() - { - getActivity().runOnUiThread(() -> load()); - } - }, refreshInterval, refreshInterval); - } - } - - private void sendMessage() - { - if (messageEditText != null) - { - final String message; - Editable text = messageEditText.getText(); - - if (text == null) - { - return; - } - - message = text.toString(); - - if (!Util.isNullOrWhiteSpace(message)) - { - messageEditText.setText(""); - - BackgroundTask task = new FragmentBackgroundTask(getActivity(), false, swipeRefresh, cancellationToken) - { - @Override - protected Void doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.addChatMessage(message); - return null; - } - - @Override - protected void done(Void result) - { - load(); - } - }; - - task.execute(); - } - } - } - - private synchronized void load() - { - BackgroundTask> task = new FragmentBackgroundTask>(getActivity(), false, swipeRefresh, cancellationToken) - { - @Override - protected List doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - return musicService.getChatMessages(lastChatMessageTime); - } - - @Override - protected void done(List result) - { - if (result != null && !result.isEmpty()) - { - // Reset lastChatMessageTime if we have a newer message - for (ChatMessage message : result) - { - if (message.getTime() > lastChatMessageTime) - { - lastChatMessageTime = message.getTime(); - } - } - - // Reverse results to show them on the bottom - Collections.reverse(result); - messageList.addAll(result); - - ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList); - chatListView.setAdapter(chatAdapter); - } - } - - @Override - protected void error(Throwable error) { - // Stop the timer in case of an error, otherwise it may repeat the error message forever - if (timer != null) - { - timer.cancel(); - timer = null; - } - super.error(error); - } - }; - - task.execute(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java deleted file mode 100644 index bf59e1c8..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java +++ /dev/null @@ -1,72 +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.app.Activity; -import android.os.Handler; - -/** - * @author Sindre Mehus - */ -public abstract class BackgroundTask implements ProgressListener -{ - private final Activity activity; - private final Handler handler; - - public BackgroundTask(Activity activity) - { - this.activity = activity; - handler = new Handler(); - } - - protected Activity getActivity() - { - return activity; - } - - protected Handler getHandler() - { - return handler; - } - - public abstract void execute(); - - protected abstract T doInBackground() throws Throwable; - - protected abstract void done(T result); - - protected void error(Throwable error) - { - CommunicationError.handleError(error, activity); - } - - protected String getErrorMessage(Throwable error) - { - return CommunicationError.getErrorMessage(error); - } - - @Override - public abstract void updateProgress(final String message); - - @Override - public void updateProgress(int messageId) - { - updateProgress(activity.getResources().getString(messageId)); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FragmentBackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FragmentBackgroundTask.java deleted file mode 100644 index 7625a8eb..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FragmentBackgroundTask.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.app.Activity; - -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -/** - * @author Sindre Mehus - * @version $Id$ - */ -public abstract class FragmentBackgroundTask extends BackgroundTask -{ - private final boolean changeProgress; - private final SwipeRefreshLayout swipe; - private final CancellationToken cancel; - - public FragmentBackgroundTask(Activity activity, boolean changeProgress, - SwipeRefreshLayout swipe, CancellationToken cancel) - { - super(activity); - this.changeProgress = changeProgress; - this.swipe = swipe; - this.cancel = cancel; - } - - @Override - public void execute() - { - if (changeProgress) - { - if (swipe != null) swipe.setRefreshing(true); - } - - new Thread() - { - @Override - public void run() - { - try - { - final T result = doInBackground(); - if (cancel.isCancellationRequested()) - { - return; - } - - getHandler().post(new Runnable() - { - @Override - public void run() - { - if (changeProgress) - { - if (swipe != null) swipe.setRefreshing(false); - } - - done(result); - } - }); - } - catch (final Throwable t) - { - if (cancel.isCancellationRequested()) - { - return; - } - getHandler().post(new Runnable() - { - @Override - public void run() - { - if (changeProgress) - { - if (swipe != null) swipe.setRefreshing(false); - } - - error(t); - } - }); - } - } - }.start(); - } - - @Override - public void updateProgress(final String message) - { - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java deleted file mode 100644 index a2c25fa2..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.app.Activity; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -/** - * @author Sindre Mehus - * @version $Id$ - */ -public abstract class LoadingTask extends BackgroundTask -{ - private final SwipeRefreshLayout swipe; - private final CancellationToken cancel; - - public LoadingTask(Activity activity, SwipeRefreshLayout swipe, CancellationToken cancel) - { - super(activity); - this.swipe = swipe; - this.cancel = cancel; - } - - - @Override - public void execute() - { - swipe.setRefreshing(true); - - new Thread() - { - @Override - public void run() - { - try - { - final T result = doInBackground(); - if (cancel.isCancellationRequested()) - { - return; - } - - getHandler().post(() -> { - swipe.setRefreshing(false); - done(result); - }); - } - catch (final Throwable t) - { - if (cancel.isCancellationRequested()) - { - return; - } - - getHandler().post(() -> { - swipe.setRefreshing(false); - error(t); - }); - } - } - }.start(); - } - - @Override - public void updateProgress(final String message) - { - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ProgressListener.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ProgressListener.java deleted file mode 100644 index 3b32a736..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ProgressListener.java +++ /dev/null @@ -1,29 +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 - */ -public interface ProgressListener -{ - void updateProgress(String message); - - void updateProgress(int messageId); -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java deleted file mode 100644 index 005df384..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.moire.ultrasonic.util; - -import org.moire.ultrasonic.domain.Track; - -import java.util.List; - -/** - * Created by Josh on 12/17/13. - */ -public class ShareDetails -{ - public String Description; - public boolean ShareOnServer; - public long Expiration; - public List Entries; -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeSpanPreference.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeSpanPreference.java deleted file mode 100644 index 799f7f2d..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeSpanPreference.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.content.Context; -import android.util.AttributeSet; -import androidx.preference.DialogPreference; -import org.moire.ultrasonic.R; - -/** - * Created by Joshua Bahnsen on 12/22/13. - */ -public class TimeSpanPreference extends DialogPreference -{ - Context context; - - public TimeSpanPreference(Context context, AttributeSet attrs) - { - super(context, attrs); - this.context = context; - - setPositiveButtonText(android.R.string.ok); - setNegativeButtonText(android.R.string.cancel); - - setDialogIcon(null); - - } - - public String getText() - { - String persisted = getPersistedString(""); - - if (!"".equals(persisted)) - { - return persisted.replace(':', ' '); - } - - return this.context.getResources().getString(R.string.time_span_disabled); - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AboutFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AboutFragment.kt index 1e1ed54a..c7846cdd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AboutFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AboutFragment.kt @@ -18,7 +18,6 @@ import android.widget.TextView import androidx.fragment.app.Fragment import java.util.Locale import org.moire.ultrasonic.R -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.getVersionName @@ -56,7 +55,7 @@ class AboutFragment : Fragment() { versionName ) - setTitle(this@AboutFragment, getString(R.string.menu_about)) + FragmentTitle.setTitle(this@AboutFragment, getString(R.string.menu_about)) titleText?.text = title webPageButton?.setOnClickListener { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 9e7cb6c5..2a8c29c3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -5,8 +5,6 @@ * Distributed under terms of the GNU GPLv3 license. */ -@file:Suppress("NAME_SHADOWING") - package org.moire.ultrasonic.fragment import android.os.Bundle @@ -32,6 +30,7 @@ import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.model.AlbumListModel import org.moire.ultrasonic.util.LayoutType import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.toastingExceptionHandler import org.moire.ultrasonic.view.FilterButtonBar import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.ViewCapabilities @@ -76,9 +75,10 @@ class AlbumListFragment( } private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) { - - listModel.viewModelScope.launch(handler) { - refreshListView?.isRefreshing = true + listModel.viewModelScope.launch( + toastingExceptionHandler() + ) { + swipeRefresh?.isRefreshing = true if (navArgs.byArtist) { listModel.getAlbumsOfArtist( @@ -95,7 +95,7 @@ class AlbumListFragment( refresh = refresh or append ) } - refreshListView?.isRefreshing = false + swipeRefresh?.isRefreshing = false } } @@ -185,8 +185,8 @@ class AlbumListFragment( super.onViewCreated(view, savedInstanceState) // Setup refresh handler - refreshListView = view.findViewById(refreshListId) - refreshListView?.setOnRefreshListener { + swipeRefresh = view.findViewById(refreshListId) + swipeRefresh?.setOnRefreshListener { fetchAlbums(refresh = true) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 87a0ac8d..d4b873f3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -43,7 +43,7 @@ class ArtistListFragment : EntryListFragment() { * The central function to pass a query to the model and return a LiveData object */ override fun getLiveData(refresh: Boolean, append: Boolean): LiveData> { - return listModel.getItems(navArgs.refresh || refresh, refreshListView!!) + return listModel.getItems(navArgs.refresh || refresh, swipeRefresh!!) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 1e7038a9..7c235e5e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -16,8 +16,9 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MediaPlayerManager +import org.moire.ultrasonic.util.toastingExceptionHandler /** * Lists the Bookmarks available on the server @@ -40,10 +41,12 @@ class BookmarksFragment : TrackCollectionFragment() { refresh: Boolean, append: Boolean ): LiveData> { - listModel.viewModelScope.launch(handler) { - refreshListView?.isRefreshing = true + listModel.viewModelScope.launch( + toastingExceptionHandler() + ) { + swipeRefresh?.isRefreshing = true listModel.getBookmarks() - refreshListView?.isRefreshing = false + swipeRefresh?.isRefreshing = false } return listModel.currentList } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 1072790e..4b1eed22 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -73,7 +73,7 @@ abstract class EntryListFragment : MultiListFragment(), Koi currentSetting.musicFolderId = it.id serverSettingsModel.updateItem(currentSetting) } - listModel.refresh(refreshListView!!) + listModel.refresh(swipeRefresh!!) } viewAdapter.register( @@ -90,7 +90,7 @@ abstract class EntryListFragment : MultiListFragment(), Koi * What to do when the list has changed */ override val defaultObserver: (List) -> Unit = { - emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing ?: false) + emptyView.isVisible = it.isEmpty() && !(swipeRefresh?.isRefreshing ?: false) if (showFolderHeader()) { val list = mutableListOf(folderHeader) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt index 6e729045..047ed65d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt @@ -26,7 +26,7 @@ import java.util.HashMap import java.util.Locale import org.moire.ultrasonic.R import org.moire.ultrasonic.audiofx.EqualizerController -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.util.Util.applyTheme import timber.log.Timber diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt index 01386265..81b79cca 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt @@ -7,24 +7,22 @@ import androidx.navigation.fragment.NavHostFragment /** * Contains utility functions related to Fragment title handling */ -class FragmentTitle { - companion object { - fun setTitle(fragment: Fragment, title: CharSequence?) { - // Only set the title if our fragment is a direct child of the NavHostFragment... - if (fragment.parentFragment is NavHostFragment) { - (fragment.activity as AppCompatActivity).supportActionBar?.title = title - } - } - - fun setTitle(fragment: Fragment, id: Int) { - // Only set the title if our fragment is a direct child of the NavHostFragment... - if (fragment.parentFragment is NavHostFragment) { - (fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id) - } - } - - fun getTitle(fragment: Fragment): CharSequence? { - return (fragment.activity as AppCompatActivity).supportActionBar?.title +object FragmentTitle { + fun setTitle(fragment: Fragment, title: CharSequence?) { + // Only set the title if our fragment is a direct child of the NavHostFragment... + if (fragment.parentFragment is NavHostFragment) { + (fragment.activity as AppCompatActivity).supportActionBar?.title = title } } + + fun setTitle(fragment: Fragment, id: Int) { + // Only set the title if our fragment is a direct child of the NavHostFragment... + if (fragment.parentFragment is NavHostFragment) { + (fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id) + } + } + + fun getTitle(fragment: Fragment): CharSequence? { + return (fragment.activity as AppCompatActivity).supportActionBar?.title + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 0d7cca69..43cc9b2a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -8,8 +8,6 @@ package org.moire.ultrasonic.fragment import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -23,7 +21,6 @@ import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import kotlinx.coroutines.CoroutineExceptionHandler import org.koin.android.ext.android.inject import org.koin.androidx.scope.ScopeFragment import org.koin.androidx.viewmodel.ext.android.viewModel @@ -34,18 +31,18 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.subsonic.ImageLoaderProvider -import org.moire.ultrasonic.util.CommunicationError +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Util /** * An abstract Model, which can be extended to display a list of items of type T from the API * @param T: The type of data which will be used (must extend GenericEntry) */ -abstract class MultiListFragment : ScopeFragment() { +abstract class MultiListFragment : ScopeFragment(), RefreshableFragment { internal val activeServerProvider: ActiveServerProvider by inject() internal val serverSettingsModel: ServerSettingsModel by viewModel() internal val imageLoaderProvider: ImageLoaderProvider by inject() - protected var refreshListView: SwipeRefreshLayout? = null + override var swipeRefresh: SwipeRefreshLayout? = null internal var listView: RecyclerView? = null internal lateinit var viewManager: LinearLayoutManager internal lateinit var emptyView: ConstraintLayout @@ -95,16 +92,6 @@ abstract class MultiListFragment : ScopeFragment() { */ open val refreshOnCreation: Boolean = true - /** - * The default Exception Handler for Coroutines - */ - val handler = CoroutineExceptionHandler { _, exception -> - Handler(Looper.getMainLooper()).post { - CommunicationError.handleError(exception, context) - } - refreshListView?.isRefreshing = false - } - open fun setTitle(title: String?) { if (title == null) { FragmentTitle.setTitle( @@ -122,7 +109,7 @@ abstract class MultiListFragment : ScopeFragment() { * What to do when the list has changed */ internal open val defaultObserver: ((List) -> Unit) = { - emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false) + emptyView.isVisible = it.isEmpty() && !(swipeRefresh?.isRefreshing?:false) viewAdapter.submitList(it) } @@ -130,9 +117,9 @@ abstract class MultiListFragment : ScopeFragment() { super.onViewCreated(view, savedInstanceState) // Setup refresh handler - refreshListView = view.findViewById(refreshListId) - refreshListView?.setOnRefreshListener { - listModel.refresh(refreshListView!!) + swipeRefresh = view.findViewById(refreshListId) + swipeRefresh?.setOnRefreshListener { + listModel.refresh(swipeRefresh!!) } // Populate the LiveData. This starts an API request in most cases diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 2e849731..a7249b40 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -92,7 +92,7 @@ import org.moire.ultrasonic.databinding.CurrentPlayingBinding import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus @@ -778,11 +778,8 @@ class PlayerFragment : return true } R.id.menu_item_share -> { - val tracks: MutableList = ArrayList() - val playlist = mediaPlayerManager.playlist - for (item in playlist) { - val playlistEntry = item.toTrack() - tracks.add(playlistEntry) + val tracks = mediaPlayerManager.playlist.map { + it.toTrack() } shareHandler.createShare( this, @@ -793,12 +790,9 @@ class PlayerFragment : R.id.menu_item_share_song -> { if (track == null) return true - val tracks: MutableList = ArrayList() - tracks.add(track) - shareHandler.createShare( this, - tracks, + listOf(track), ) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 048d3961..9074fe16 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -34,39 +34,32 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo -import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.util.toastingExceptionHandler /** * Initiates a search on the media library and displays the results */ -class SearchFragment : MultiListFragment(), KoinScopeComponent { +class SearchFragment : MultiListFragment(), KoinScopeComponent, RefreshableFragment { private var searchResult: SearchResult? = null - private var searchRefresh: SwipeRefreshLayout? = null - + override var swipeRefresh: SwipeRefreshLayout? = null private val mediaPlayerManager: MediaPlayerManager by inject() - - private var cancellationToken: CancellationToken? = null - private val navArgs by navArgs() - override val listModel: SearchListModel by viewModels() - override val mainLayout: Int = R.layout.search override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - cancellationToken = CancellationToken() setTitle(this, R.string.search_title) listModel.searchResult.observe( @@ -79,8 +72,8 @@ class SearchFragment : MultiListFragment(), KoinScopeComponent { } } - searchRefresh = view.findViewById(R.id.swipe_refresh_view) - searchRefresh!!.isEnabled = false + swipeRefresh = view.findViewById(R.id.swipe_refresh_view) + swipeRefresh!!.isEnabled = false registerForContextMenu(listView!!) @@ -129,17 +122,17 @@ class SearchFragment : MultiListFragment(), KoinScopeComponent { override fun onDestroyView() { Util.hideKeyboard(activity) - cancellationToken?.cancel() super.onDestroyView() } private fun search(query: String, autoplay: Boolean) { - listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { - refreshListView?.isRefreshing = true - listModel.search(query) - refreshListView?.isRefreshing = false - }.invokeOnCompletion { - if (it == null && autoplay) { + listModel.viewModelScope.launch( + toastingExceptionHandler() + ) { + swipeRefresh?.isRefreshing = true + val result = listModel.search(query) + swipeRefresh?.isRefreshing = false + if (result != null && autoplay) { autoplay() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 36a8c264..8d697581 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -20,7 +20,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.log.FileLoggerTree import org.moire.ultrasonic.log.FileLoggerTree.Companion.deleteLogFiles import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileNumber diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index ee8193a9..7f7543f0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -41,7 +41,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus @@ -56,6 +56,7 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.navigateToCurrent import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.util.toastingExceptionHandler import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.ViewCapabilities import timber.log.Timber @@ -107,8 +108,8 @@ open class TrackCollectionFragment( albumButtons = view.findViewById(R.id.menu_album) // Setup refresh handler - refreshListView = view.findViewById(refreshListId) - refreshListView?.setOnRefreshListener { + swipeRefresh = view.findViewById(refreshListId) + swipeRefresh?.setOnRefreshListener { handleRefresh() } @@ -284,7 +285,7 @@ open class TrackCollectionFragment( } else if (item.itemId == R.id.menu_item_share) { shareHandler.createShare( fragment = this@TrackCollectionFragment, - tracks = getSelectedTracks(), + tracks = getSelectedOrAllTracks(), additionalId = navArgs.id ) return true @@ -338,7 +339,7 @@ open class TrackCollectionFragment( } else { mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName mediaPlayerManager.addToPlaylist( - songs = getAllSongs(), + songs = getAllTracks(), insertionMode = insertionMode, autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), shuffle = shuffle @@ -357,26 +358,20 @@ open class TrackCollectionFragment( } private fun downloadSelectedOrAllTracks(save: Boolean) { - var tracks = getSelectedTracks() - if (tracks.isEmpty()) tracks = getAllSongs() - DownloadUtil.justDownload( action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD, fragment = this, - tracks = tracks + tracks = getSelectedOrAllTracks() ) } private fun playSelectedOrAllTracks( insertionMode: MediaPlayerManager.InsertionMode ) { - var tracks = getSelectedTracks() - if (tracks.isEmpty()) tracks = getAllSongs() - mediaPlayerManager.playTracksAndToast( fragment = this, insertionMode = insertionMode, - tracks = tracks + tracks = getSelectedOrAllTracks() ) } @@ -388,13 +383,6 @@ open class TrackCollectionFragment( ) } - @Suppress("UNCHECKED_CAST") - private fun getAllSongs(): List { - return viewAdapter.getCurrentList().filter { - it is Track && !it.isDirectory - } as List - } - private fun selectAllOrNone() { val someUnselected = viewAdapter.selectedSet.size < childCount selectAll(someUnselected) @@ -503,6 +491,19 @@ open class TrackCollectionFragment( } } + @Suppress("UNCHECKED_CAST") + private fun getAllTracks(): List { + return viewAdapter.getCurrentList().filter { + it is Track && !it.isDirectory + } as List + } + + fun getSelectedOrAllTracks(): List { + return getSelectedTracks().ifEmpty { + getAllTracks() + } + } + override fun setTitle(title: String?) { setTitle(this@TrackCollectionFragment, title) } @@ -534,8 +535,10 @@ open class TrackCollectionFragment( val offset = navArgs.offset val refresh2 = navArgs.refresh || refresh - listModel.viewModelScope.launch(handler) { - refreshListView?.isRefreshing = true + listModel.viewModelScope.launch( + toastingExceptionHandler() + ) { + swipeRefresh?.isRefreshing = true if (playlistId != null) { setTitle(playlistName!!) @@ -570,7 +573,7 @@ open class TrackCollectionFragment( } } - refreshListView?.isRefreshing = false + swipeRefresh?.isRefreshing = false } return listModel.currentList } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/ChatFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/ChatFragment.kt new file mode 100644 index 00000000..9f7e4744 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/ChatFragment.kt @@ -0,0 +1,214 @@ +/* + * ChatFragment.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment.legacy + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ListAdapter +import android.widget.ListView +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.button.MaterialButton +import java.util.Locale +import java.util.Timer +import java.util.TimerTask +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle +import org.moire.ultrasonic.model.ChatViewModel +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.RefreshableFragment +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.Util.isNullOrWhiteSpace +import org.moire.ultrasonic.util.toastingExceptionHandler +import org.moire.ultrasonic.view.ChatAdapter + +class ChatFragment : Fragment(), KoinComponent, RefreshableFragment { + private lateinit var chatListView: ListView + private lateinit var messageEditText: EditText + private lateinit var sendButton: MaterialButton + private var timer: Timer? = null + override var swipeRefresh: SwipeRefreshLayout? = null + private val activeServerProvider: ActiveServerProvider by inject() + + private val chatViewModel: ChatViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + applyTheme(requireContext()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.chat, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Add the ChatMenuProvider for creating the menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + + swipeRefresh = view.findViewById(R.id.chat_refresh) + swipeRefresh?.isEnabled = false + messageEditText = view.findViewById(R.id.chat_edittext) + sendButton = view.findViewById(R.id.chat_send) + sendButton.setOnClickListener { sendMessage() } + chatListView = view.findViewById(R.id.chat_entries_list) + chatListView.transcriptMode = ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL + chatListView.isStackFromBottom = true + val serverName = activeServerProvider.getActiveServer().name + val userName = activeServerProvider.getActiveServer().userName + val title = String.format( + Locale.ROOT, + "%s [%s@%s]", + resources.getString(R.string.button_bar_chat), + userName, + serverName + ) + setTitle(this, title) + messageEditText.imeOptions = EditorInfo.IME_ACTION_SEND + messageEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun afterTextChanged(editable: Editable) { + sendButton.isEnabled = !isNullOrWhiteSpace(editable.toString()) + } + }) + messageEditText.setOnEditorActionListener( + OnEditorActionListener { + _: TextView?, + actionId: Int, + event: KeyEvent -> + if (actionId == EditorInfo.IME_ACTION_SEND || + (actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN) + ) { + sendMessage() + return@OnEditorActionListener true + } + false + } + ) + load() + timerMethod() + } + + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chat, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == R.id.menu_refresh) { + load() + return true + } + return false + } + } + override fun onResume() { + super.onResume() + chatViewModel.chatMessages.observe(viewLifecycleOwner) { messages -> + if (!messages.isNullOrEmpty()) { + val chatAdapter: ListAdapter = ChatAdapter(requireContext(), messages) + chatListView.adapter = chatAdapter + } + } + if (timer == null) { + timerMethod() + } + } + + override fun onPause() { + super.onPause() + timer?.cancel() + timer = null + } + + private fun timerMethod() { + val refreshInterval = Settings.chatRefreshInterval + if (refreshInterval > 0) { + timer = Timer() + timer?.schedule( + object : TimerTask() { + override fun run() { + requireActivity().runOnUiThread { load() } + } + }, + refreshInterval.toLong(), refreshInterval.toLong() + ) + } + } + + private fun sendMessage() { + val text = messageEditText.text ?: return + val message = text.toString() + if (!isNullOrWhiteSpace(message)) { + messageEditText.setText("") + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() + ) { + withContext(Dispatchers.IO) { + val musicService = getMusicService() + musicService.addChatMessage(message) + } + load() + } + } + } + + fun load() { + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() + ) { + val result = withContext(Dispatchers.IO) { + val musicService = getMusicService() + musicService.getChatMessages(chatViewModel.lastChatMessageTime)?.filterNotNull() + } + swipeRefresh?.isRefreshing = false + if (!result.isNullOrEmpty()) { + for (message in result) { + if (message.time > chatViewModel.lastChatMessageTime) { + chatViewModel.lastChatMessageTime = message.time + } + } + chatViewModel.updateChatMessages(result.reversed()) + } + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/LyricsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/LyricsFragment.kt index 05d0f05c..747a4b98 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/LyricsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/LyricsFragment.kt @@ -13,35 +13,34 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.Lyrics -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.toastingExceptionHandler import timber.log.Timber /** * Displays the lyrics of a song - * - * TODO: This file has been converted from Java, but not modernized yet. */ -class LyricsFragment : Fragment() { +class LyricsFragment : Fragment(), RefreshableFragment { private var artistView: TextView? = null private var titleView: TextView? = null private var textView: TextView? = null - private var swipe: SwipeRefreshLayout? = null - private var cancellationToken: CancellationToken? = null + override var swipeRefresh: SwipeRefreshLayout? = null private val navArgs by navArgs() override fun onCreate(savedInstanceState: Bundle?) { - applyTheme(this.context) super.onCreate(savedInstanceState) + applyTheme(requireContext()) } override fun onCreateView( @@ -53,42 +52,34 @@ class LyricsFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - cancellationToken = CancellationToken() + super.onViewCreated(view, savedInstanceState) Timber.d("Lyrics set title") setTitle(this, R.string.download_menu_lyrics) - swipe = view.findViewById(R.id.lyrics_refresh) - swipe?.isEnabled = false + swipeRefresh = view.findViewById(R.id.lyrics_refresh) + swipeRefresh?.isEnabled = false artistView = view.findViewById(R.id.lyrics_artist) titleView = view.findViewById(R.id.lyrics_title) textView = view.findViewById(R.id.lyrics_text) load() } - - override fun onDestroyView() { - cancellationToken!!.cancel() - super.onDestroyView() - } - private fun load() { - val task: BackgroundTask = object : FragmentBackgroundTask( - activity, true, swipe, cancellationToken + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() ) { - @Throws(Throwable::class) - override fun doInBackground(): Lyrics { + val result = withContext(Dispatchers.IO) { val musicService = getMusicService() - return musicService.getLyrics(navArgs.artist, navArgs.title)!! + musicService.getLyrics(navArgs.artist, navArgs.title)!! } - - override fun done(result: Lyrics) { + swipeRefresh?.isRefreshing = false + withContext(Dispatchers.Main) { if (result.artist != null) { - artistView!!.text = result.artist - titleView!!.text = result.title - textView!!.text = result.text + artistView?.text = result.artist + titleView?.text = result.title + textView?.text = result.text } else { - artistView!!.setText(R.string.lyrics_nomatch) + artistView?.setText(R.string.lyrics_nomatch) } } } - task.execute() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index 91d74642..d8a51c02 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -25,48 +25,46 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.ListView import android.widget.TextView +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import java.util.Locale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.androidx.scope.ScopeFragment import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R -import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Playlist -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.service.OfflineException -import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CacheCleaner -import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.DownloadAction import org.moire.ultrasonic.util.DownloadUtil -import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.InfoDialog -import org.moire.ultrasonic.util.LoadingTask +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.util.toastingExceptionHandler /** * Displays the playlists stored on the server * * TODO: This file has been converted from Java, but not modernized yet. */ -class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { - private var refreshPlaylistsListView: SwipeRefreshLayout? = null +@Suppress("InstanceOfCheckForException") +class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment { + override var swipeRefresh: SwipeRefreshLayout? = null private var playlistsListView: ListView? = null private var emptyTextView: View? = null private var playlistAdapter: ArrayAdapter? = null - private var cancellationToken: CancellationToken? = null - override fun onCreate(savedInstanceState: Bundle?) { - applyTheme(this.context) super.onCreate(savedInstanceState) + applyTheme(requireContext()) } override fun onCreateView( @@ -78,17 +76,16 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - cancellationToken = CancellationToken() - refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh) + swipeRefresh = view.findViewById(R.id.select_playlist_refresh) playlistsListView = view.findViewById(R.id.select_playlist_list) - refreshPlaylistsListView!!.setOnRefreshListener { load(true) } + swipeRefresh?.setOnRefreshListener { load(true) } emptyTextView = view.findViewById(R.id.select_playlist_empty) - playlistsListView!!.setOnItemClickListener { parent, _, position, _ -> - val (id1, name) = parent.getItemAtPosition(position) as Playlist + playlistsListView?.setOnItemClickListener { parent, _, position, _ -> + val (id, name) = parent.getItemAtPosition(position) as Playlist val action = NavigationGraphDirections.toTrackCollection( - id = id1, - playlistId = id1, + id = id, + playlistId = id, name = name, playlistName = name, ) @@ -99,33 +96,25 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { load(false) } - override fun onDestroyView() { - cancellationToken!!.cancel() - super.onDestroyView() - } - private fun load(refresh: Boolean) { - val task: BackgroundTask> = - object : FragmentBackgroundTask>( - activity, true, refreshPlaylistsListView, cancellationToken - ) { - @Throws(Throwable::class) - override fun doInBackground(): List { - val musicService = getMusicService() - val playlists = musicService.getPlaylists(refresh) - val cacheCleaner: CacheCleaner by inject() - if (!isOffline()) cacheCleaner.cleanPlaylists(playlists) - return playlists - } - - override fun done(result: List) { - playlistAdapter = - ArrayAdapter(requireContext(), R.layout.list_item_generic, result) - playlistsListView!!.adapter = playlistAdapter - emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE - } + val cacheCleaner: CacheCleaner by inject() + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() + ) { + val result = withContext(Dispatchers.IO) { + val musicService = getMusicService() + val playlists = musicService.getPlaylists(refresh) + playlists } - task.execute() + swipeRefresh?.isRefreshing = false + withContext(Dispatchers.Main) { + playlistAdapter = + ArrayAdapter(requireContext(), R.layout.list_item_generic, result) + playlistsListView?.adapter = playlistAdapter + emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + } + if (!isOffline()) cacheCleaner.cleanPlaylists(result) + } } override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { @@ -143,7 +132,7 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { override fun onContextItemSelected(menuItem: MenuItem): Boolean { val info = menuItem.menuInfo as AdapterContextMenuInfo - val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist + val playlist = playlistsListView?.getItemAtPosition(info.position) as Playlist when (menuItem.itemId) { R.id.playlist_menu_pin -> { DownloadUtil.justDownload( @@ -214,57 +203,45 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { .setTitle(R.string.common_confirm).setMessage( resources.getString(R.string.delete_playlist, playlist.name) ).setPositiveButton(R.string.common_ok) { _, _ -> - object : LoadingTask(activity, refreshPlaylistsListView, cancellationToken) { - @Throws(Throwable::class) - override fun doInBackground(): Any? { + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler( + resources.getString( + R.string.menu_deleted_playlist_error, + playlist.name + ) + ) + ) { + withContext(Dispatchers.IO) { val musicService = getMusicService() musicService.deletePlaylist(playlist.id) - return null } - override fun done(result: Any?) { - playlistAdapter!!.remove(playlist) - playlistAdapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + playlistAdapter?.remove(playlist) + playlistAdapter?.notifyDataSetChanged() toast( resources.getString(R.string.menu_deleted_playlist, playlist.name) ) } - - override fun error(error: Throwable) { - val msg: String = - if (error is OfflineException || error is ApiNotSupportedException) - getErrorMessage( - error - ) else String.format( - Locale.ROOT, - "%s %s", - resources.getString( - R.string.menu_deleted_playlist_error, - playlist.name - ), - getErrorMessage(error) - ) - toast(msg, false) - } - }.execute() + } }.setNegativeButton(R.string.common_cancel, null).show() } private fun displayPlaylistInfo(playlist: Playlist) { - val textView = TextView(context) + val textView = TextView(requireContext()) textView.setPadding(5, 5, 5, 5) val message: Spannable = SpannableString( """ - Owner: ${playlist.owner} - Comments: ${playlist.comment} - Song Count: ${playlist.songCount} + Owner: ${playlist.owner} + Comments: ${playlist.comment} + Song Count: ${playlist.songCount} """.trimIndent() + if (playlist.public == null) "" else """ - - Public: ${playlist.public} + + Public: ${playlist.public} """.trimIndent() + """ - - Creation Date: ${playlist.created.replace('T', ' ')} + + Creation Date: ${playlist.created.replace('T', ' ')} """.trimIndent() ) Linkify.addLinks(message, Linkify.WEB_URLS) @@ -293,42 +270,31 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { alertDialog.setTitle(R.string.playlist_update_info) alertDialog.setView(dialogView) alertDialog.setPositiveButton(R.string.common_ok) { _, _ -> - object : LoadingTask(activity, refreshPlaylistsListView, cancellationToken) { - @Throws(Throwable::class) - override fun doInBackground(): Any? { - val nameBoxText = nameBox.text - val commentBoxText = commentBox.text - val name = nameBoxText?.toString() - val comment = commentBoxText?.toString() - val musicService = getMusicService() + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler( + resources.getString( + R.string.playlist_updated_info_error, + playlist.name + ) + ) + ) { + val nameBoxText = nameBox.text + val commentBoxText = commentBox.text + val name = nameBoxText?.toString() + val comment = commentBoxText?.toString() + val musicService = getMusicService() + + withContext(Dispatchers.IO) { musicService.updatePlaylist(playlist.id, name, comment, publicBox.isChecked) - return null } - override fun done(result: Any?) { + withContext(Dispatchers.Main) { load(true) toast( resources.getString(R.string.playlist_updated_info, playlist.name) ) } - - override fun error(error: Throwable) { - val msg: String = - if (error is OfflineException || error is ApiNotSupportedException) - getErrorMessage( - error - ) else String.format( - Locale.ROOT, - "%s %s", - resources.getString( - R.string.playlist_updated_info_error, - playlist.name - ), - getErrorMessage(error) - ) - toast(msg, false) - } - }.execute() + } } alertDialog.setNegativeButton(R.string.common_cancel, null) alertDialog.show() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt index 2ab59b00..4c550fa1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt @@ -13,35 +13,33 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ListView -import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.androidx.scope.ScopeFragment import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.PodcastsChannel -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.toastingExceptionHandler /** * Displays the podcasts available on the server - * - * TODO: This file has been converted from Java, but not modernized yet. - * TODO: Use Coroutines */ -class PodcastFragment : Fragment() { - +class PodcastFragment : ScopeFragment(), RefreshableFragment { private var emptyTextView: View? = null - var channelItemsListView: ListView? = null - private var cancellationToken: CancellationToken? = null - private var swipeRefresh: SwipeRefreshLayout? = null + private var channelItemsListView: ListView? = null + override var swipeRefresh: SwipeRefreshLayout? = null override fun onCreate(savedInstanceState: Bundle?) { - applyTheme(this.context) super.onCreate(savedInstanceState) + applyTheme(requireContext()) } override fun onCreateView( @@ -54,45 +52,33 @@ class PodcastFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - cancellationToken = CancellationToken() swipeRefresh = view.findViewById(R.id.podcasts_refresh) - swipeRefresh!!.setOnRefreshListener { load(true) } + swipeRefresh?.setOnRefreshListener { load(true) } setTitle(this, R.string.podcasts_label) emptyTextView = view.findViewById(R.id.select_podcasts_empty) channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list) - channelItemsListView!!.setOnItemClickListener { parent, _, position, _ -> - val (id) = parent.getItemAtPosition(position) as PodcastsChannel - val action = NavigationGraphDirections.toTrackCollection( - podcastChannelId = id - ) - + channelItemsListView?.setOnItemClickListener { parent, _, position, _ -> + val id = (parent.getItemAtPosition(position) as PodcastsChannel).id + val action = NavigationGraphDirections.toTrackCollection(podcastChannelId = id) findNavController().navigate(action) } load(false) } - override fun onDestroyView() { - cancellationToken!!.cancel() - super.onDestroyView() - } - private fun load(refresh: Boolean) { - val task: BackgroundTask> = - object : FragmentBackgroundTask>( - activity, true, swipeRefresh, cancellationToken - ) { - @Throws(Throwable::class) - override fun doInBackground(): List { - val musicService = getMusicService() - return musicService.getPodcastsChannels(refresh) - } - - override fun done(result: List) { - channelItemsListView!!.adapter = - ArrayAdapter(requireContext(), R.layout.list_item_generic, result) - emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE - } + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() + ) { + val result = withContext(Dispatchers.IO) { + val musicService = getMusicService() + musicService.getPodcastsChannels(refresh) } - task.execute() + swipeRefresh?.isRefreshing = false + withContext(Dispatchers.Main) { + channelItemsListView?.adapter = + ArrayAdapter(requireContext(), R.layout.list_item_generic, result) + emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + } + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt index 0a876b45..879dafe2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt @@ -1,10 +1,3 @@ -/* - * SelectGenreFragment.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - package org.moire.ultrasonic.fragment.legacy import android.os.Bundle @@ -15,35 +8,34 @@ import android.widget.AdapterView import android.widget.ListView import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Settings.maxSongs import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.toastingExceptionHandler import org.moire.ultrasonic.view.GenreAdapter -import timber.log.Timber /** * Displays the available genres in the media library - * - * TODO: This file has been converted from Java, but not modernized yet. */ -class SelectGenreFragment : Fragment() { - private var refreshGenreListView: SwipeRefreshLayout? = null +class SelectGenreFragment : Fragment(), RefreshableFragment { + override var swipeRefresh: SwipeRefreshLayout? = null private var genreListView: ListView? = null private var emptyView: View? = null - private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { - applyTheme(this.context) super.onCreate(savedInstanceState) + applyTheme(requireContext()) } override fun onCreateView( @@ -55,15 +47,15 @@ class SelectGenreFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - cancellationToken = CancellationToken() - refreshGenreListView = view.findViewById(R.id.select_genre_refresh) + super.onViewCreated(view, savedInstanceState) + swipeRefresh = view.findViewById(R.id.select_genre_refresh) genreListView = view.findViewById(R.id.select_genre_list) - refreshGenreListView!!.setOnRefreshListener { load(true) } + swipeRefresh?.setOnRefreshListener { load(true) } - genreListView!!.setOnItemClickListener { parent: AdapterView<*>, - _: View?, - position: Int, - _: Long -> + genreListView?.setOnItemClickListener { + parent: AdapterView<*>, _: View?, + position: Int, _: Long + -> val genre = parent.getItemAtPosition(position) as Genre val action = NavigationGraphDirections.toTrackCollection( @@ -79,34 +71,19 @@ class SelectGenreFragment : Fragment() { load(false) } - override fun onDestroyView() { - cancellationToken!!.cancel() - super.onDestroyView() - } - - // TODO: Migrate to Coroutines private fun load(refresh: Boolean) { - val task: BackgroundTask> = object : FragmentBackgroundTask>( - activity, true, refreshGenreListView, cancellationToken + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() ) { - override fun doInBackground(): List { + val result = withContext(Dispatchers.IO) { val musicService = getMusicService() - var genres: List = ArrayList() - try { - genres = musicService.getGenres(refresh) - } catch (all: Exception) { - Timber.e(all, "Failed to load genres") - } - return genres + musicService.getGenres(refresh) } - - override fun done(result: List) { - emptyView!!.isVisible = result.isEmpty() - if (context != null) { - genreListView!!.adapter = GenreAdapter(context!!, result) - } + swipeRefresh?.isRefreshing = false + withContext(Dispatchers.Main) { + emptyView?.isVisible = result.isEmpty() + genreListView?.adapter = GenreAdapter(requireContext(), result) } } - task.execute() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt index 981cb3b9..a3336730 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -24,29 +24,29 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.ListView import android.widget.TextView +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import java.util.Locale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.androidx.scope.ScopeFragment import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R -import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.fragment.FragmentTitle import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.service.OfflineException -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.DownloadAction import org.moire.ultrasonic.util.DownloadUtil -import org.moire.ultrasonic.util.FragmentBackgroundTask -import org.moire.ultrasonic.util.LoadingTask +import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.TimeSpanPicker import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.util.launchWithToast +import org.moire.ultrasonic.util.toastingExceptionHandler import org.moire.ultrasonic.view.ShareAdapter /** @@ -54,16 +54,16 @@ import org.moire.ultrasonic.view.ShareAdapter * * TODO: This file has been converted from Java, but not modernized yet. */ -class SharesFragment : ScopeFragment(), KoinScopeComponent { - private var refreshSharesListView: SwipeRefreshLayout? = null +class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment { + override var swipeRefresh: SwipeRefreshLayout? = null private var sharesListView: ListView? = null private var emptyTextView: View? = null private var shareAdapter: ShareAdapter? = null private val mediaPlayerManager: MediaPlayerManager by inject() - private var cancellationToken: CancellationToken? = null + override fun onCreate(savedInstanceState: Bundle?) { - Util.applyTheme(this.context) super.onCreate(savedInstanceState) + Util.applyTheme(requireContext()) } override fun onCreateView( @@ -75,48 +75,41 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - cancellationToken = CancellationToken() - refreshSharesListView = view.findViewById(R.id.select_share_refresh) + swipeRefresh = view.findViewById(R.id.select_share_refresh) sharesListView = view.findViewById(R.id.select_share_list) - refreshSharesListView!!.setOnRefreshListener { load(true) } + swipeRefresh!!.setOnRefreshListener { load(true) } emptyTextView = view.findViewById(R.id.select_share_empty) - sharesListView!!.onItemClickListener = - AdapterView.OnItemClickListener { parent, _, position, _ -> - val share = parent.getItemAtPosition(position) as Share + sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener { + parent, _, + position, _ -> + val share = parent.getItemAtPosition(position) as Share - val action = NavigationGraphDirections.toTrackCollection( - shareId = share.id, - shareName = share.name - ) - findNavController().navigate(action) - } + val action = NavigationGraphDirections.toTrackCollection( + shareId = share.id, + shareName = share.name + ) + findNavController().navigate(action) + } registerForContextMenu(sharesListView!!) FragmentTitle.setTitle(this, R.string.button_bar_shares) load(false) } - override fun onDestroyView() { - cancellationToken!!.cancel() - super.onDestroyView() - } - private fun load(refresh: Boolean) { - val task: BackgroundTask> = object : FragmentBackgroundTask>( - activity, true, refreshSharesListView, cancellationToken + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler() ) { - @Throws(Throwable::class) - override fun doInBackground(): List { + val result = withContext(Dispatchers.IO) { val musicService = MusicServiceFactory.getMusicService() - return musicService.getShares(refresh) + musicService.getShares(refresh) } - - override fun done(result: List) { + swipeRefresh?.isRefreshing = false + withContext(Dispatchers.Main) { shareAdapter = ShareAdapter(requireContext(), result) sharesListView?.adapter = shareAdapter emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE } } - task.execute() } override fun onCreateContextMenu( @@ -202,45 +195,34 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent { .setTitle(R.string.common_confirm).setMessage( resources.getString(R.string.delete_playlist, share.name) ).setPositiveButton(R.string.common_ok) { _, _ -> - object : LoadingTask(activity, refreshSharesListView, cancellationToken) { - @Throws(Throwable::class) - override fun doInBackground(): Any? { - val musicService = MusicServiceFactory.getMusicService() - musicService.deleteShare(share.id) - return null - } - - override fun done(result: Any?) { - shareAdapter!!.remove(share) - shareAdapter!!.notifyDataSetChanged() - toast( - resources.getString(R.string.menu_deleted_share, share.name) - ) - } - - override fun error(error: Throwable) { - val msg: String = - if (error is OfflineException || error is ApiNotSupportedException) { - getErrorMessage( - error - ) - } else { - String.format( - Locale.ROOT, - "%s %s", - resources.getString( - R.string.menu_deleted_share_error, - share.name - ), - getErrorMessage(error) - ) - } - toast(msg, false) - } - }.execute() + deleteShareOnServer(share) }.setNegativeButton(R.string.common_cancel, null).show() } + private fun deleteShareOnServer(share: Share) { + viewLifecycleOwner.lifecycleScope.launch( + toastingExceptionHandler( + resources.getString( + R.string.menu_deleted_share_error, + share.name + ) + ) + ) { + withContext(Dispatchers.IO) { + val musicService = MusicServiceFactory.getMusicService() + musicService.deleteShare(share.id) + } + + withContext(Dispatchers.Main) { + shareAdapter?.remove(share) + shareAdapter?.notifyDataSetChanged() + toast( + resources.getString(R.string.menu_deleted_share, share.name) + ) + } + } + } + private fun displayShareInfo(share: Share) { val textView = TextView(context) textView.setPadding(5, 5, 5, 5) @@ -255,18 +237,18 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent { ( if (share.created == null) "" else """ - Creation Date: ${share.created!!.replace('T', ' ')} + Creation Date: ${share.created!!.replace('T', ' ')} """.trimIndent() ) + ( if (share.lastVisited == null) "" else """ - Last Visited Date: ${share.lastVisited!!.replace('T', ' ')} + Last Visited Date: ${share.lastVisited!!.replace('T', ' ')} """.trimIndent() ) + if (share.expires == null) "" else """ - Expiration Date: ${share.expires!!.replace('T', ' ')} + Expiration Date: ${share.expires!!.replace('T', ' ')} """.trimIndent() ) Linkify.addLinks(message, Linkify.WEB_URLS) @@ -297,49 +279,31 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent { alertDialog.setTitle(R.string.playlist_update_info) alertDialog.setView(dialogView) alertDialog.setPositiveButton(R.string.common_ok) { _, _ -> - object : LoadingTask(activity, refreshSharesListView, cancellationToken) { - @Throws(Throwable::class) - override fun doInBackground(): Any? { - var millis = timeSpanPicker.getTimeSpan() - if (millis > 0) { - millis += System.currentTimeMillis() - } - val shareDescriptionText = shareDescription.text - val description = shareDescriptionText?.toString() - val musicService = MusicServiceFactory.getMusicService() - musicService.updateShare(share.id, description, millis) - return null - } - - override fun done(result: Any?) { - load(true) - toast( - resources.getString(R.string.playlist_updated_info, share.name) - ) - } - - override fun error(error: Throwable) { - val msg: String = - if (error is OfflineException || error is ApiNotSupportedException) { - getErrorMessage( - error - ) - } else { - String.format( - Locale.ROOT, - "%s %s", - resources.getString( - R.string.playlist_updated_info_error, - share.name - ), - getErrorMessage(error) - ) - } - toast(msg, false) - } - }.execute() + var millis = timeSpanPicker.getTimeSpan() + if (millis > 0) { + millis += System.currentTimeMillis() + } + updateShareOnServer(millis, shareDescription.text.toString(), share) } alertDialog.setNegativeButton(R.string.common_cancel, null) alertDialog.show() } + + private fun updateShareOnServer( + millis: Long, + description: String, + share: Share + ) { + launchWithToast { + withContext(Dispatchers.IO) { + val musicService = MusicServiceFactory.getMusicService() + musicService.updateShare(share.id, description, millis) + } + + withContext(Dispatchers.Main) { + load(true) + resources.getString(R.string.playlist_updated_info, share.name) + } + } + } } 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 a63417ca..bb7a76c0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -197,6 +197,7 @@ class ImageLoader( * Download a cover art file of a Track and cache it on disk */ fun downloadCoverArt(track: Track) { + if (track.coverArt == null) return downloadCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track)) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ChatViewModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ChatViewModel.kt new file mode 100644 index 00000000..6f285062 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ChatViewModel.kt @@ -0,0 +1,31 @@ +/* + * ChatViewModel.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.model +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.moire.ultrasonic.domain.ChatMessage + +class ChatViewModel : ViewModel() { + + // MutableLiveData to store chat messages + private val _chatMessages = MutableLiveData>() + + // LiveData to observe chat messages + val chatMessages: LiveData> + get() = _chatMessages + + // Last chat message time + var lastChatMessageTime: Long = 0 + + // Function to update chat messages + fun updateChatMessages(messages: List) { + val updatedMessages = _chatMessages.value.orEmpty() + messages + _chatMessages.postValue(updatedMessages) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index 84087b86..b0492475 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -14,17 +14,18 @@ class SearchListModel(application: Application) : GenericListModel(application) var searchResult: MutableLiveData = MutableLiveData() - suspend fun search(query: String) { + suspend fun search(query: String): SearchResult? { val maxArtists = Settings.maxArtists val maxAlbums = Settings.maxAlbums val maxSongs = Settings.maxSongs - withContext(Dispatchers.IO) { + return withContext(Dispatchers.IO) { val criteria = SearchCriteria(query, maxArtists, maxAlbums, maxSongs) val service = MusicServiceFactory.getMusicService() val result = service.search(criteria) if (result != null) searchResult.postValue(result) + result } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt index 026fb22f..d55de716 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt @@ -58,6 +58,7 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent { Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile) // TODO: Check if the dependency on the image loader could be removed. + // TODO: This method can be called outside of our regular lifecycle, where Koin might not exist yet imageLoaderProvider.executeOn { it.downloadCoverArt(parts[0], albumArtFile) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 8eeb60ba..0bf444a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -510,12 +510,16 @@ class DownloadService : Service(), KoinComponent { } private fun startService() { - val context = UApp.applicationContext() - val intent = Intent(context, DownloadService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) + try { + val context = UApp.applicationContext() + val intent = Intent(context, DownloadService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } catch (e: IllegalStateException) { + Timber.w(e, "Failed to start download service: the app is in the background") } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt index 483220b2..443defb5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt @@ -256,7 +256,7 @@ class DownloadTask( // Download the largest size that we can display in the UI imageLoaderProvider.executeOn { imageLoader -> - imageLoader.downloadCoverArt(this) + imageLoader.downloadCoverArt(this@cacheMetadataAndArtwork) // Cache small copies of the Artist picture directArtist?.let { imageLoader.cacheArtistPicture(it) } compilationArtist?.let { imageLoader.cacheArtistPicture(it) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt index 17eedd37..6671a0f3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -11,7 +11,6 @@ import android.os.Handler import android.os.Looper import androidx.annotation.IntRange import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import androidx.media3.common.C import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem @@ -502,9 +501,8 @@ class MediaPlayerManager( shuffle: Boolean = false, isArtist: Boolean = false ) { - val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope - scope.launchWithToast { + fragment.launchWithToast { val list: List = tracks.ifEmpty { 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 daf0eb98..b70852f1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -12,6 +12,7 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.imageloader.ImageLoaderConfig +import org.moire.ultrasonic.util.CoroutinePatterns import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -55,7 +56,7 @@ class ImageLoaderProvider : } fun executeOn(cb: (iL: ImageLoader) -> Unit) { - launch { + launch(CoroutinePatterns.loggingExceptionHandler) { val iL = getImageLoader() withContext(Dispatchers.Main) { cb(iL) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index 2dd7f18f..ce2f0eaa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -68,14 +68,14 @@ class ShareHandler { val ids: MutableList = ArrayList() - if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) + if (!shareDetails.shareOnServer && shareDetails.entries.size == 1) return@withContext null - if (shareDetails.Entries.isEmpty()) { + if (shareDetails.entries.isEmpty()) { additionalId.ifNotNull { ids.add(it) } } else { - for ((id) in shareDetails.Entries) { + for ((id) in shareDetails.entries) { ids.add(id) } } @@ -83,15 +83,21 @@ class ShareHandler { val musicService = getMusicService() var timeInMillis: Long = 0 - if (shareDetails.Expiration != 0L) { - timeInMillis = shareDetails.Expiration + if (shareDetails.expiration != 0L) { + timeInMillis = shareDetails.expiration } - val shares = - musicService.createShare(ids, shareDetails.Description, timeInMillis) + val shares = musicService.createShare( + ids = ids, + description = shareDetails.description, + expires = timeInMillis + ) // Return the share - shares[0] + if (shares.isNotEmpty()) + shares[0] + else + null } catch (ignored: Exception) { null } @@ -120,15 +126,15 @@ class ShareHandler { val textBuilder = StringBuilder() textBuilder.appendLine(Settings.shareGreeting) - if (!shareDetails.Entries[0].title.isNullOrEmpty()) + if (!shareDetails.entries[0].title.isNullOrEmpty()) textBuilder.append(getString(R.string.common_title)) - .append(": ").appendLine(shareDetails.Entries[0].title) - if (!shareDetails.Entries[0].artist.isNullOrEmpty()) + .append(": ").appendLine(shareDetails.entries[0].title) + if (!shareDetails.entries[0].artist.isNullOrEmpty()) textBuilder.append(getString(R.string.common_artist)) - .append(": ").appendLine(shareDetails.Entries[0].artist) - if (!shareDetails.Entries[0].album.isNullOrEmpty()) + .append(": ").appendLine(shareDetails.entries[0].artist) + if (!shareDetails.entries[0].album.isNullOrEmpty()) textBuilder.append(getString(R.string.common_album)) - .append(": ").append(shareDetails.Entries[0].album) + .append(": ").append(shareDetails.entries[0].album) intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString()) } @@ -144,17 +150,17 @@ class ShareHandler { fun createShare( fragment: Fragment, - tracks: List?, + tracks: List, additionalId: String? = null ) { + if (tracks.isEmpty()) return val askForDetails = Settings.shouldAskForShareDetails - val shareDetails = ShareDetails() - shareDetails.Entries = tracks + val shareDetails = ShareDetails(tracks) if (askForDetails) { showDialog(fragment, shareDetails, additionalId) } else { - shareDetails.Description = Settings.defaultShareDescription - shareDetails.Expiration = System.currentTimeMillis() + + shareDetails.description = Settings.defaultShareDescription + shareDetails.expiration = System.currentTimeMillis() + Settings.defaultShareExpirationInMillis share(fragment, shareDetails, additionalId) } @@ -177,12 +183,12 @@ class ShareHandler { noExpirationCheckBox = timeSpanPicker!!.findViewById( R.id.timeSpanDisableCheckBox ) as CheckBox - textViewComment = layout.findViewById(R.id.textViewComment) as TextView - textViewExpiration = layout.findViewById(R.id.textViewExpiration) as TextView + textViewComment = layout.findViewById(R.id.commentHeading) as TextView + textViewExpiration = layout.findViewById(R.id.expirationHeading) as TextView } // Handle the visibility based on shareDetails.Entries size - if (shareDetails.Entries.size == 1) { + if (shareDetails.entries.size == 1) { shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ -> updateVisibility() } @@ -238,11 +244,11 @@ class ShareHandler { builder.setPositiveButton(R.string.menu_share) { _, _ -> if (!noExpirationCheckBox!!.isChecked) { val timeSpan: Long = timeSpanPicker!!.getTimeSpan() - shareDetails.Expiration = System.currentTimeMillis() + timeSpan + shareDetails.expiration = System.currentTimeMillis() + timeSpan } - shareDetails.Description = shareDescription!!.text.toString() - shareDetails.ShareOnServer = shareOnServerCheckBox!!.isChecked + shareDetails.description = shareDescription!!.text.toString() + shareDetails.shareOnServer = shareOnServerCheckBox!!.isChecked if (hideDialogCheckBox!!.isChecked) { Settings.shouldAskForShareDetails = false @@ -255,8 +261,8 @@ class ShareHandler { if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0) String.format("%d:%s", timeSpanAmount, timeSpanType) else "" - Settings.defaultShareDescription = shareDetails.Description - Settings.shareOnServer = shareDetails.ShareOnServer + Settings.defaultShareDescription = shareDetails.description!! + Settings.shareOnServer = shareDetails.shareOnServer } share(fragment, shareDetails, additionalId) 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 0318f70c..6dbb142d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CancellationToken.kt @@ -8,7 +8,9 @@ package org.moire.ultrasonic.util /** * This class contains a very simple implementation of a CancellationToken - */ + * TODO: Remove this class and refactor all user to coroutines + **/ + class CancellationToken { var isCancellationRequested: Boolean = false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt index 220ca6c1..420b4c90 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt @@ -130,8 +130,7 @@ object ContextMenuUtil : KoinComponent { val shareHandler: ShareHandler by inject() shareHandler.createShare( fragment = fragment, - tracks = tracks, - additionalId = null + tracks = tracks ) } else -> return false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt index 08b9f07c..dad0ae39 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -9,13 +9,16 @@ package org.moire.ultrasonic.util import android.os.Handler import android.os.Looper +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch -import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.util.CommunicationError.getErrorMessage +import org.moire.ultrasonic.util.Util.toast import timber.log.Timber object CoroutinePatterns { @@ -28,24 +31,43 @@ object CoroutinePatterns { } } -fun CoroutineScope.launchWithToast( +fun Fragment.toastingExceptionHandler( + prefix: String = "" +): CoroutineExceptionHandler { + return CoroutineExceptionHandler { _, exception -> + // Stop the spinner if applicable + if (this is RefreshableFragment) { + this.swipeRefresh?.isRefreshing = false + } + toast("$prefix ${getErrorMessage(exception)}", shortDuration = false) + } +} + +/* +* Launch a coroutine with a toast +* This extension can be only started from a fragment +* because it needs the fragments scope to create the toast + */ +fun Fragment.launchWithToast( block: suspend CoroutineScope.() -> String? ) { + // Get the scope + val scope = activity?.lifecycleScope ?: lifecycleScope + // Launch the Job - val deferred = async(CoroutinePatterns.loggingExceptionHandler, block = block) + val deferred = scope.async(block = block) // Setup a handler when the job is done deferred.invokeOnCompletion { val toastString = if (it != null && it !is CancellationException) { - CommunicationError.getErrorMessage(it) + getErrorMessage(it) } else { null } - - launch(Dispatchers.Main) { + scope.launch(Dispatchers.Main) { val successString = toastString ?: deferred.await() if (successString != null) { - Util.toast(successString, UApp.applicationContext()) + this@launchWithToast.toast(successString) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt index 8c78b73f..00ce9acd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt @@ -8,7 +8,6 @@ package org.moire.ultrasonic.util import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -36,10 +35,8 @@ object DownloadUtil { tracks: List? = null ) { - val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope - // Launch the Job - scope.launchWithToast { + fragment.launchWithToast { val tracksToDownload: List = tracks ?: getTracksFromServerAsync(isArtist, id!!, isDirectory, name, isShare) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/RefreshableFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/RefreshableFragment.kt new file mode 100644 index 00000000..749c4d61 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/RefreshableFragment.kt @@ -0,0 +1,14 @@ +/* + * RefreshableFragment.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +interface RefreshableFragment { + var swipeRefresh: SwipeRefreshLayout? +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShareDetails.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShareDetails.kt new file mode 100644 index 00000000..bf0ba606 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShareDetails.kt @@ -0,0 +1,12 @@ +package org.moire.ultrasonic.util + +import org.moire.ultrasonic.domain.Track + +/** + * Created by Josh on 12/17/13. + */ +data class ShareDetails(val entries: List) { + var description: String? = null + var shareOnServer = false + var expiration: Long = 0 +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt index 90316aaf..33b66540 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt @@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty AdapterView.OnItemSelectedListener { private val timeSpanEditText: EditText private val timeSpanSpinner: Spinner - val timeSpanDisableCheckbox: CheckBox + private val timeSpanDisableCheckbox: CheckBox private var mTimeSpan: Long = -1L private val adapter: ArrayAdapter private val dialog: View @@ -49,7 +49,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty dialog = inflater.inflate(R.layout.time_span_dialog, this, true) timeSpanEditText = dialog.findViewById(R.id.timeSpanEditText) as EditText timeSpanEditText.setText("0") - timeSpanSpinner = dialog.findViewById(R.id.timeSpanSpinner) as Spinner + timeSpanSpinner = dialog.findViewById(R.id.timeSpanUnitSelector) as Spinner timeSpanDisableCheckbox = dialog.findViewById(R.id.timeSpanDisableCheckBox) as CheckBox timeSpanDisableCheckbox.setOnCheckedChangeListener { _, b -> @@ -128,7 +128,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty companion object { fun getTimeSpanFromDialog(context: Context, dialog: View): Long { val timeSpanEditText = dialog.findViewById(R.id.timeSpanEditText) as EditText - val timeSpanSpinner = dialog.findViewById(R.id.timeSpanSpinner) as Spinner + val timeSpanSpinner = dialog.findViewById(R.id.timeSpanUnitSelector) as Spinner val timeSpanType = timeSpanSpinner.selectedItem as String Timber.i("SELECTED ITEM: %d", timeSpanSpinner.selectedItemId) val text = timeSpanEditText.text diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPreference.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPreference.kt new file mode 100644 index 00000000..cbb55ddd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPreference.kt @@ -0,0 +1,27 @@ +package org.moire.ultrasonic.util + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.DialogPreference +import org.moire.ultrasonic.R + +/** + * Created by Joshua Bahnsen on 12/22/13. + */ +class TimeSpanPreference(mContext: Context, attrs: AttributeSet?) : DialogPreference( + mContext, attrs +) { + init { + setPositiveButtonText(android.R.string.ok) + setNegativeButtonText(android.R.string.cancel) + dialogIcon = null + } + + val text: String + get() { + val persisted = getPersistedString("") + return if ("" != persisted) { + persisted.replace(':', ' ') + } else context.resources.getString(R.string.time_span_disabled) + } +} 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 fae3ef41..a21df27f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -30,7 +30,6 @@ import android.os.Build import android.os.Environment import android.text.TextUtils import android.util.DisplayMetrics -import android.view.Gravity import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.ComponentActivity @@ -87,7 +86,6 @@ object Util { // Used by hexEncode() private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') - private var toast: Toast? = null // Retrieves an instance of the application Context fun appContext(): Context { @@ -145,19 +143,11 @@ object Util { // some background processing, our context might have expired! fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) { try { - if (toast == null) { - toast = Toast.makeText( - context, - message, - if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG - ) - toast!!.setGravity(Gravity.CENTER, 0, 0) - } else { - toast!!.setText(message) - toast!!.duration = - if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG - } - toast!!.show() + Toast.makeText( + context, + message, + if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + ).show() } catch (all: Exception) { Timber.w(all) } @@ -277,15 +267,16 @@ object Util { * @param s The string to encode. * @return The encoded string. */ - @Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught") + @Suppress("TooGenericExceptionThrown") fun utf8HexEncode(s: String?): String? { if (s == null) { return null } val utf8: ByteArray = try { s.toByteArray(charset(Constants.UTF_8)) - } catch (x: UnsupportedEncodingException) { - throw RuntimeException(x) + } catch (all: UnsupportedEncodingException) { + // TODO: Why is it needed to change the exception type here? + throw RuntimeException(all) } return hexEncode(utf8) } @@ -299,7 +290,7 @@ object Util { * @return A string containing hexadecimal characters. */ @Suppress("MagicNumber") - fun hexEncode(data: ByteArray): String { + private fun hexEncode(data: ByteArray): String { val length = data.size val out = CharArray(length shl 1) var j = 0 @@ -319,15 +310,16 @@ object Util { * @return MD5 digest as a hex string. */ @JvmStatic - @Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught") + @Suppress("TooGenericExceptionThrown") fun md5Hex(s: String?): String? { return if (s == null) { null } else try { val md5 = MessageDigest.getInstance("MD5") hexEncode(md5.digest(s.toByteArray(charset(Constants.UTF_8)))) - } catch (x: Exception) { - throw RuntimeException(x.message, x) + } catch (all: Exception) { + // TODO: Why is it needed to change the exception type here? + throw RuntimeException(all.message, all) } } diff --git a/ultrasonic/src/main/res/layout/filter_button_bar.xml b/ultrasonic/src/main/res/layout/filter_button_bar.xml index 2a20c8c6..806652df 100644 --- a/ultrasonic/src/main/res/layout/filter_button_bar.xml +++ b/ultrasonic/src/main/res/layout/filter_button_bar.xml @@ -45,7 +45,7 @@ app:layout_constraintHorizontal_chainStyle="spread_inside" app:layout_constraintStart_toEndOf="@+id/chip_view_toggle" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintWidth_default="wrap"> + > \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/share_details.xml b/ultrasonic/src/main/res/layout/share_details.xml index 4e27ed2d..e717152e 100644 --- a/ultrasonic/src/main/res/layout/share_details.xml +++ b/ultrasonic/src/main/res/layout/share_details.xml @@ -1,70 +1,79 @@ - - + a:id="@+id/share_details" + a:layout_width="match_parent" + a:layout_height="wrap_content" + a:padding="8dp"> + a:padding="16dp"> - + a:text="@string/share_on_server" /> + a:theme="@style/Ultrasonic.AllCapsLabel" /> + a:layout_marginBottom="16dip" + a:inputType="text" + a:singleLine="false" /> + + a:theme="@style/Ultrasonic.AllCapsLabel" /> + + + + + a:layout_marginTop="16dip" + a:labelFor="@id/share_description" + a:text="@string/settings.share_options" + a:theme="@style/Ultrasonic.AllCapsLabel" /> - + a:layout_marginTop="0dp" + a:layout_marginBottom="0dp" + a:text="@string/do_not_show_dialog_again" /> - + a:layout_marginTop="0dp" + a:layout_marginBottom="8dp" + a:text="@string/save_as_defaults" /> diff --git a/ultrasonic/src/main/res/layout/time_span_dialog.xml b/ultrasonic/src/main/res/layout/time_span_dialog.xml index 65b95425..69106219 100644 --- a/ultrasonic/src/main/res/layout/time_span_dialog.xml +++ b/ultrasonic/src/main/res/layout/time_span_dialog.xml @@ -3,11 +3,11 @@ + android:orientation="horizontal"> diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 906d8a6e..066933fd 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -232,7 +232,7 @@ android:name="org.moire.ultrasonic.fragment.BookmarksFragment" /> + android:name="org.moire.ultrasonic.fragment.legacy.ChatFragment" /> diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 4ab4279d..98c0263f 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -356,6 +356,7 @@ Save as default Comment Time To Expiration + Options \"%s\" was removed from playlist Share Playlist Share Current Song