Merge branch 'kotlin' into 'develop'

Migrate more files to Kotlin

See merge request ultrasonic/ultrasonic!1094
This commit is contained in:
birdbird 2023-08-05 15:10:25 +00:00
commit ae97ded344
17 changed files with 411 additions and 594 deletions

View File

@ -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<MediaPlayerLifecycleSupport> 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.
}
}
}

View File

@ -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; }
}

View File

@ -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();
}
}

View File

@ -1,12 +0,0 @@
package org.moire.ultrasonic.service;
/**
* Abstract class for supplying items to a consumer
* @param <T> The type of the item supplied
*/
public abstract class Supplier<T>
{
public abstract T get();
}

View File

@ -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<ChatMessage>
{
private final Context context;
private final List<ChatMessage> 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> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
public ChatAdapter(Context context, List<ChatMessage> 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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<Genre> 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<Genre> genres)
{
super(context, R.layout.list_item_generic, genres);
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Collection<String> sectionSet = new LinkedHashSet<String>(30);
List<Integer> positionList = new ArrayList<Integer>(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;
}
}

View File

@ -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<Share>
{
private final Context context;
public ShareAdapter(Context context, List<Share> 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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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());
}
}

View File

@ -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<DownloadHandler>()
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<Share>) {
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,

View File

@ -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)
}
}
}

View File

@ -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>(
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.
}
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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<ChatMessage>) :
ArrayAdapter<ChatMessage>(
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
}
}

View File

@ -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<Genre>) :
ArrayAdapter<Genre?>(context, R.layout.list_item_generic, genres), SectionIndexer {
private val layoutInflater: LayoutInflater
// Both arrays are indexed by section ID.
private val sections: Array<Any>
private val positions: Array<Int>
init {
layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val sectionSet: MutableCollection<String> = LinkedHashSet(INITIAL_CAPACITY)
val positionList: MutableList<Int> = 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<Any> {
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
}
}

View File

@ -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<Share>) : ArrayAdapter<Share>(
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
}
}

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="0dp"
a:layout_width="match_parent"
a:layout_height="56dp"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:minHeight="44dip"
a:orientation="vertical"
a:paddingStart="16dip"
a:paddingEnd="16dip"
tools:layout_width="match_parent">
a:paddingEnd="16dip">
<LinearLayout
a:layout_width="fill_parent"