From 410e4d9d963e250d231d9890939bc7dd62992c84 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 31 Jul 2023 16:56:07 +0000 Subject: [PATCH 01/27] Translated using Weblate (Spanish) Currently translated at 100.0% (429 of 429 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/ --- ultrasonic/src/main/res/values-es/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 6584faf7..6583680d 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -455,4 +455,9 @@ Canciones aleatorias Reproducir las canciones aleatoriamente No me gusta + + %d canción añadida a la cola de reproducción + %d canciones añadidas a la cola de reproducción + %d canciones añadidas a la cola de reproducción + \ No newline at end of file From 6b000bc90f685b977eb7345a906b4e83ab68d279 Mon Sep 17 00:00:00 2001 From: birdbird Date: Thu, 3 Aug 2023 11:18:26 +0000 Subject: [PATCH 02/27] Translated using Weblate (German) Currently translated at 99.3% (426 of 429 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/ --- ultrasonic/src/main/res/values-de/strings.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index c73cc34b..bc954811 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -8,7 +8,7 @@ Antwort nicht verstanden. Bitte die Serveradresse überprüfen. HTTPS Zertifikatsfehler: %1$s. SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen. - Bitte warten…  + Bitte warten… Lesezeichen Medienbibliothek Chat @@ -59,8 +59,8 @@ Fernbedienung ist nicht erlaubt. Bitte Jukebox Modus auf dem Subsonic Server in Benutzer > Einstellungen aktivieren. Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben. Fernbedienungs-Modus is Offline nicht verfügbar. - Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben. - Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren. + Fernbedienung eingeschaltet. Musik wird auf dem Server wiedergegeben. + Fernbedienungs Modus wird nicht unterstützt. Bitte aktualisiere den Subsonic Server. Equalizer Jukebox Aus Jukebox An @@ -443,9 +443,15 @@ Album-Artwork Benachrichtigungen sind für die Medienwiedergabe erforderlich. Du kannst die Erlaubnis jederzeit in den Android-Einstellungen erteilen. Hardware-Wiedergabe verwenden (experimentell) - Versuche, die Medien mit dem Mediendecoder-Chip auf Deinem Telefon abzuspielen. Dadurch kann der Akku besser genutzt werden. + Versuche, die Medien mit dem Mediendecoder-Chip auf Deinem Telefon abzuspielen. Dadurch kann der Akku besser genutzt werden. Einige Nutzer berichten über Stotterer beim Abspielen, wenn diese Option aktiv ist. Liste Cover Unterstützte Funktionen Jukebox + Zufällige Titel abspielen + Zufällige Titel + + Ein Titel wurde zur Wiedergabeliste hinzugefügt + %d Titel wurden zur Wiedergabeliste hinzugefügt + \ No newline at end of file From 71d45c89fb6c33abcb3aada8039092a4a3c9fbd5 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sat, 5 Aug 2023 15:10:25 +0000 Subject: [PATCH 03/27] Migrate more files to Kotlin --- .../receiver/UltrasonicIntentReceiver.java | 38 ----- .../service/ExternalStorageMonitor.java | 53 ------ .../moire/ultrasonic/service/Scrobbler.java | 50 ------ .../moire/ultrasonic/service/Supplier.java | 12 -- .../moire/ultrasonic/view/ChatAdapter.java | 159 ------------------ .../moire/ultrasonic/view/GenreAdapter.java | 110 ------------ .../moire/ultrasonic/view/ShareAdapter.java | 56 ------ .../org/moire/ultrasonic/view/ShareView.java | 64 ------- .../fragment/legacy/SharesFragment.kt | 19 ++- .../receiver/BluetoothIntentReceiver.kt | 74 ++++---- .../receiver/UltrasonicIntentReceiver.kt | 34 ++++ .../service/ExternalStorageMonitor.kt | 47 ++++++ .../org/moire/ultrasonic/service/Scrobbler.kt | 52 ++++++ .../org/moire/ultrasonic/view/ChatAdapter.kt | 108 ++++++++++++ .../org/moire/ultrasonic/view/GenreAdapter.kt | 71 ++++++++ .../org/moire/ultrasonic/view/ShareAdapter.kt | 53 ++++++ .../src/main/res/layout/share_list_item.xml | 5 +- 17 files changed, 411 insertions(+), 594 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt (54%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ExternalStorageMonitor.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Scrobbler.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/GenreAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ShareAdapter.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java deleted file mode 100644 index 5eb8e214..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.moire.ultrasonic.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import timber.log.Timber; - -import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -public class UltrasonicIntentReceiver extends BroadcastReceiver -{ - private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); - - @Override - public void onReceive(Context context, Intent intent) - { - String intentAction = intent.getAction(); - Timber.i("Received Ultrasonic Intent: %s", intentAction); - - try - { - lifecycleSupport.getValue().receiveIntent(intent); - - if (isOrderedBroadcast()) - { - abortBroadcast(); - } - } - catch (Exception x) - { - // Ignored. - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java deleted file mode 100644 index a2813758..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.moire.ultrasonic.service; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; - -import org.moire.ultrasonic.app.UApp; - -import timber.log.Timber; - -/** - * Monitors the state of the mobile's external storage - */ -public class ExternalStorageMonitor -{ - private BroadcastReceiver ejectEventReceiver; - private boolean externalStorageAvailable = true; - - public void onCreate(final Runnable ejectedCallback) - { - // Stop when SD card is ejected. - ejectEventReceiver = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); - if (!externalStorageAvailable) - { - Timber.i("External media is ejecting. Stopping playback."); - ejectedCallback.run(); - } - else - { - Timber.i("External media is available."); - } - } - }; - - IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); - ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); - ejectFilter.addDataScheme("file"); - UApp.Companion.applicationContext().registerReceiver(ejectEventReceiver, ejectFilter); - } - - public void onDestroy() - { - UApp.Companion.applicationContext().unregisterReceiver(ejectEventReceiver); - } - - public boolean isExternalStorageAvailable() { return externalStorageAvailable; } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java deleted file mode 100644 index 27ea8172..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.moire.ultrasonic.service; - -import timber.log.Timber; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.Track; - -/** - * Scrobbles played songs to Last.fm. - * - * @author Sindre Mehus - * @version $Id$ - */ -public class Scrobbler -{ - private String lastSubmission; - private String lastNowPlaying; - - public void scrobble(final Track song, final boolean submission) - { - if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return; - - final String id = song.getId(); - - // Avoid duplicate registrations. - if (submission && id.equals(lastSubmission)) return; - - if (!submission && id.equals(lastNowPlaying)) return; - - if (submission) lastSubmission = id; - else lastNowPlaying = id; - - new Thread(String.format("Scrobble %s", song)) - { - @Override - public void run() - { - MusicService service = MusicServiceFactory.getMusicService(); - try - { - service.scrobble(id, submission); - Timber.i("Scrobbled '%s' for %s", submission ? "submission" : "now playing", song); - } - catch (Exception x) - { - Timber.i(x, "Failed to scrobble'%s' for %s", submission ? "submission" : "now playing", song); - } - } - }.start(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java deleted file mode 100644 index 6137491b..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.moire.ultrasonic.service; - -/** - * Abstract class for supplying items to a consumer - * @param The type of the item supplied - */ -public abstract class Supplier -{ - public abstract T get(); -} - - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java deleted file mode 100644 index 6a6ccf37..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.ChatMessage; -import org.moire.ultrasonic.imageloader.ImageLoader; -import org.moire.ultrasonic.subsonic.ImageLoaderProvider; - -import java.text.DateFormat; -import java.util.Date; -import java.util.List; -import java.util.regex.Pattern; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -public class ChatAdapter extends ArrayAdapter -{ - private final Context context; - private final List messages; - - private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; - private static final Pattern phoneMatcher = Pattern.compile(phoneRegex); - - private final Lazy activeServerProvider = inject(ActiveServerProvider.class); - private final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); - - public ChatAdapter(Context context, List messages) - { - super(context, R.layout.chat_item, messages); - this.context = context; - this.messages = messages; - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public boolean isEnabled(int position) { - return false; - } - - @Override - public int getCount() - { - return messages.size(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) - { - ChatMessage message = this.getItem(position); - - ViewHolder holder; - int layout; - - String messageUser = message.getUsername(); - Date messageTime = new java.util.Date(message.getTime()); - String messageText = message.getMessage(); - - String me = activeServerProvider.getValue().getActiveServer().getUserName(); - - layout = messageUser.equals(me) ? R.layout.chat_item_reverse : R.layout.chat_item; - - if (convertView == null) - { - convertView = inflateView(layout, parent); - holder = createViewHolder(layout, convertView); - } - else - { - holder = (ViewHolder) convertView.getTag(); - - if (!holder.chatMessage.equals(message)) - { - convertView = inflateView(layout, parent); - holder = createViewHolder(layout, convertView); - } - } - - holder.chatMessage = message; - - DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context); - String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime)); - - ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader(); - - if (holder.avatar != null && !TextUtils.isEmpty(messageUser)) - { - imageLoader.loadAvatarImage(holder.avatar, messageUser); - } - - holder.username.setText(messageUser); - holder.message.setText(messageText); - holder.time.setText(messageTimeFormatted); - - return convertView; - } - - private View inflateView(int layout, ViewGroup parent) - { - return LayoutInflater.from(context).inflate(layout, parent, false); - } - - private static ViewHolder createViewHolder(int layout, View convertView) - { - ViewHolder holder = new ViewHolder(); - holder.layout = layout; - - TextView usernameView; - TextView timeView; - TextView messageView; - ImageView imageView; - - if (convertView != null) - { - usernameView = (TextView) convertView.findViewById(R.id.chat_username); - timeView = (TextView) convertView.findViewById(R.id.chat_time); - messageView = (TextView) convertView.findViewById(R.id.chat_message); - imageView = (ImageView) convertView.findViewById(R.id.chat_avatar); - - messageView.setMovementMethod(LinkMovementMethod.getInstance()); - Linkify.addLinks(messageView, Linkify.ALL); - Linkify.addLinks(messageView, phoneMatcher, "tel:"); - - holder.avatar = imageView; - holder.message = messageView; - holder.username = usernameView; - holder.time = timeView; - - convertView.setTag(holder); - } - - return holder; - } - - private static class ViewHolder - { - int layout; - ImageView avatar; - TextView message; - TextView username; - TextView time; - ChatMessage chatMessage; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java deleted file mode 100644 index eeb1c48b..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java +++ /dev/null @@ -1,110 +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 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.view; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.SectionIndexer; -import android.widget.TextView; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Genre; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; - -/** - * @author Sindre Mehus - */ -public class GenreAdapter extends ArrayAdapter implements SectionIndexer -{ - private final LayoutInflater layoutInflater; - // Both arrays are indexed by section ID. - private final Object[] sections; - private final Integer[] positions; - - public GenreAdapter(@NonNull Context context, List genres) - { - super(context, R.layout.list_item_generic, genres); - - layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - Collection sectionSet = new LinkedHashSet(30); - List positionList = new ArrayList(30); - - for (int i = 0; i < genres.size(); i++) - { - Genre genre = genres.get(i); - String index = genre.getIndex(); - if (!sectionSet.contains(index)) - { - sectionSet.add(index); - positionList.add(i); - } - } - - sections = sectionSet.toArray(new Object[0]); - positions = positionList.toArray(new Integer[0]); - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View rowView = convertView; - if (rowView == null) { - rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false); - } - - ((TextView) rowView).setText(getItem(position).getName()); - - return rowView; - } - - @Override - public Object[] getSections() - { - return sections; - } - - @Override - public int getPositionForSection(int section) - { - return positions[section]; - } - - @Override - public int getSectionForPosition(int pos) - { - for (int i = 0; i < sections.length - 1; i++) - { - if (pos < positions[i + 1]) - { - return i; - } - } - return sections.length - 1; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java deleted file mode 100644 index 2df142a7..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Share; - -import java.util.List; - -/** - * @author Sindre Mehus - */ -public class ShareAdapter extends ArrayAdapter -{ - private final Context context; - - public ShareAdapter(Context context, List Shares) - { - super(context, R.layout.share_list_item, Shares); - this.context = context; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) - { - Share entry = getItem(position); - ShareView view; - - if (convertView instanceof ShareView) - { - ShareView currentView = (ShareView) convertView; - - ViewHolder viewHolder = (ViewHolder) convertView.getTag(); - view = currentView; - view.setViewHolder(viewHolder); - } - else - { - view = new ShareView(context); - view.setLayout(); - } - - view.setShare(entry); - return view; - } - - static class ViewHolder - { - TextView url; - TextView description; - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java deleted file mode 100644 index 0bed3b2c..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java +++ /dev/null @@ -1,64 +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.view; - -import android.content.Context; -import android.view.LayoutInflater; -import android.widget.LinearLayout; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Share; - -/** - * Used to display playlists in a {@code ListView}. - * - * @author Joshua Bahnsen - */ -public class ShareView extends LinearLayout -{ - private final Context context; - private ShareAdapter.ViewHolder viewHolder; - - public ShareView(Context context) - { - super(context); - this.context = context; - } - - public void setLayout() - { - LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true); - viewHolder = new ShareAdapter.ViewHolder(); - viewHolder.url = findViewById(R.id.share_url); - viewHolder.description = findViewById(R.id.share_description); - setTag(viewHolder); - } - - public void setViewHolder(ShareAdapter.ViewHolder viewHolder) - { - this.viewHolder = viewHolder; - setTag(this.viewHolder); - } - - public void setShare(Share share) - { - viewHolder.url.setText(share.getName()); - viewHolder.description.setText(share.getDescription()); - } -} \ No newline at end of file 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 31fa05bd..32bb6176 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 @@ -1,6 +1,6 @@ /* * SharesFragment.kt - * Copyright (C) 2009-2022 Ultrasonic developers + * Copyright (C) 2009-2023 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -58,7 +58,7 @@ class SharesFragment : Fragment(), KoinComponent { private var sharesListView: ListView? = null private var emptyTextView: View? = null private var shareAdapter: ShareAdapter? = null - private val downloadHandler = inject() + private val downloadHandler: DownloadHandler by inject() private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { Util.applyTheme(this.context) @@ -110,8 +110,9 @@ class SharesFragment : Fragment(), KoinComponent { } override fun done(result: List) { - sharesListView!!.adapter = ShareAdapter(context, result).also { shareAdapter = it } - emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + shareAdapter = ShareAdapter(requireContext(), result) + sharesListView?.adapter = shareAdapter + emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE } } task.execute() @@ -132,7 +133,7 @@ class SharesFragment : Fragment(), KoinComponent { val share = sharesListView!!.getItemAtPosition(info.position) as Share when (menuItem.itemId) { R.id.share_menu_pin -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.PIN, fragment = this, id = share.id, @@ -142,7 +143,7 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_unpin -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.UNPIN, fragment = this, id = share.id, @@ -152,7 +153,7 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_download -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.DOWNLOAD, fragment = this, id = share.id, @@ -162,7 +163,7 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_play_now -> { - downloadHandler.value.fetchTracksAndAddToController( + downloadHandler.fetchTracksAndAddToController( this, share.id, share.name, @@ -172,7 +173,7 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_play_shuffled -> { - downloadHandler.value.fetchTracksAndAddToController( + downloadHandler.fetchTracksAndAddToController( this, share.id, share.name, diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt similarity index 54% rename from ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt index 727a7ff5..54753ea9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt @@ -1,6 +1,6 @@ /* * BluetoothIntentReceiver.kt - * Copyright (C) 2009-2022 Ultrasonic developers + * Copyright (C) 2009-2023 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -10,9 +10,6 @@ package org.moire.ultrasonic.receiver import android.Manifest import android.bluetooth.BluetoothA2dp import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED -import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED -import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED import android.bluetooth.BluetoothProfile import android.content.BroadcastReceiver import android.content.Context @@ -22,9 +19,6 @@ import android.os.Build import androidx.core.app.ActivityCompat import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_A2DP -import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_ALL -import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_DISABLED import org.moire.ultrasonic.util.Settings import timber.log.Timber @@ -42,27 +36,27 @@ class BluetoothIntentReceiver : BroadcastReceiver() { Timber.d("Bluetooth device: $name; State: $state; Action: $action") // In these flags we store what kind of device (any or a2dp) has (dis)connected - var connectionStatus = PREFERENCE_VALUE_DISABLED - var disconnectionStatus = PREFERENCE_VALUE_DISABLED + var connectionStatus = Constants.PREFERENCE_VALUE_DISABLED + var disconnectionStatus = Constants.PREFERENCE_VALUE_DISABLED // First check for general devices when (action) { - ACTION_ACL_CONNECTED -> { - connectionStatus = PREFERENCE_VALUE_ALL + BluetoothDevice.ACTION_ACL_CONNECTED -> { + connectionStatus = Constants.PREFERENCE_VALUE_ALL } - ACTION_ACL_DISCONNECTED, - ACTION_ACL_DISCONNECT_REQUESTED -> { - disconnectionStatus = PREFERENCE_VALUE_ALL + BluetoothDevice.ACTION_ACL_DISCONNECTED, + BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED -> { + disconnectionStatus = Constants.PREFERENCE_VALUE_ALL } } // Then check for A2DP devices when (state) { BluetoothA2dp.STATE_CONNECTED -> { - connectionStatus = PREFERENCE_VALUE_A2DP + connectionStatus = Constants.PREFERENCE_VALUE_A2DP } BluetoothA2dp.STATE_DISCONNECTED -> { - disconnectionStatus = PREFERENCE_VALUE_A2DP + disconnectionStatus = Constants.PREFERENCE_VALUE_A2DP } } @@ -72,20 +66,20 @@ class BluetoothIntentReceiver : BroadcastReceiver() { // Now check the settings and set the appropriate flags when (Settings.resumeOnBluetoothDevice) { - PREFERENCE_VALUE_ALL -> { - shouldResume = (connectionStatus != PREFERENCE_VALUE_DISABLED) + Constants.PREFERENCE_VALUE_ALL -> { + shouldResume = (connectionStatus != Constants.PREFERENCE_VALUE_DISABLED) } - PREFERENCE_VALUE_A2DP -> { - shouldResume = (connectionStatus == PREFERENCE_VALUE_A2DP) + Constants.PREFERENCE_VALUE_A2DP -> { + shouldResume = (connectionStatus == Constants.PREFERENCE_VALUE_A2DP) } } when (Settings.pauseOnBluetoothDevice) { - PREFERENCE_VALUE_ALL -> { - shouldPause = (disconnectionStatus != PREFERENCE_VALUE_DISABLED) + Constants.PREFERENCE_VALUE_ALL -> { + shouldPause = (disconnectionStatus != Constants.PREFERENCE_VALUE_DISABLED) } - PREFERENCE_VALUE_A2DP -> { - shouldPause = (disconnectionStatus == PREFERENCE_VALUE_A2DP) + Constants.PREFERENCE_VALUE_A2DP -> { + shouldPause = (disconnectionStatus == Constants.PREFERENCE_VALUE_A2DP) } } @@ -105,24 +99,24 @@ class BluetoothIntentReceiver : BroadcastReceiver() { ) } } -} -private fun BluetoothDevice?.getNameSafely(): String? { - val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - ( - ActivityCompat.checkSelfPermission( - UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT - ) != PackageManager.PERMISSION_GRANTED - ) + private fun BluetoothDevice?.getNameSafely(): String? { + val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ( + ActivityCompat.checkSelfPermission( + UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) - return if (logBluetoothName) this?.name else "Unknown" -} + return if (logBluetoothName) this?.name else "Unknown" + } -private fun Intent.getBluetoothDevice(): BluetoothDevice? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) - } else { - @Suppress("DEPRECATION") - getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + private fun Intent.getBluetoothDevice(): BluetoothDevice? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt new file mode 100644 index 00000000..9f75fdfd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt @@ -0,0 +1,34 @@ +/* + * UltrasonicIntentReceiver.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import timber.log.Timber + +class UltrasonicIntentReceiver : BroadcastReceiver() { + private val lifecycleSupport = inject( + MediaPlayerLifecycleSupport::class.java + ) + + override fun onReceive(context: Context, intent: Intent) { + val intentAction = intent.action + Timber.i("Received Ultrasonic Intent: %s", intentAction) + try { + lifecycleSupport.value.receiveIntent(intent) + if (isOrderedBroadcast) { + abortBroadcast() + } + } catch (_: Exception) { + // Ignored. + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ExternalStorageMonitor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ExternalStorageMonitor.kt new file mode 100644 index 00000000..86cee2b8 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ExternalStorageMonitor.kt @@ -0,0 +1,47 @@ +/* + * ExternalStorageMonitor.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import timber.log.Timber + +/** + * Monitors the state of the mobile's external storage + */ +class ExternalStorageMonitor { + private var ejectEventReceiver: BroadcastReceiver? = null + var isExternalStorageAvailable = true + private set + + fun onCreate(ejectedCallback: Runnable) { + // Stop when SD card is ejected. + ejectEventReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + isExternalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED == intent.action + if (!isExternalStorageAvailable) { + Timber.i("External media is ejecting. Stopping playback.") + ejectedCallback.run() + } else { + Timber.i("External media is available.") + } + } + } + val ejectFilter = IntentFilter(Intent.ACTION_MEDIA_EJECT) + ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED) + ejectFilter.addDataScheme("file") + applicationContext().registerReceiver(ejectEventReceiver, ejectFilter) + } + + fun onDestroy() { + applicationContext().unregisterReceiver(ejectEventReceiver) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Scrobbler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Scrobbler.kt new file mode 100644 index 00000000..0f7d6e3f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Scrobbler.kt @@ -0,0 +1,52 @@ +/* + * Scrobbler.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.service + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isScrobblingEnabled +import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import timber.log.Timber + +/** + * Scrobbles played songs to Last.fm. + */ +class Scrobbler : CoroutineScope by CoroutineScope(Dispatchers.IO) { + private var lastSubmission: String? = null + private var lastNowPlaying: String? = null + fun scrobble(song: Track?, submission: Boolean) { + if (song == null || !isScrobblingEnabled()) return + val id = song.id + + // Avoid duplicate registrations. + if (submission && id == lastSubmission) return + if (!submission && id == lastNowPlaying) return + if (submission) lastSubmission = id else lastNowPlaying = id + + launch { + val service = getMusicService() + try { + service.scrobble(id, submission) + Timber.i( + "Scrobbled '%s' for %s", + if (submission) "submission" else "now playing", + song + ) + } catch (all: Exception) { + Timber.i( + all, + "Failed to scrobble'%s' for %s", + if (submission) "submission" else "now playing", + song + ) + } + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt new file mode 100644 index 00000000..cb2db531 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt @@ -0,0 +1,108 @@ +/* + * ChatAdapter.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.view + +import android.content.Context +import android.text.TextUtils +import android.text.format.DateFormat +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +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.domain.ChatMessage +import org.moire.ultrasonic.subsonic.ImageLoaderProvider + +class ChatAdapter(private val context: Context, private val messages: List) : + ArrayAdapter( + context, R.layout.chat_item, messages + ), + KoinComponent { + + private val activeServerProvider: ActiveServerProvider by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() + + override fun areAllItemsEnabled(): Boolean { + return true + } + + override fun isEnabled(position: Int): Boolean { + return false + } + + override fun getCount(): Int { + return messages.size + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var view = convertView + val message = getItem(position) + val holder: ViewHolder + val layout: Int + val me = activeServerProvider.getActiveServer().userName + + // Different layouts based on the message sender/recipient + layout = if (message?.username == me) R.layout.chat_item_reverse else R.layout.chat_item + + if (view == null || (view.tag as ViewHolder).layout != layout) { + view = inflateView(layout, parent) + holder = ViewHolder() + } else { + holder = view.tag as ViewHolder + } + + linkHolder(holder, layout, view) + if (message != null) setData(holder, message) + return view + } + + private fun inflateView(layout: Int, parent: ViewGroup): View { + return LayoutInflater.from(context).inflate(layout, parent, false) + } + + private class ViewHolder { + var layout = R.layout.chat_item + var avatar: ImageView? = null + var message: TextView? = null + var username: TextView? = null + var time: TextView? = null + var chatMessage: ChatMessage? = null + } + + private fun linkHolder(holder: ViewHolder, layout: Int, view: View) { + holder.layout = layout + holder.avatar = view.findViewById(R.id.chat_avatar) + holder.message = view.findViewById(R.id.chat_message) + holder.message?.movementMethod = LinkMovementMethod.getInstance() + holder.username = view.findViewById(R.id.chat_username) + holder.time = view.findViewById(R.id.chat_time) + view.tag = holder + } + + private fun setData( + holder: ViewHolder, + message: ChatMessage + ) { + holder.chatMessage = message + val timeFormat = DateFormat.getTimeFormat(context) + val messageTimeFormatted = "[${timeFormat.format(message.time)}]" + val imageLoader = imageLoaderProvider.getImageLoader() + if (holder.avatar != null && !TextUtils.isEmpty(message.username)) { + imageLoader.loadAvatarImage(holder.avatar!!, message.username) + } + holder.username?.text = message.username + holder.message?.text = message.message + holder.time?.text = messageTimeFormatted + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/GenreAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/GenreAdapter.kt new file mode 100644 index 00000000..0641a83a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/GenreAdapter.kt @@ -0,0 +1,71 @@ +/* + * GenreAdapter.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ +package org.moire.ultrasonic.view + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.SectionIndexer +import android.widget.TextView +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Genre + +class GenreAdapter(context: Context, genres: List) : + ArrayAdapter(context, R.layout.list_item_generic, genres), SectionIndexer { + private val layoutInflater: LayoutInflater + + // Both arrays are indexed by section ID. + private val sections: Array + private val positions: Array + + init { + layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val sectionSet: MutableCollection = LinkedHashSet(INITIAL_CAPACITY) + val positionList: MutableList = ArrayList(INITIAL_CAPACITY) + for (i in genres.indices) { + val (index) = genres[i] + if (!sectionSet.contains(index)) { + sectionSet.add(index) + positionList.add(i) + } + } + sections = sectionSet.toTypedArray() + positions = positionList.toTypedArray() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var rowView = convertView + if (rowView == null) { + rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false) + } + (rowView as TextView?)!!.text = getItem(position)!!.name + return rowView!! + } + + override fun getSections(): Array { + return sections + } + + override fun getPositionForSection(section: Int): Int { + return positions[section] + } + + override fun getSectionForPosition(pos: Int): Int { + for (i in 0 until sections.size - 1) { + if (pos < positions[i + 1]) { + return i + } + } + return sections.size - 1 + } + + companion object { + const val INITIAL_CAPACITY: Int = 30 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ShareAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ShareAdapter.kt new file mode 100644 index 00000000..9f6d3195 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ShareAdapter.kt @@ -0,0 +1,53 @@ +/* + * ShareAdapter.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.view + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Share + +class ShareAdapter(private val context: Context, shares: List) : ArrayAdapter( + context, R.layout.share_list_item, shares +) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val entry = getItem(position) + val view: View + val holder: ViewHolder + + if (convertView == null) { + holder = ViewHolder() + val inflater = LayoutInflater.from(context) + view = inflater.inflate(R.layout.share_list_item, parent, false) + holder.url = view.findViewById(R.id.share_url) + holder.description = view.findViewById(R.id.share_description) + view.tag = holder + } else { + view = convertView + holder = view.tag as ViewHolder + } + + if (entry != null) setData(entry, holder) + + return view + } + + private fun setData(entry: Share, holder: ViewHolder) { + holder.url?.text = entry.name + holder.description?.text = entry.description + } + + class ViewHolder { + var url: TextView? = null + var description: TextView? = null + } +} diff --git a/ultrasonic/src/main/res/layout/share_list_item.xml b/ultrasonic/src/main/res/layout/share_list_item.xml index a0f3c45e..6bd892c3 100644 --- a/ultrasonic/src/main/res/layout/share_list_item.xml +++ b/ultrasonic/src/main/res/layout/share_list_item.xml @@ -1,15 +1,14 @@ + a:paddingEnd="16dip"> Date: Mon, 7 Aug 2023 19:31:40 +0000 Subject: [PATCH 04/27] Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6137c2c9..9965426a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ navigation = "2.6.0" gradlePlugin = "8.1.0" androidxcore = "1.10.1" ktlint = "0.43.2" -ktlintGradle = "11.5.0" +ktlintGradle = "11.5.1" detekt = "1.23.0" preferences = "1.2.1" media3 = "1.1.0" From 82f67586499d138ef26f7e8bcc51257d15de2f64 Mon Sep 17 00:00:00 2001 From: Alex Katlein Date: Wed, 9 Aug 2023 21:16:52 +0200 Subject: [PATCH 05/27] Utilize CarConnection to determine whether to set repeat mode to ALL --- gradle/libs.versions.toml | 4 +- ultrasonic/build.gradle | 1 + ultrasonic/src/main/AndroidManifest.xml | 2 + .../playback/AutoMediaBrowserCallback.kt | 44 ++++++++++++++++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6137c2c9..9b704ce0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ gradle = "8.1.1" navigation = "2.6.0" gradlePlugin = "8.1.0" +androidxcar = "1.2.0" androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.5.0" @@ -49,6 +50,7 @@ kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin" ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" } detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +car = { module = "androidx.car.app:app", version.ref = "androidxcar" } core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" } design = { module = "com.google.android.material:material", version.ref = "materialDesign" } annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" } @@ -103,4 +105,4 @@ apacheCodecs = { module = "commons-codec:commons-codec", version.ref robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } [plugins] -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 004d151c..08dd6559 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -100,6 +100,7 @@ dependencies { exclude group: "com.android.support" } + implementation libs.car implementation libs.core implementation libs.design implementation libs.multidex diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 711f13e2..0d25405d 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -12,6 +12,8 @@ + + = Build.VERSION_CODES.M) { + Timber.d("Car app library available, observing CarConnection") + + val originalRepeatMode = player.repeatMode + + var lastCarConnectionType = -1 + + CarConnection(applicationContext).type.observeForever { + if (lastCarConnectionType == it) + return@observeForever + + lastCarConnectionType = it + + Timber.d("CarConnection type changed to %s", it) + + when (it) { + CarConnection.CONNECTION_TYPE_PROJECTION -> + if (!customRepeatModeSet) { + Timber.d("[CarConnection] Setting repeat mode to ALL") + player.repeatMode = Player.REPEAT_MODE_ALL + customRepeatModeSet = true + } + + CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> + if (customRepeatModeSet) { + Timber.d("[CarConnection] Resetting repeat mode") + player.repeatMode = originalRepeatMode + customRepeatModeSet = false + } + } + } + } else + Timber.d("Car app library not available") + } + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { if (controller.controllerVersion != 0) { // Let Media3 controller (for instance the MediaNotificationProvider) @@ -369,6 +410,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac PlaybackService.CUSTOM_COMMAND_REPEAT_MODE -> { customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS)) + customRepeatModeSet = true session.player.setNextRepeatMode() session.updateCustomCommands() From ab10d39f75fecc6bd4371a36878c033d016273aa Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 9 Aug 2023 20:31:51 +0000 Subject: [PATCH 06/27] Update dependency org.mockito.kotlin:mockito-kotlin to v5.1.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6137c2c9..9051be3b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ picasso = "2.8" junit4 = "4.13.2" junit5 = "5.10.0" mockito = "5.4.0" -mockitoKotlin = "5.0.0" +mockitoKotlin = "5.1.0" kluent = "1.73" apacheCodecs = "1.16.0" robolectric = "4.10.3" From 002a3250e4bed539d13e7df1f12a8e6578f660c0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 16 Aug 2023 12:32:00 +0000 Subject: [PATCH 07/27] Update media3 to v1.1.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6137c2c9..b6d7a09b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ ktlint = "0.43.2" ktlintGradle = "11.5.0" detekt = "1.23.0" preferences = "1.2.1" -media3 = "1.1.0" +media3 = "1.1.1" androidSupport = "1.6.0" materialDesign = "1.9.0" From fa3ca57a4efeb59834303c51512f250fbf9a78ab Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 17 Aug 2023 07:32:56 +0000 Subject: [PATCH 08/27] Update dependency gradle to v8.3 --- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c747538f..d11cdd90 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca1..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From 0a3717f448234b979bd5e4c336ae1f6f6accf5e4 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 22 Aug 2023 08:14:06 +0000 Subject: [PATCH 09/27] Merge back from release --- fastlane/metadata/android/en-US/changelogs/130.txt | 9 +++++++++ ultrasonic/build.gradle | 4 ++-- .../ultrasonic/playback/AutoMediaBrowserCallback.kt | 2 +- .../org/moire/ultrasonic/service/MediaPlayerManager.kt | 2 +- .../org/moire/ultrasonic/subsonic/DownloadHandler.kt | 2 +- ultrasonic/src/main/res/drawable/empty.xml | 7 ------- 6 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/130.txt delete mode 100644 ultrasonic/src/main/res/drawable/empty.xml diff --git a/fastlane/metadata/android/en-US/changelogs/130.txt b/fastlane/metadata/android/en-US/changelogs/130.txt new file mode 100644 index 00000000..3d2ccf81 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/130.txt @@ -0,0 +1,9 @@ +### Features +- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto) +- Properly handling nested directory structures (Android Auto) +- Add a toast when adding tracks to the playlist +- Allow pinning when offline + +### Dependencies +- Update koin +- Update media3 to v1.1.0 \ No newline at end of file diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 004d151c..cea9d70f 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -12,8 +12,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 126 - versionName "4.6.3" + versionCode 128 + versionName "4.7.0" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index d7abb1bb..0154a05a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -276,7 +276,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac private fun getPlaceholderButton() = CommandButton.Builder() .setDisplayName("Placeholder") - .setIconResId(R.drawable.empty) + .setIconResId(android.R.color.transparent) .setSessionCommand( SessionCommand( PlaybackService.CUSTOM_COMMAND_PLACEHOLDER, 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 93588750..d07b4c52 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -460,7 +460,7 @@ class MediaPlayerManager( // We can't just use play(0,0) then all random playlists will start with the first track. // Additionally the shuffle order becomes clear on after some time, so we need to wait for // the right event, and can start playback only then. - if (autoPlay) { + if (autoPlay && controller?.isPlaying != true) { if (isShufflePlayEnabled) { deferredPlay = { val start = controller?.currentTimeline diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 207c9141..a0b4c156 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -169,7 +169,7 @@ class DownloadHandler( ) if (Settings.shouldTransitionOnPlayback && - (insertionMode == MediaPlayerManager.InsertionMode.CLEAR || autoPlay) + insertionMode == MediaPlayerManager.InsertionMode.CLEAR ) { fragment.findNavController().popBackStack(R.id.playerFragment, true) fragment.findNavController().navigate(R.id.playerFragment) diff --git a/ultrasonic/src/main/res/drawable/empty.xml b/ultrasonic/src/main/res/drawable/empty.xml deleted file mode 100644 index a4762a86..00000000 --- a/ultrasonic/src/main/res/drawable/empty.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - From cd101aef2af95d0b35a2449c39ba66eb0f82ba4b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 22 Aug 2023 09:31:49 +0000 Subject: [PATCH 10/27] Update dependency com.android.tools.build:gradle to v8.1.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b704ce0..511d4584 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "8.1.1" navigation = "2.6.0" -gradlePlugin = "8.1.0" +gradlePlugin = "8.1.1" androidxcar = "1.2.0" androidxcore = "1.10.1" ktlint = "0.43.2" From deef540ddcd3215ee46beef42bec5aa7ade94a18 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 22 Aug 2023 10:08:53 +0000 Subject: [PATCH 11/27] Disable a lint --- ultrasonic/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index de0ec940..40c72e39 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -81,6 +81,7 @@ android { disable 'ObsoleteLintCustomCheck' // We manage dependencies on Gitlab with RenovateBot disable 'GradleDependency' + disable 'AndroidGradlePluginVersion' textReport true checkDependencies true } From 896c946a5a03a6cb81a63f864c9ad475b5b3cd26 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 22 Aug 2023 13:31:52 +0000 Subject: [PATCH 12/27] Update dependency org.mockito:mockito-core to v5.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d35a63f4..e507d621 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ picasso = "2.8" junit4 = "4.13.2" junit5 = "5.10.0" -mockito = "5.4.0" +mockito = "5.5.0" mockitoKotlin = "5.1.0" kluent = "1.73" apacheCodecs = "1.16.0" From fb123d926d885e5a17e260b54bb5e3c982e2f364 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Aug 2023 09:45:59 +0000 Subject: [PATCH 13/27] Update dependency com.google.devtools.ksp to v1.9.0-1.0.13 --- gradle/libs.versions.toml | 6 +++--- gradle/versions.gradle | 4 ++-- .../moire/ultrasonic/fragment/PlayerFragment.kt | 8 ++++---- .../moire/ultrasonic/fragment/SettingsFragment.kt | 4 +++- .../moire/ultrasonic/service/JukeboxMediaPlayer.kt | 1 + .../service/JukeboxUnimplementedFunctions.kt | 14 ++++++++++++++ 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d35a63f4..c41108d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # You need to run ./gradlew wrapper after updating the version gradle = "8.1.1" -navigation = "2.6.0" +navigation = "2.7.0" gradlePlugin = "8.1.1" androidxcar = "1.2.0" androidxcore = "1.10.1" @@ -17,8 +17,8 @@ materialDesign = "1.9.0" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" -kotlin = "1.8.22" -ksp = "1.8.22-1.0.11" +kotlin = "1.9.0" +ksp = "1.9.0-1.0.13" kotlinxCoroutines = "1.7.3" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 4e7d1ef2..3b8d8617 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -1,5 +1,5 @@ ext.versions = [ minSdk : 21, targetSdk : 33, - compileSdk : 33, -] \ No newline at end of file + compileSdk : 34, +] 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 adcffd95..dd7a23c1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -1219,14 +1219,14 @@ class PlayerFragment : @Suppress("ReturnCount") override fun onFling( - e1: MotionEvent, + e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float ): Boolean { - val e1X = e1.x + val e1X = e1?.x ?: 0F val e2X = e2.x - val e1Y = e1.y + val e1Y = e1?.y ?: 0F val e2Y = e2.y val absX = abs(velocityX) val absY = abs(velocityY) @@ -1263,7 +1263,7 @@ class PlayerFragment : override fun onLongPress(e: MotionEvent) {} override fun onScroll( - e1: MotionEvent, + e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float 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 d007c399..9e654186 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -108,7 +108,9 @@ class SettingsFragment : preferences.unregisterOnSharedPreferenceChangeListener(this) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == null || sharedPreferences == null) return + Timber.d("Preference changed: %s", key) updateCustomPreferences() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index fb7d3b71..4be7ed45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -286,6 +286,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} + @Deprecated("Deprecated in Java") override fun setDeviceVolume(volume: Int) { setDeviceVolume(volume, 0) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt index c92abd23..d32b2ca6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt @@ -91,10 +91,12 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun hasPrevious(): Boolean { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun hasPreviousWindow(): Boolean { TODO("Not yet implemented") } @@ -103,10 +105,12 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun previous() { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun seekToPreviousWindow() { TODO("Not yet implemented") } @@ -115,10 +119,12 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun hasNext(): Boolean { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun hasNextWindow(): Boolean { TODO("Not yet implemented") } @@ -127,10 +133,12 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun next() { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun seekToNextWindow() { TODO("Not yet implemented") } @@ -163,10 +171,12 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun getCurrentWindowIndex(): Int { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun getNextWindowIndex(): Int { TODO("Not yet implemented") } @@ -175,6 +185,7 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun getPreviousWindowIndex(): Int { TODO("Not yet implemented") } @@ -183,14 +194,17 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun isCurrentWindowDynamic(): Boolean { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun isCurrentWindowLive(): Boolean { TODO("Not yet implemented") } + @Deprecated("Deprecated in Java") override fun isCurrentWindowSeekable(): Boolean { TODO("Not yet implemented") } From 2d943edd6121a6c0103943b045d216f8d9ebf14a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Aug 2023 10:31:43 +0000 Subject: [PATCH 14/27] Update kotlin monorepo to v1.9.10 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7492581..2a47f388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ materialDesign = "1.9.0" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" -kotlin = "1.9.0" +kotlin = "1.9.10" ksp = "1.9.0-1.0.13" kotlinxCoroutines = "1.7.3" viewModelKtx = "2.6.1" From e581776e43d53c8892f4a17f027e1ac1aeaabe33 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Aug 2023 12:31:51 +0000 Subject: [PATCH 15/27] Update dependency io.reactivex.rxjava3:rxjava to v3.1.7 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a47f388..8ec6f398 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ robolectric = "4.10.3" timber = "5.0.1" fastScroll = "2.0.1" colorPicker = "2.2.4" -rxJava = "3.1.6" +rxJava = "3.1.7" rxAndroid = "3.0.2" multiType = "4.3.0" From 8ab3b2634d850aeebffc955c0df27480132f9e37 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Aug 2023 17:08:15 +0000 Subject: [PATCH 16/27] Update navigation to v2.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ec6f398..e96393c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # You need to run ./gradlew wrapper after updating the version gradle = "8.1.1" -navigation = "2.7.0" +navigation = "2.7.1" gradlePlugin = "8.1.1" androidxcar = "1.2.0" androidxcore = "1.10.1" From d2ed058d311884f7508d06500ce1cbe9b4a90b64 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 24 Aug 2023 06:32:43 +0000 Subject: [PATCH 17/27] Sync master and develop --- fastlane/metadata/android/en-US/changelogs/128.txt | 9 +++++++++ fastlane/metadata/android/en-US/changelogs/129.txt | 12 ++++++++++++ ultrasonic/build.gradle | 4 ++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/128.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/129.txt diff --git a/fastlane/metadata/android/en-US/changelogs/128.txt b/fastlane/metadata/android/en-US/changelogs/128.txt new file mode 100644 index 00000000..3d2ccf81 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/128.txt @@ -0,0 +1,9 @@ +### Features +- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto) +- Properly handling nested directory structures (Android Auto) +- Add a toast when adding tracks to the playlist +- Allow pinning when offline + +### Dependencies +- Update koin +- Update media3 to v1.1.0 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/129.txt b/fastlane/metadata/android/en-US/changelogs/129.txt new file mode 100644 index 00000000..99df21de --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/129.txt @@ -0,0 +1,12 @@ +### Fixes +- Fix a bug in 4.7.0 that repeat mode was activated by default. + +### Features +- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto) +- Properly handling nested directory structures (Android Auto) +- Add a toast when adding tracks to the playlist +- Allow pinning when offline + +### Dependencies +- Update koin +- Update media3 to v1.1.0 \ No newline at end of file diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 40c72e39..5f9420ae 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -12,8 +12,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 128 - versionName "4.7.0" + versionCode 129 + versionName "4.7.1" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk From db0f3b21e1d4298085a0baddae50d64e2c05ac2d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 24 Aug 2023 09:31:42 +0000 Subject: [PATCH 18/27] Update dependency com.google.devtools.ksp to v1.9.10-1.0.13 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..751681ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" kotlin = "1.9.10" -ksp = "1.9.0-1.0.13" +ksp = "1.9.10-1.0.13" kotlinxCoroutines = "1.7.3" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" From f6ad5be3e05d7d96dd6c10d76831801562f7a996 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 6 Sep 2023 17:31:37 +0000 Subject: [PATCH 19/27] Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.6.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..b7c2803e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ room = "2.5.2" kotlin = "1.9.10" ksp = "1.9.0-1.0.13" kotlinxCoroutines = "1.7.3" -viewModelKtx = "2.6.1" +viewModelKtx = "2.6.2" swipeRefresh = "1.1.0" retrofit = "2.9.0" From 907c94096bde56d4117d573763ee293fbc900dc8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 6 Sep 2023 18:31:55 +0000 Subject: [PATCH 20/27] Update dependency androidx.annotation:annotation to v1.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..b30748cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ detekt = "1.23.0" preferences = "1.2.1" media3 = "1.1.1" -androidSupport = "1.6.0" +androidSupport = "1.7.0" materialDesign = "1.9.0" constraintLayout = "2.1.4" multidex = "2.0.1" From 76f16cc9f11fc16e222bad9abee84ab36dae286c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 6 Sep 2023 18:31:57 +0000 Subject: [PATCH 21/27] Update dependency androidx.core:core-ktx to v1.12.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..7d435cd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ gradle = "8.1.1" navigation = "2.7.1" gradlePlugin = "8.1.1" androidxcar = "1.2.0" -androidxcore = "1.10.1" +androidxcore = "1.12.0" ktlint = "0.43.2" ktlintGradle = "11.5.1" detekt = "1.23.0" From 8c7e8a8ae050c7d8ad0de46af179a72df6f063e6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 13 Sep 2023 08:31:46 +0000 Subject: [PATCH 22/27] Update koin to v3.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..4a3e2f46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ retrofit = "2.9.0" ## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24 jackson = "2.13.5" okhttp = "4.11.0" -koin = "3.4.3" +koin = "3.5.0" picasso = "2.8" junit4 = "4.13.2" From f9bafa93da6751e8ef326526ab385c8e24c38eed Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 18 Sep 2023 20:31:51 +0000 Subject: [PATCH 23/27] Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..3ae99cd8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ gradlePlugin = "8.1.1" androidxcar = "1.2.0" androidxcore = "1.10.1" ktlint = "0.43.2" -ktlintGradle = "11.5.1" +ktlintGradle = "11.6.0" detekt = "1.23.0" preferences = "1.2.1" media3 = "1.1.1" From 7a24be4b98dc2aad95d4a93522fc0ecfcea07c5a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 20 Sep 2023 17:31:48 +0000 Subject: [PATCH 24/27] Update navigation to v2.7.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..78c6e66c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # You need to run ./gradlew wrapper after updating the version gradle = "8.1.1" -navigation = "2.7.1" +navigation = "2.7.3" gradlePlugin = "8.1.1" androidxcar = "1.2.0" androidxcore = "1.10.1" From 5755363f2e668a44183187a3429964acfb8c9520 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 29 Sep 2023 09:31:24 +0000 Subject: [PATCH 25/27] Update dependency io.reactivex.rxjava3:rxjava to v3.1.8 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e96393c3..6ed1929f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ robolectric = "4.10.3" timber = "5.0.1" fastScroll = "2.0.1" colorPicker = "2.2.4" -rxJava = "3.1.7" +rxJava = "3.1.8" rxAndroid = "3.0.2" multiType = "4.3.0" From 104df418cc6e6e2e611025e0f6e8046cff7d8063 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sat, 30 Sep 2023 08:01:56 +0000 Subject: [PATCH 26/27] Merge master to dev From e48c823de0aa62fd51c605d87b8547ea2be20ad6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 30 Sep 2023 08:31:34 +0000 Subject: [PATCH 27/27] Update dependency com.android.tools.build:gradle to v8.1.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0560d51f..fb4af1c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "8.1.1" navigation = "2.7.3" -gradlePlugin = "8.1.1" +gradlePlugin = "8.1.2" androidxcar = "1.2.0" androidxcore = "1.12.0" ktlint = "0.43.2"