Merge branch 'random_but_working'

develop v0.1
LEdoian 3 years ago
commit 1b4f943dc3

Binary file not shown.

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

@ -5,10 +5,13 @@ VisualStudioVersion = 16.0.31112.23
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickPlay", "QuickPlay\QuickPlay.csproj", "{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickPlay", "QuickPlay\QuickPlay.csproj", "{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MpcCore", "..\..\Third-party\mpcCore\src\MpcCore\MpcCore.csproj", "{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release-Stable|Any CPU = Release-Stable|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -17,6 +20,15 @@ Global
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Build.0 = Release|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Build.0 = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Deploy.0 = Release|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Deploy.0 = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.Build.0 = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.Deploy.0 = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.Build.0 = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

@ -1,14 +1,133 @@
using System; using System;
using System.Net.Http;
using System.IO; using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace QuickPlayer namespace QuickPlay
{ {
#pragma warning disable CS0659 // This is basically an almost-singleton, which will never be put in any hash table.
/// <summary> /// <summary>
/// Application (global) configuration data class /// Application (global) configuration data class
/// </summary> /// </summary>
class AppConfiguration [Serializable]
public sealed class AppConfiguration
{ {
// XXX: All the fields need to be checked in overriden Equals method
// Also: sensible defaults should be provided in defaultConfiguration field
public string playerConfigUrl;
// XXX: Any other fields of this model need [NonSerialized] attribute
[NonSerialized]
public static readonly string configFilePath =
Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"appConfig.xml"
);
public static AppConfiguration loadConfiguration()
{
var cfg = loadSavedConfiguration();
if (cfg == null)
{
cfg = defaultConfiguration;
}
return cfg;
}
public static AppConfiguration loadSavedConfiguration()
{
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(AppConfiguration));
AppConfiguration result = null;
try
{
using (var stream = new System.IO.FileStream(configFilePath, System.IO.FileMode.Open))
{
result = (AppConfiguration)serializer.Deserialize(stream);
}
} catch (FileNotFoundException)
{
// This is fine, we will return null anyway.
} catch (InvalidCastException) {
// This is not so much fine, TODO log it.
// Still, we cannot supply config, so treat it as if it does not exist.
}
return result;
}
public void saveConfiguration()
{
// First we save the config
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(AppConfiguration));
using (var stream = new System.IO.FileStream(configFilePath, System.IO.FileMode.Create))
{
serializer.Serialize(stream, this);
}
// Make sure that the configuration is same
var newConfig = AppConfiguration.loadSavedConfiguration();
if (this != newConfig) throw new InvalidDataException("Saved configuration is different from the supplied one.");
}
[NonSerialized]
public static readonly AppConfiguration defaultConfiguration = new AppConfiguration
{
playerConfigUrl = "http://www.ms.mff.cuni.cz/~turinskp/znelky.ini",
};
public async Task<PlayerConfiguration> GetPlayerConfig()
{
IPlayerConfigurationProvider cfgProvider;
if (playerConfigUrl.StartsWith("http:") || playerConfigUrl.StartsWith("https:"))
{
cfgProvider = new HttpConfigurationProvider(playerConfigUrl);
} else if (playerConfigUrl.StartsWith("file://"))
{
string filename = playerConfigUrl.Substring("file://".Length);
var reader = new StreamReader(filename);
cfgProvider = new FileConfigurationProvider(reader);
} else if (playerConfigUrl == "default") {
cfgProvider = new DummyConfigurationProvider();
} else
{
throw new InvalidOperationException("Schema of player config URL not supported");
}
PlayerConfiguration playercfg;
try
{
playercfg = await cfgProvider.GetConfigurationAsync();
}
// FIXME: These are provider-specific exceptions!
catch (Exception e) when (
e is Java.Net.UnknownHostException
|| e is OperationCanceledException
|| e is HttpRequestException
|| e is System.Net.WebException
)
{
var ctx = Android.App.Application.Context;
var t = Android.Widget.Toast.MakeText(ctx, "Could not load config:" + e.Message, Android.Widget.ToastLength.Long);
t.Show();
cfgProvider = new DummyConfigurationProvider();
playercfg = await cfgProvider.GetConfigurationAsync();
}
return playercfg;
}
// We want to compare by values.
// It might actually be a bit more sensible to compare serialized
// versions of the objects, but that seems unneccessarily hard.
public override bool Equals(object obj)
{
var other = (AppConfiguration)obj;
// These fields have to match...
if (this.playerConfigUrl != other.playerConfigUrl) return false;
return true;
}
}
#pragma warning restore CS0659
public enum PlayerType
{
MPD,
} }
/// <summary> /// <summary>
@ -16,27 +135,77 @@ namespace QuickPlayer
/// ///
/// Contains details about connection, configured songs &c. /// Contains details about connection, configured songs &c.
/// </summary> /// </summary>
class PlayerConfiguration public class PlayerConfiguration
{ {
public Dictionary<string, IPlayable> songs = new Dictionary<string, IPlayable>();
public string playerName = "";
public PlayerType playerType;
public string playerConnectionDetails = ""; // This is opaque to this class, the player will make sense of it.
public static PlayerConfiguration FromFile(StreamReader reader) public static PlayerConfiguration FromFile(StreamReader reader)
{ {
return null; // FIXME: Implement var parser = new IniParser(reader);
var ini = parser.Parse();
var result = new PlayerConfiguration();
foreach (var elem in ini)
{
// Two options: either general options, or song descriptions
if (elem.Key == "general")
{
result.playerName = elem.Value["name"];
result.playerType = PlayerType.MPD; //FIXME: not always.
result.playerConnectionDetails = elem.Value["connection"];
} else
{
var song = new Song(elem.Key, elem.Value);
result.songs[elem.Key] = song;
}
}
return result;
}
public IPlayer GetPlayer()
{
IPlayer result;
switch (playerType)
{
case PlayerType.MPD:
result = new MpdPlayer(this);
break;
default:
throw new InvalidOperationException("Cannot happen: Player had no type.");
}
return result;
} }
} }
interface IPlayerConfigurationProvider interface IPlayerConfigurationProvider
{ {
PlayerConfiguration GetConfiguration(); Task<PlayerConfiguration> GetConfigurationAsync();
} }
sealed class NetworkConfigurationProvider : IPlayerConfigurationProvider sealed class HttpConfigurationProvider : IPlayerConfigurationProvider
{ {
readonly string configUrl;
public HttpConfigurationProvider(string url)
{
configUrl = url;
}
public async Task<PlayerConfiguration> GetConfigurationAsync()
{
return await Task.Run(async () =>
{
var client = new HttpClient();
var resp = await client.GetStreamAsync(configUrl);
var sr = new StreamReader(resp);
return PlayerConfiguration.FromFile(sr);
});
}
} }
sealed class FileConfigurationProvider : IPlayerConfigurationProvider sealed class FileConfigurationProvider : IPlayerConfigurationProvider
{ {
public readonly StreamReader reader; StreamReader reader;
public FileConfigurationProvider(StreamReader reader) public FileConfigurationProvider(StreamReader reader)
{ {
this.reader = reader; this.reader = reader;
@ -46,14 +215,36 @@ namespace QuickPlayer
{ {
this.reader = new StreamReader(filename); this.reader = new StreamReader(filename);
} }
public PlayerConfiguration GetConfiguration() public Task<PlayerConfiguration> GetConfigurationAsync()
{ {
return PlayerConfiguration.FromFile(reader); return Task.FromResult(PlayerConfiguration.FromFile(reader));
} }
} }
sealed class DummyConfigurationProvider : IPlayerConfigurationProvider sealed class DummyConfigurationProvider : IPlayerConfigurationProvider
{ {
public PlayerConfiguration GetConfiguration() => null; public Task<PlayerConfiguration> GetConfigurationAsync()
{
PlayerConfiguration cfg = new PlayerConfiguration();
cfg.playerType = PlayerType.MPD;
cfg.playerName = "Dummy player";
cfg.playerConnectionDetails = "127.0.0.1";
return Task.FromResult(cfg);
}
}
class Song: IPlayable
{
public string Identifier { get; private set; }
public PlayableMetadata Metadata { get; private set; }
public Song(string id, Dictionary<string, string> data) {
Identifier = id;
Metadata = new PlayableMetadata
{
usualPlayingTime = TimeSpan.Parse(data["time"]),
filePath = data["path"],
};
}
} }
} }

@ -0,0 +1,67 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace QuickPlay
{
/// <summary>
/// This class implements (yet another) INI-style parser.
///
/// INI specification: everything is in sections, comments using both '#'
/// or ';', space around '=' is trimmed, last value assigned overrides
/// previous assignments, sections may not repeat. Section names may only
/// contain reasonable characters (at least [a-zA-Z0-9. ], not really
/// enforced). Everything is case-sensitive.
///
/// Both CRLF and just LF should be supported as line ends.
/// </summary>
class IniParser
{
StreamReader reader;
string currentSection = null;
public IniParser(StreamReader sr)
{
reader = sr;
}
// The return type is dictionary by sections, which in turn holds the section key-value dictionary
// Maybe the code is a bit more Pythonic than it should be...
public Dictionary<string, Dictionary<string, string>> Parse()
{
Dictionary<string, Dictionary<string, string>> result = new Dictionary<string, Dictionary<string, string>>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
line = StripComments(line).Trim();
if (line.Length == 0) continue;
if (line.StartsWith('['))
{
// This does not really do the right thing, but for QuickPlay's use it is good enough.
currentSection = line.Split(new char[] { '[', ']' })[1];
if (result.ContainsKey(currentSection)) throw new InvalidOperationException("Multiple sections with same name");
result[currentSection] = new Dictionary<string, string>();
}
else
{
// Other lines may only be assignments
int equalSignPosition = line.IndexOf('=');
string key = line.Substring(0, equalSignPosition).Trim();
string value = line.Substring(equalSignPosition + 1).Trim();
result[currentSection][key] = value;
}
}
return result;
}
string StripComments(string line)
{
return line.Split(new char[] { ';', '#' })[0];
}
}
}

@ -1,44 +1,65 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
// MPD client abstractions and simplifications // MPD client abstractions and simplifications
namespace QuickPlayer namespace QuickPlay
{ {
/// <summary> /// <summary>
/// Simplified abstraction of possible players. Only methods needed are included. /// Simplified abstraction of possible players. Only methods needed are included.
/// ///
/// That means that the interface may be extended in the future, which is sad. /// That means that the interface may be extended in the future, which is sad.
/// </summary> /// </summary>
interface IPlayer public interface IPlayer
{ {
void Play(string identifier); public enum PlayerStatus
{
Disconnected, Playing, Stopped
}
Dictionary<string, IPlayable> Songs { get; }
string PlayerName { get; }
Task Play(IPlayable playable);
float CurrentProgress { get; } float CurrentProgress { get; }
bool IsReady { get; } PlayerStatus Status { get; }
void SetReasonableOptions(); //TODO Task SetReasonableOptions();
/// <summary>
/// Attach to the real player.
///
/// Since this operation can be asynchronous, we cannot put it in th
/// constructor. And this allows for some tweaks before connecting.
/// </summary>
/// <returns></returns>
Task ConnectAsync();
} }
/// <summary> /// <summary>
/// A simple dataclass to hold auxiliary data of the Playable objects. /// A simple dataclass to hold auxiliary data of the Playable objects.
/// </summary> /// </summary>
// This is not really an interface, but since it is a dataclass, I treat it // This is not really an interface, but since it is a dataclass, I treat it
// more as a part of interface, so it belongs to this file. // more as a part of interface, so it belongs to this file.
class PlayableMetadata
// Also, in order not to need boilerplate code, this should be semantically
// treated as immutable even though it's not.
public class PlayableMetadata
{ {
public readonly TimeSpan usualPlayingTime; public TimeSpan usualPlayingTime;
public string filePath;
} }
interface IPlayable public interface IPlayable
{ {
void Play();
string Identifier { get; } string Identifier { get; }
PlayableMetadata Metadata { get; } PlayableMetadata Metadata { get; }
} }
interface ILayout interface ILayoutStrategy
{ {
// TODO List<IPlayable> LayOut(ICollection<IPlayable> playables);
} }
interface ILayoutStrategy
public class CannotConnectException: Exception
{ {
ILayout LayOut(ICollection<IPlayable> playables); public CannotConnectException(string msg, Exception e) : base(msg, e) { }
public CannotConnectException(string msg) : base(msg) { }
public CannotConnectException() : base() { }
} }
} }

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace QuickPlay
{
class LexicographicLayoutStrategy : ILayoutStrategy
{
public List<IPlayable> LayOut(ICollection<IPlayable> playables)
{
List<IPlayable> list = new List<IPlayable>(playables);
list.Sort((IPlayable x, IPlayable y) => x.Identifier.CompareTo(y.Identifier));
return list;
}
}
}

@ -1,60 +1,139 @@
using System; using System;
using Android.App; using Android.App;
using Android.OS; using Android.OS;
using Android.Runtime;
using Android.Views; using Android.Views;
using AndroidX.AppCompat.Widget; using Android.Widget;
using AndroidX.AppCompat.App; using Android.Support.V7.App;
using Google.Android.Material.FloatingActionButton; using Toolbar = Android.Support.V7.Widget.Toolbar;
using Google.Android.Material.Snackbar; using GridLayoutManager = Android.Support.V7.Widget.GridLayoutManager;
namespace QuickPlay namespace QuickPlay
{ {
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)] [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity public class MainActivity : AppCompatActivity
{ {
protected override void OnCreate(Bundle savedInstanceState) private AppConfiguration appConfig;
private Android.Support.V7.Widget.RecyclerView recyclerView;
private IPlayer currentPlayer;
protected override async void OnCreate(Bundle savedInstanceState)
{ {
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
// App initialization
appConfig = AppConfiguration.loadConfiguration();
var playerConfig = await appConfig.GetPlayerConfig();
currentPlayer = playerConfig.GetPlayer();
try
{
await currentPlayer.ConnectAsync();
} catch (CannotConnectException e)
{
//TODO: View a toast with details and change some colors?
var t = Toast.MakeText(this, e.Message + ": " + e.InnerException.Message ?? "", ToastLength.Long);
t.Show();
}
// UI initialization
SetContentView(Resource.Layout.activity_main); SetContentView(Resource.Layout.activity_main);
Toolbar toolbar = FindViewById<Toolbar>(Resource.Id.toolbar); // UI Toolbar initialization
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar); SetSupportActionBar(toolbar);
SupportActionBar.Title = "My Toolbar";
// Hide the play bar by default
var bar = FindViewById(Resource.Id.currentSongBar);
bar.Visibility = ViewStates.Invisible;
FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource.Id.fab); // Initialize the RecyclerView
fab.Click += FabOnClick; // Since this is rather complicated, it is in a separate method
InitializeRecyclerView();
// FIXME: This should be in OnResume...
// Refresh player info
OnPlayerUpdate();
}
private void InitializeRecyclerView()
{
recyclerView = FindViewById<Android.Support.V7.Widget.RecyclerView>(Resource.Id.recyclerView1);
var layoutStrategy = new LexicographicLayoutStrategy();
var adapter = new SongRecyclerAdapter(currentPlayer, layoutStrategy);
adapter.SongClick += OnSongClick;
recyclerView.SetAdapter(adapter);
var layoutManager = new GridLayoutManager(this, 2, GridLayoutManager.Vertical, false);
recyclerView.SetLayoutManager(layoutManager);
} }
public override bool OnCreateOptionsMenu(IMenu menu) public override bool OnCreateOptionsMenu(IMenu menu)
{ {
MenuInflater.Inflate(Resource.Menu.menu_main, menu); MenuInflater.Inflate(Resource.Menu.menu_main, menu);
return true; return base.OnCreateOptionsMenu(menu);
} }
public override bool OnOptionsItemSelected(IMenuItem item) public override bool OnOptionsItemSelected(IMenuItem item)
{ {
int id = item.ItemId; if (item.ItemId == Resource.Id.action_settings)
if (id == Resource.Id.action_settings)
{ {
return true; // Show the play bar
var bar = FindViewById(Resource.Id.currentSongBar);
bar.Visibility = ViewStates.Visible;
} }
if (item.ItemId == Resource.Id.action_edit)
{
// Hide the play bar
var bar = FindViewById(Resource.Id.currentSongBar);
bar.Visibility = ViewStates.Invisible;
}
return base.OnOptionsItemSelected(item); return base.OnOptionsItemSelected(item);
} }
private void FabOnClick(object sender, EventArgs eventArgs) /// <summary>
/// A callback for all player updates.
///
/// The update should be easy on resources, so no need to have separate
/// partial updates. This simply performs full update of the activity
/// state and views.
/// </summary>
public void OnPlayerUpdate()
{ {
View view = (View) sender; TextView playerName = FindViewById<TextView>(Resource.Id.playerNameText);
Snackbar.Make(view, "Replace with your own action", Snackbar.LengthLong) playerName.Text = currentPlayer.PlayerName;
.SetAction("Action", (View.IOnClickListener)null).Show(); var bar = FindViewById(Resource.Id.currentSongBar);
switch (currentPlayer.Status)
{
case IPlayer.PlayerStatus.Disconnected:
Toolbar tb = FindViewById<Toolbar>(Resource.Id.toolbar);
tb.SetBackgroundColor(Android.Graphics.Color.Red);
bar.Visibility = ViewStates.Invisible;
break;
case IPlayer.PlayerStatus.Playing:
ResetToolbarColor();
// Show progress bar
bar.Visibility = ViewStates.Visible;
break;
case IPlayer.PlayerStatus.Stopped:
ResetToolbarColor();
bar.Visibility = ViewStates.Invisible;
break;
}
} }
private void ResetToolbarColor()
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{ {
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); Toolbar tb = FindViewById<Toolbar>(Resource.Id.toolbar);
tb.SetBackgroundColor(
new Android.Graphics.Color(
Android.Support.V4.Content.ContextCompat.GetColor(this, Resource.Color.colorPrimary)
)
);
}
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); public async void OnSongClick(object sender, IPlayable song)
{
await currentPlayer.Play(song);
} }
} }
} }

@ -0,0 +1,32 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
namespace QuickPlay
{
[Service]
class MpdMonitorService : Service
{
public override void OnCreate()
{
base.OnCreate();
// TODO: Create the watching thread
}
public override IBinder OnBind(Intent intent)
{
throw new NotImplementedException();
}
public override bool OnUnbind(Intent intent)
{
return base.OnUnbind(intent);
}
public override void OnDestroy()
{
base.OnDestroy();
}
}
}

@ -0,0 +1,68 @@
using Android.Content;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MpcCore;
using System.Net.Sockets;
namespace QuickPlay
{
class MpdPlayer: IPlayer
{
MpcCoreClient mpd;
// MpcCore uses strings, so be it
string mpdIP, mpdPort;
public IPlayer.PlayerStatus Status { get; private set; }
public Dictionary<string, IPlayable> Songs { get; private set; }
public string PlayerName { get; private set; }
public float CurrentProgress { get {
throw new NotImplementedException();
} }
public async Task Play(IPlayable song)
{
await mpd.SendAsync(new MpcCore.Commands.Player.Stop());
await SetReasonableOptions();
await mpd.SendAsync(new MpcCore.Commands.Queue.ClearQueue());
await mpd.SendAsync(new MpcCore.Commands.Queue.AddToQueue(song.Metadata.filePath));
await mpd.SendAsync(new MpcCore.Commands.Player.Play());
}
public async Task SetReasonableOptions()
{
await mpd.SendAsync(new MpcCore.Commands.Options.SetConsume(true));
await mpd.SendAsync(new MpcCore.Commands.Options.SetRandom(false));
await mpd.SendAsync(new MpcCore.Commands.Options.SetRepeat(false));
await mpd.SendAsync(new MpcCore.Commands.Options.SetSingle(false));
}
public MpdPlayer(PlayerConfiguration cfg)
{
// Populate known fields/properties
Songs = cfg.songs;
PlayerName = cfg.playerName;
// NOTE: We separate the port by '@', since ':' could be part of IPv6 and we do not want to complicate parsing.
var connDetails = cfg.playerConnectionDetails.Split('@');
if (connDetails.Length > 2) throw new InvalidOperationException("Bad connection details");
mpdIP = connDetails[0];
mpdPort = connDetails.Length >=2 ? connDetails[1] : "6600"; // XXX: Unneccessary default here...
// Connecting and monitoring remote player is done in ConnectAsync.
// Therefore, we start disconnected
Status = IPlayer.PlayerStatus.Disconnected;
}
public async Task ConnectAsync()
{
// Create a persistent connection
var conn = new MpcCoreConnection(mpdIP, mpdPort);
mpd = new MpcCoreClient(conn);
try
{
await mpd.ConnectAsync();
} catch (SocketException e)
{
throw new CannotConnectException("MPD connect failed", e);
}
// Start the monitoring service
var ctx = Android.App.Application.Context;
var intent = new Intent(ctx, typeof(MpdMonitorService));
}
}
}

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.quickplay" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="0.1" package="cz.ledoian.android.quickplay" android:installLocation="auto">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="19" /> <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"></application> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest> </manifest>

@ -15,6 +15,12 @@ using Android.App;
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
#if DEBUG
[assembly: Application(Debuggable=true)]
#else
[assembly: Application(Debuggable = false)]
#endif
// Version information for an assembly consists of the following four values: // Version information for an assembly consists of the following four values:
// //

@ -36,14 +36,17 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<AndroidUseSharedRuntime>True</AndroidUseSharedRuntime> <AndroidUseSharedRuntime>True</AndroidUseSharedRuntime>
<AndroidLinkMode>None</AndroidLinkMode> <AndroidLinkMode>None</AndroidLinkMode>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>false</EmbedAssembliesIntoApk>
<AotAssemblies>false</AotAssemblies> <AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM> <EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies> <BundleAssemblies>false</BundleAssemblies>
<AndroidKeyStore>false</AndroidKeyStore>
<AndroidUseAapt2>false</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>True</DebugSymbols> <DebugSymbols>false</DebugSymbols>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<Optimize>True</Optimize> <Optimize>True</Optimize>
<OutputPath>bin\Release\</OutputPath> <OutputPath>bin\Release\</OutputPath>
@ -52,11 +55,20 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols> <AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>False</AndroidUseSharedRuntime> <AndroidUseSharedRuntime>False</AndroidUseSharedRuntime>
<AndroidLinkMode>SdkOnly</AndroidLinkMode> <AndroidLinkMode>Full</AndroidLinkMode>
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
<AndroidLinkTool>r8</AndroidLinkTool>
<MandroidI18n />
<AndroidSupportedAbis />
<AndroidDexTool>d8</AndroidDexTool>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="Mono.Android" /> <Reference Include="Mono.Android" />
@ -64,9 +76,16 @@
<Reference Include="System.Numerics.Vectors" /> <Reference Include="System.Numerics.Vectors" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Configuration.cs" />
<Compile Include="IniParser.cs" />
<Compile Include="Interfaces.cs" />
<Compile Include="LayoutStrategies.cs" />
<Compile Include="MpdMonitorService.cs" />
<Compile Include="MainActivity.cs" /> <Compile Include="MainActivity.cs" />
<Compile Include="MpdPlayer.cs" />
<Compile Include="Resources\Resource.designer.cs" /> <Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SongRecycler.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Resources\AboutResources.txt" /> <None Include="Resources\AboutResources.txt" />
@ -77,9 +96,6 @@
<AndroidResource Include="Resources\layout\activity_main.xml"> <AndroidResource Include="Resources\layout\activity_main.xml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</AndroidResource> </AndroidResource>
<AndroidResource Include="Resources\layout\content_main.xml">
<SubType>Designer</SubType>
</AndroidResource>
<AndroidResource Include="Resources\values\colors.xml" /> <AndroidResource Include="Resources\values\colors.xml" />
<AndroidResource Include="Resources\values\dimens.xml" /> <AndroidResource Include="Resources\values\dimens.xml" />
<AndroidResource Include="Resources\values\ic_launcher_background.xml" /> <AndroidResource Include="Resources\values\ic_launcher_background.xml" />
@ -105,15 +121,119 @@
<AndroidResource Include="Resources\mipmap-xxxhdpi\ic_launcher_round.png" /> <AndroidResource Include="Resources\mipmap-xxxhdpi\ic_launcher_round.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources\drawable\" /> <AndroidResource Include="Resources\drawable-anydpi\material_settings.xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.2.0.5" /> <PackageReference Include="Xamarin.Android.Support.Design">
<PackageReference Include="Xamarin.AndroidX.Arch.Core.Runtime"> <Version>28.0.0.3</Version>
<Version>2.1.0.8</Version> </PackageReference>
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat">
<Version>28.0.0.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.0.0.1" /> <PackageReference Include="Xamarin.Android.Support.Vector.Drawable">
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" /> <Version>28.0.0.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Third-party\mpcCore\src\MpcCore\MpcCore.csproj">
<Project>{d0a5ad05-b98c-45e6-b61d-4700f7aa72cf}</Project>
<Name>MpcCore</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\material_settings.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-v21\material_settings.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-v24\material_settings.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-mdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-hdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xxhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xxxhdpi\material_settings.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-anydpi-v26\material_settings.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\material_edit.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-anydpi\material_edit.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-v21\material_edit.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-v24\material_edit.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-mdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-hdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xxhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-xxxhdpi\material_edit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\mipmap-anydpi-v26\material_edit.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\commonTopBar.axml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\songLayout.xml">
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" /></vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" /></vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" /></vector>

@ -1,32 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="fill_parent"
android:layout_height="match_parent"> android:layout_width="fill_parent"
android:orientation="vertical"
>
<include
layout="@layout/commonTopBar"
android:id="@+id/commonTopBar"
/>
<com.google.android.material.appbar.AppBarLayout <!-- The content itself -->
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="fill_parent"
android:theme="@style/AppTheme.AppBarOverlay"> android:layout_below="@id/commonTopBar"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <android.support.v7.widget.RecyclerView
android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="match_parent"
android:background="?attr/colorPrimary" android:id="@+id/recyclerView1"
app:popupTheme="@style/AppTheme.PopupOverlay" /> />
</com.google.android.material.appbar.AppBarLayout> <RelativeLayout
android:layout_width="match_parent"
<include layout="@layout/content_main" /> android:layout_height="wrap_content"
android:gravity="bottom|end"
<com.google.android.material.floatingactionbutton.FloatingActionButton android:layout_marginLeft="5pt"
android:id="@+id/fab" android:layout_marginRight="5pt"
android:layout_width="wrap_content" android:layout_marginBottom="5pt"
android:layout_height="wrap_content" android:id="@+id/currentSongBar"
android:layout_gravity="bottom|end" >
android:layout_margin="@dimen/fab_margin" <android.support.design.widget.FloatingActionButton
app:srcCompat="@android:drawable/ic_dialog_email" /> android:layout_width="@dimen/design_fab_size_mini"
android:layout_height="@dimen/design_fab_size_mini"
android:id="@+id/currentSongInteractButton"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="0pt"
app:elevation="0dp"
/>
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/currentSongProgress"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:layout_toLeftOf="@id/currentSongInteractButton"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/currentSong"
android:text="Hello World!"
android:layout_alignParentLeft="true"
android:textAlignment="center"
android:layout_toLeftOf="@id/currentSongInteractButton"
android:layout_above="@id/currentSongProgress"
/>
<!-- Alternative: SeekBar -->
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
</LinearLayout>

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
>
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:id="@+id/titleText">
<TextView
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/appTitleText" />
<TextView
android:text="@string/player_name"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/playerNameText" />
</LinearLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello World!" />
</RelativeLayout>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- I am going to assume that the match_parent width respects the two-column layout. -->
<!-- The CardView just adds some beauty -->
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="100dp"
android:layout_gravity="center"
app:cardElevation="4dp"
app:cardUseCompatPadding="true"
app:cardCornerRadius="5dp"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center"
android:text="Song Name"
android:id="@+id/songName"
/>
</android.support.v7.widget.CardView>
</FrameLayout>

@ -5,5 +5,12 @@
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"
android:title="@string/action_settings" android:title="@string/action_settings"
app:showAsAction="never" /> android:icon="@mipmap/material_settings"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_edit"
android:orderInCategory="200"
android:title="@string/action_edit"
android:icon="@mipmap/material_edit"
app:showAsAction="ifRoom" />
</menu> </menu>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /></vector>

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="#FFFFFFFF" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" /></vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -1,4 +1,7 @@
<resources> <resources>
<string name="app_name">QuickPlay</string> <string name="app_name">QuickPlay</string>
<string name="player_name">Not Connected!</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_edit">Edit</string>
</resources> </resources>

@ -1,11 +1,13 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style> </style>
<style name="AppTheme.NoActionBar"> <style name="AppTheme.NoActionBar">
@ -13,8 +15,8 @@
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
</style> </style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" /> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" /> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources> </resources>

@ -0,0 +1,57 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Support.V7.Widget;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
namespace QuickPlay
{
class SongRecyclerAdapter : Android.Support.V7.Widget.RecyclerView.Adapter
{
public event EventHandler<IPlayable> SongClick;
IPlayer player;
ILayoutStrategy layoutStrategy;
public SongRecyclerAdapter(IPlayer player, ILayoutStrategy layoutStrategy)
{
this.player = player;
this.layoutStrategy = layoutStrategy;
}
public override int ItemCount
{
get
{
return player.Songs.Count;
}
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
// I admit I have little idea what I am doing.
View itemView = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.songLayout, parent, false);
return new SongRecyclerViewHolder(itemView, OnItemClick);
}
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
SongRecyclerViewHolder vh = (SongRecyclerViewHolder)holder;
List<IPlayable> layout = layoutStrategy.LayOut(player.Songs.Values);
vh.SongName.Text = layout[position].Identifier;
}
void OnItemClick(int position)
{
IPlayable playable = layoutStrategy.LayOut(player.Songs.Values)[position];
if (SongClick != null) SongClick.Invoke(this, playable);
}
}
class SongRecyclerViewHolder : Android.Support.V7.Widget.RecyclerView.ViewHolder
{
public TextView SongName { get; private set; }
public SongRecyclerViewHolder(View itemView, Action<int> callback) : base(itemView)
{
SongName = itemView.FindViewById<TextView>(Resource.Id.songName);
itemView.Click += (sender, e) => callback(base.LayoutPosition);
}
}
}

@ -0,0 +1,117 @@
QuickPlay
===
A simple Xamarin.Android app to allow fast playing of songs on remote players
like MPD. Supports all Android versions since KitKat (to repurpose an old tablet).
Setup of additional services
===
Since QuickPlay is only a frontend to MPD running somewhere, and the location
has to be specified in the player configuration (see below).
Currently, only unauthenticated MPD connections specified by IP address are supported.
Usage
===
First, make sure that you have the player configuration INI file. Put that on
the magic URL: <http://www.ms.mff.cuni.cz/~turinskp/znelky.ini>. Now run the
app, your songs will appear and you can play them.
An example commented INI file is provided: `playerconfig.ini`. This is what you
would see if you use that:
![UI example](./uiexample.png)
Architecture Overview
===
The app comprises of three main parts:
- Configuration
- Player -- the backend
- Layout -- the frontend
Configuration
---
The configuration is split into two parts: Applocation configuration and player
configuration.
Application configuration is local to the device and describes application
behaviour and where to obtain the player configuration. It
is just a XML-serializable class (for simplicity).
Player configuration is stored on an HTTP server or local filesystem. It is an
INI-like file designed to be written by hand. Since INI is not a well-specified
format, the specific format used in QuickPlay is described in `IniParser.cs`
file. (It was faster to implement own format than to find a usable one on
NuGet.) For use as a player configuration, the section `[general]` has to be
specified with connection details, other sections directly describe individual
songs.
The motivation for the split is to make player configuration easily shareable,
e.g. by a QR code pointing to the config.
Currently, there is no application configuration and the player configuration
is always downloaded from <http://www.ms.mff.cuni.cz/~turinskp/znelky.ini>,
Players
---
QuickPlay is designed to support other players, not just MPD. Therefore, a
`Player` is just an interface that can play Playables and export some trivial
state.
A `Playable` is an abstract object that can be played. Usually it is a song
(description thereof), but there is a possibility it could be for example a
playlist. (In the extreme case that someone would like to play chess instead of
songs, the individual chess pieces would be playables.)
In order to allow players to show current state, `MainActivity` has method
`OnPlayerUpdate`, which re-fetches player state and displays it.
It is expected that the players will maintain a service to monitor the remote
state. It is not required though, it is really implementation detail of the
client. (`MainActivity.OnPlayerUpdate` still has to be run on UI thread.)
### MpdPlayer details
The MpdPlayer is just a thin wrapper around [slightly
patched](https://github.com/LEdoian/mpcCore)
[mpcCore](https://github.com/svanelten/mpcCore) library. It currently only
sends commands to the MPD, but extension by service to monitor it is ready
(`MpdMonitorService.cs`).
Frontend
---
Frontend is just a bunch of boilerplate code to power the RecyclerView behind
it. The only "original" part are layout strategies -- objects that determine in
what order the playables will be shown, based on their metadata.
At the moment, the only layout strategy implemented is lexicographic sorting by
the shown name.
Planned Features
====
- [ ] Working application configuration
- [ ] Player configuration sharing via QR codes
- [ ] Showing current song progress
- [ ] Possibility to stop current song
- [ ] MPD player monitoring service
- [ ] Time-based layout strategy
Bugs
---
- [ ] The MpcCore client seems to disconnect after some time for no apparent reason.
The far future
---
- [ ] Editing the player config right on the device (with INI file export)
- [ ] Tests. (The code should be testable, but no tests has been written yet.)

@ -0,0 +1,43 @@
# This is a player configuration example for QuickPlay.
# Everything after either ';' or '#' is ignored.
# Spaces around '=' and at both ends of each line are trimmed.
# Everything is case-sensitive and UTF-8.
# Multiple sections are forbidden, so are same keys in same section.
# (Really, this is just an encoding of a Dictionary<string, Dictionary<string, string>>)
# Section 'general' is special, it declares general config (well...) of the
# player
[general]
# Name is shown in the app to aid distinguishing between multiple possible players
name = Example client
# Type will determine what kind of player should be used. Currently it is not used.
# More generally, keys that are not used are ignored. They are still parsed,
# though, and should be considered reserved by QuickPlay (i.e. not used arbitrarily).
type = MPD
# Connection specifies where to connect. Usually it is just IP address, but if
# you want to specify port, use notation ip.ad.dr.ess@port (i.e. separate using '@').
connection = 198.51.100.3
# All other sections represent individual songs / playables.
# The section name is the name the song will be visible as.
[Breakfast]
# Expected time of playing. Currently unused, will be used for sorting.
# Allowed format: HH:MM.
time = 15:00
# Path, under which MPD can find the song. Can contain even strange characters,
# but cannot start with whitespace due to trimming. (Frankly, if your
# filenames start with whitespace, you have much bigger problems.)
path = Unsorted/Kevin_MacLeod/Polkas/Snare Bounce Polka.mp3
# Create as many playables as you wish. Remember, no repeating of section names.
[Lunch]
time = 16:00
path = Classical/MacLeod K. - Dance of the Sugar Plum Fairy (P.I.Tchaikovsky).mp3
# Even section names are UTF-8, so we can use non-plain-ASCII characters.
[Café]
time = 22:00
path = Irish/Fiddles McGinty.mp3
# All the mentioned songs are from Kevin MacLeod under CC BY 4.0 licence. Not
# that you can hear them, but the attribution has to be somewhere :-)

@ -0,0 +1,13 @@
jako zápočtový program do C# (NPRG035) bych chtěl dělat aplikaci pro
Android na rychlé spouštění skladeb pomocí Music Player Daemonu (MPD).
Použili bychom to v rámci korespondenčního semináře M&M, kde tento
přehrávač používáme jako "znělky" ke svolání účastníků soustředění na
program.
Program by tedy sestával z tlačítek pro různé znělky, při jejichž
stisknutí by se daná znělka spustila. Tato tlačítka by byla popsána v
nějakém konfiguračním souboru (asi INI), který by si uživatelé mohli
stáhnout a nahrát.
Očekávám, že bych to psal v Xamarinu s použitím existujících bindingů
pro ovládáni MPD. Chtěl bych si vyzkoušet psaní mobilních aplikací.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Loading…
Cancel
Save