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] 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">