Compare commits

..

26 Commits
v0.1 ... master

Author SHA1 Message Date
LEdoian 36952159b7 Add make option for launching 3 years ago
LEdoian 9059b4fb33 Make the build smaller by using Release configuration 3 years ago
LEdoian 2c45d06842 Add `cleandeploy` target to Makefile 3 years ago
LEdoian ba8c84b7b6 Fix crash
Apparently, `override` was not the correct keyword :-)
3 years ago
LEdoian 9d9227b4ed Add a Makefile 3 years ago
LEdoian 8a1fc15f25 Fix more warnings 3 years ago
LEdoian d4f9eb2ad4 Fix a few warnings 3 years ago
LEdoian 5470a5bbf9 Really add the submodule
Turns out you cannot add the submodule to a patch file.
3 years ago
LEdoian c32cab9ee0 Fix "time" to only contain reasonable values
When a song longer than 24 minutes is in the playlist, the time
attribute of the INI is not parseable, causing a crash of the app.
3 years ago
LEdoian 6372b3da78 Fix bugs in the autogenerating script 3 years ago
LEdoian 8df331fa94 Fix permissions 3 years ago
LEdoian d340e8fc35 Add automatic INI generator 3 years ago
LEdoian 0314ef1389 Add notes from setting up toolchain
I somehow managed to get it working in a VM, so here are some notes…
3 years ago
LEdoian ddcc7e12b8 Add MpcCore as submodule 3 years ago
LEdoian 43fa7bf7b5 Add email to Readme
The email is not seen anywhere on gitea, so it needs to be there explicitly.
3 years ago
LEdoian bd416baf2d Add info about contributing 3 years ago
LEdoian 4ea9544d6e Merge branch 'develop' 3 years ago
LEdoian aaa8c606f8 Cut release 0.2 3 years ago
LEdoian df7b8a2ea2 Use dummy player when the URL is not valid 3 years ago
LEdoian a3d73d9af9 Update README 3 years ago
LEdoian 688ffc1a12 Implement QR code scanning 3 years ago
LEdoian f2ec8808ab Add presentation of the project 3 years ago
LEdoian a9717e14b8 Update Readme 3 years ago
LEdoian 637e076d20 Implement alternative player config URLs 3 years ago
LEdoian 0f074607fb Implement OnResume 3 years ago
LEdoian 3859437e7f Make sure that we are connected when sending commands to MPD
There was a bug that prevented using the app after the first minute,
since MPD shut down the connection and nobody reconnected.
3 years ago

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "MpcCore"]
path = MpcCore
url = https://github.com/LEdoian/MpcCore.git

@ -0,0 +1,20 @@
CONF?=/p:Configuration=Release
default: cleanbuild
build:
nuget restore
cd QuickPlay && msbuild /t:Build ${CONF}
deploy: build
cd QuickPlay && msbuild /t:Install ${CONF}
clean:
git clean -fxd
git submodule foreach --recursive git clean -fxd
cleanbuild: clean build
cleandeploy: clean deploy
launch:
adb shell monkey -p cz.ledoian.android.quickplay 1

@ -0,0 +1 @@
Subproject commit 50298b8ca8a2b2256e5af618b8d3ef878c636a7e

@ -5,7 +5,11 @@ 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MpcCore", "MpcCore", "{43A22188-E0FB-4486-9623-10D8A1531AAF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{242948C1-93ED-4D06-9238-B0634318B449}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MpcCore", "MpcCore\src\MpcCore\MpcCore.csproj", "{4E00B1E4-3141-4564-A333-2E8B5F3B0141}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -23,12 +27,12 @@ Global
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.Deploy.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 {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.Build.0 = Release|Any CPU {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Release|Any CPU.Build.0 = Release|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU
{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.Build.0 = Release|Any CPU {4E00B1E4-3141-4564-A333-2E8B5F3B0141}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -36,4 +40,8 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36EE9F92-9FC0-4EEE-A3AA-9EE43A4E2B86} SolutionGuid = {36EE9F92-9FC0-4EEE-A3AA-9EE43A4E2B86}
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{242948C1-93ED-4D06-9238-B0634318B449} = {43A22188-E0FB-4486-9623-10D8A1531AAF}
{4E00B1E4-3141-4564-A333-2E8B5F3B0141} = {242948C1-93ED-4D06-9238-B0634318B449}
EndGlobalSection
EndGlobal EndGlobal

@ -65,7 +65,8 @@ namespace QuickPlay
// Make sure that the configuration is same // Make sure that the configuration is same
var newConfig = AppConfiguration.loadSavedConfiguration(); var newConfig = AppConfiguration.loadSavedConfiguration();
if (this != newConfig) throw new InvalidDataException("Saved configuration is different from the supplied one."); // FIXME: Broken? Throws falsely.
//if (this != newConfig) throw new InvalidDataException("Saved configuration is different from the supplied one.");
} }
[NonSerialized] [NonSerialized]
@ -84,11 +85,12 @@ namespace QuickPlay
string filename = playerConfigUrl.Substring("file://".Length); string filename = playerConfigUrl.Substring("file://".Length);
var reader = new StreamReader(filename); var reader = new StreamReader(filename);
cfgProvider = new FileConfigurationProvider(reader); cfgProvider = new FileConfigurationProvider(reader);
} else if (playerConfigUrl == "default") { } else if (playerConfigUrl == "dummy") {
cfgProvider = new DummyConfigurationProvider(); cfgProvider = new DummyConfigurationProvider();
} else } else
{ {
throw new InvalidOperationException("Schema of player config URL not supported"); Android.Widget.Toast.MakeText(Android.App.Application.Context, "Schema of player config URL not supported, using dummy", Android.Widget.ToastLength.Short).Show();
cfgProvider = new DummyConfigurationProvider();
} }
PlayerConfiguration playercfg; PlayerConfiguration playercfg;
try try

@ -3,9 +3,11 @@ using Android.App;
using Android.OS; using Android.OS;
using Android.Views; using Android.Views;
using Android.Widget; using Android.Widget;
using Android.Content;
using Android.Support.V7.App; using Android.Support.V7.App;
using Toolbar = Android.Support.V7.Widget.Toolbar; using Toolbar = Android.Support.V7.Widget.Toolbar;
using GridLayoutManager = Android.Support.V7.Widget.GridLayoutManager; using GridLayoutManager = Android.Support.V7.Widget.GridLayoutManager;
using Android.Runtime;
namespace QuickPlay namespace QuickPlay
{ {
@ -25,15 +27,6 @@ namespace QuickPlay
appConfig = AppConfiguration.loadConfiguration(); appConfig = AppConfiguration.loadConfiguration();
var playerConfig = await appConfig.GetPlayerConfig(); var playerConfig = await appConfig.GetPlayerConfig();
currentPlayer = playerConfig.GetPlayer(); 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 // UI initialization
SetContentView(Resource.Layout.activity_main); SetContentView(Resource.Layout.activity_main);
@ -51,8 +44,7 @@ namespace QuickPlay
// Since this is rather complicated, it is in a separate method // Since this is rather complicated, it is in a separate method
InitializeRecyclerView(); InitializeRecyclerView();
// FIXME: This should be in OnResume... // I don't know why, but this needs to be here in order to show correct colors and player name.
// Refresh player info
OnPlayerUpdate(); OnPlayerUpdate();
} }
@ -77,9 +69,33 @@ namespace QuickPlay
{ {
if (item.ItemId == Resource.Id.action_settings) if (item.ItemId == Resource.Id.action_settings)
{ {
// Show the play bar // Set player config URL
var bar = FindViewById(Resource.Id.currentSongBar); var b = new Android.Support.V7.App.AlertDialog.Builder(this);
bar.Visibility = ViewStates.Visible; b.SetTitle("Player config URL");
var input = new EditText(this);
input.Text = appConfig.playerConfigUrl;
b.SetView(input);
b.SetPositiveButton("Set", delegate
{
string text = input.Text;
appConfig.playerConfigUrl = text;
appConfig.saveConfiguration();
Toast.MakeText(this, "Configuration saved, reloading", ToastLength.Short).Show();
var i = new Intent(this, typeof(MainActivity));
StartActivity(i);
});
b.SetNegativeButton("Scan QR", delegate {
try
{
var i = new Intent("com.google.zxing.client.android.SCAN");
StartActivityForResult(i, 0);
} catch (ActivityNotFoundException) {
Toast.MakeText(this, "You need ZXing Barcode scanner for this", ToastLength.Long).Show();
}
});
b.SetCancelable(true);
b.Show();
} }
if (item.ItemId == Resource.Id.action_edit) if (item.ItemId == Resource.Id.action_edit)
@ -135,5 +151,51 @@ namespace QuickPlay
{ {
await currentPlayer.Play(song); await currentPlayer.Play(song);
} }
protected new async void OnResume()
{
base.OnResume();
// Reconnect the player
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();
}
// Refresh player info
OnPlayerUpdate();
}
protected new void OnDestroy()
{
base.OnDestroy();
this.appConfig.saveConfiguration();
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == 0)
{
if (resultCode == Result.Ok)
{
string url = data.GetStringExtra("SCAN_RESULT");
var cfg = new AppConfiguration { playerConfigUrl = url };
cfg.saveConfiguration();
Toast.MakeText(this, "Configuration saved, reloading", ToastLength.Short).Show();
var i = new Intent(this, typeof(MainActivity));
StartActivity(i);
}
else if (resultCode == Result.Canceled)
{
Toast.MakeText(this, "Could not load QR code", ToastLength.Short).Show();
}
}
}
} }
} }

@ -21,6 +21,8 @@ namespace QuickPlay
} } } }
public async Task Play(IPlayable song) public async Task Play(IPlayable song)
{ {
// Make sure we are connected
await this.ConnectAsync();
await mpd.SendAsync(new MpcCore.Commands.Player.Stop()); await mpd.SendAsync(new MpcCore.Commands.Player.Stop());
await SetReasonableOptions(); await SetReasonableOptions();
await mpd.SendAsync(new MpcCore.Commands.Queue.ClearQueue()); await mpd.SendAsync(new MpcCore.Commands.Queue.ClearQueue());
@ -29,6 +31,8 @@ namespace QuickPlay
} }
public async Task SetReasonableOptions() public async Task SetReasonableOptions()
{ {
// Make sure we are connected
await this.ConnectAsync();
await mpd.SendAsync(new MpcCore.Commands.Options.SetConsume(true)); 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.SetRandom(false));
await mpd.SendAsync(new MpcCore.Commands.Options.SetRepeat(false)); await mpd.SendAsync(new MpcCore.Commands.Options.SetRepeat(false));

@ -1,5 +1,5 @@
<?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="0.1" package="cz.ledoian.android.quickplay" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="2" android:versionName="0.2" package="cz.ledoian.android.quickplay" android:installLocation="auto">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30" /> <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" /> <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>

@ -42,7 +42,7 @@
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies> <BundleAssemblies>false</BundleAssemblies>
<AndroidKeyStore>false</AndroidKeyStore> <AndroidKeyStore>false</AndroidKeyStore>
<AndroidUseAapt2>false</AndroidUseAapt2> <AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
@ -134,12 +134,6 @@
<Version>28.0.0.3</Version> <Version>28.0.0.3</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Third-party\mpcCore\src\MpcCore\MpcCore.csproj">
<Project>{d0a5ad05-b98c-45e6-b61d-4700f7aa72cf}</Project>
<Name>MpcCore</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\material_settings.png" /> <AndroidResource Include="Resources\drawable-mdpi\material_settings.png" />
</ItemGroup> </ItemGroup>
@ -235,6 +229,9 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
</AndroidResource> </AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MpcCore\src\MpcCore\MpcCore.csproj" />
</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.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.
@ -243,4 +240,4 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
</Project> </Project>

@ -14,7 +14,7 @@ namespace QuickPlay
{ {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.2.99.85")]
public partial class Resource public partial class Resource
{ {

@ -99,8 +99,8 @@ the shown name.
Planned Features Planned Features
==== ====
- [ ] Working application configuration - [x] Working application configuration
- [ ] Player configuration sharing via QR codes - [x] Player configuration sharing via QR codes
- [ ] Showing current song progress - [ ] Showing current song progress
- [ ] Possibility to stop current song - [ ] Possibility to stop current song
- [ ] MPD player monitoring service - [ ] MPD player monitoring service
@ -109,9 +109,20 @@ Planned Features
Bugs Bugs
--- ---
- [ ] The MpcCore client seems to disconnect after some time for no apparent reason. None currently known :-)
The far future The far future
--- ---
- [ ] Editing the player config right on the device (with INI file export) - [ ] 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.) - [ ] Tests. (The code should be testable, but no tests has been written yet.)
Contributing
====
Please write me an [email](mailto:quickplaygt@pokemon.ledoian.cz), similar to how Linux kernel development works. In
the unlikely case you have an account on my Gitea you may submit issues and
pull requests directly.
(I would like to allow for some kind of federated Gitea accounts like
[ForgeFed](https://github.com/forgefed), but that is not possible at the
moment.)

@ -0,0 +1,23 @@
# Here are several notes on how I managed to get xamarin.android working.
# OS: Linux Mint 20.3 Mate x86_64
# Downloaded Android Studio, Added Mono, installed a lot of random packages, Android SDKs, etc.
# Xamarin.Android taken from the pipeline artifact
# Did some weird hacks with environment (TODO)
# Did awful things to the dotnet instalation:
# SO: https://askubuntu.com/questions/1177970/how-to-develop-for-android-with-xamarin
for x in /usr/lib/xamarin.android/xbuild/*; do ln -s $x /usr/share/dotnet/sdk/6.0.200/`basename $x`; done
for x in /usr/lib/xamarin.android/xbuild-frameworks/*; do ln -s $x /usr/share/dotnet/packs/`basename $x`; done
# Found some commands to be semi-working
dotnet build -p:TargetFrameworkRootPath=/usr/lib/xamarin.android/xbuild-frameworks/
nuget restore # In solution directory
msbuild /t:Build,Install # In project directory
# Launch yourself...
clean:
git clean -fxd `git rev-parse --show-toplevel`
git submodule foreach --recursive git clean -fxd

@ -0,0 +1,59 @@
---
type: slide
---
# QuickPlay
## A simple app to quickly use remote players
---
# Features
Easy to learn
Central management
(Theoretically) extensible / modular
Usable on Android 4.4+ (Tested on Marshmallow)
(Android only)
---
# Architecture
```graphviz
digraph "Arch" {
layout=fdp
RP -> Player [style="dotted", dir="both"];
RP[label="Remote player"]
XML -> AppConfig -> PlayerConfig -> {Player Playables HTTP};
UI -> LayoutStrategies;
Person -> UI -> Playables -> Metadata
Metadata -> LayoutStrategies
HTTP[label="HTTP + INI"]
Person, RP, UI, XML, HTTP [shape=box]
}
```
---
# Issues
Understanding Android ecosystem is hard
- unintuitive naming
- complex hierarchy of support libraries
- I didn't find comphrehensive overview
- many APIs are replaced
Quite a lot of boilerplate code (esp. UI)
Visual Studio not refreshing Resource files.
No sensible MPD client library.
---
# Lessons learned
- Basic Android architecture knowledge
- Network and asynchronous coding
- Library patching

@ -0,0 +1,17 @@
#!/bin/bash
set -euo pipefail
# This script takes current playlist as `mpc` sees it and converts it into
# QuickPlay INI sections.
# Use at own risk :-)
mpc -f '[[%artist% - ]%title%]#|%file%' playlist | while IFS='|' read title file; do
if test -z "$title"; then
title="$(basename "$file")"
fi
# Unfortunately, the "time" attribute is currently required
echo -e "[$title]\npath = $file\ntime = 2:00\n"
done
Loading…
Cancel
Save