|
|
|
|
using System;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
namespace QuickPlay
|
|
|
|
|
{
|
|
|
|
|
#pragma warning disable CS0659 // This is basically an almost-singleton, which will never be put in any hash table.
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Application (global) configuration data class
|
|
|
|
|
/// </summary>
|
|
|
|
|
[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)
|
|
|
|
|
{
|
|
|
|
|
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>
|
|
|
|
|
/// Configuration of the player.
|
|
|
|
|
///
|
|
|
|
|
/// Contains details about connection, configured songs &c.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
{
|
|
|
|
|
Task<PlayerConfiguration> GetConfigurationAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sealed class HttpConfigurationProvider : IPlayerConfigurationProvider
|
|
|
|
|
{
|
|
|
|
|
readonly string configUrl;
|
|
|
|
|
public HttpConfigurationProvider(string url)
|
|
|
|
|
{
|
|
|
|
|
configUrl = url;
|
|
|
|
|
}
|
|
|
|
|
public async Task<PlayerConfiguration> GetConfigurationAsync()
|
|
|
|
|
{
|
|
|
|
|
var client = new HttpClient() ;
|
|
|
|
|
var resp = await client.GetStreamAsync(configUrl);
|
|
|
|
|
var sr = new StreamReader(resp);
|
|
|
|
|
return PlayerConfiguration.FromFile(sr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sealed class FileConfigurationProvider : IPlayerConfigurationProvider
|
|
|
|
|
{
|
|
|
|
|
StreamReader reader;
|
|
|
|
|
public FileConfigurationProvider(StreamReader reader)
|
|
|
|
|
{
|
|
|
|
|
this.reader = reader;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FileConfigurationProvider(string filename)
|
|
|
|
|
{
|
|
|
|
|
this.reader = new StreamReader(filename);
|
|
|
|
|
}
|
|
|
|
|
public Task<PlayerConfiguration> GetConfigurationAsync()
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(PlayerConfiguration.FromFile(reader));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sealed class DummyConfigurationProvider : IPlayerConfigurationProvider
|
|
|
|
|
{
|
|
|
|
|
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"],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|