mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-13 07:57:16 +03:00
Merge branch 'kotlin' into 'develop'
Migrate more files to Kotlin See merge request ultrasonic/ultrasonic!1094
This commit is contained in:
commit
ae97ded344
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user