Skip to content

Commit

Permalink
Export / import a creation via QR code (imurvai#61)
Browse files Browse the repository at this point in the history
* Export / import a creation via QR code
* Resolve warnings
* Finetune config + doc + DE resources
  • Loading branch information
vicocz authored Nov 5, 2024
1 parent cd868aa commit 2c0583c
Show file tree
Hide file tree
Showing 31 changed files with 652 additions and 12 deletions.
2 changes: 2 additions & 0 deletions BrickController2/BrickController2.Android/MainApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using BrickController2.Droid.UI.Services.DI;
using BrickController2.UI.Controls;
using BrickController2.UI.DI;
using ZXing.Net.Maui.Controls;

namespace BrickController2.Droid
{
Expand All @@ -43,6 +44,7 @@ protected override MauiApp CreateMauiApp()
{
handlers.AddHandler<ExtendedSlider, ExtendedSliderHandler>();
})
.UseBarcodeReader()
.ConfigureContainer(new AutofacServiceProviderFactory(), autofacBuilder =>
{
autofacBuilder.RegisterInstance(this).As<Context>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<SharedFileStorageService>().As<ISharedFileStorageService>().SingleInstance();
builder.RegisterType<ReadWriteExternalStoragePermission>().As<IReadWriteExternalStoragePermission>().InstancePerDependency();
builder.RegisterType<BluetoothPermission>().As<IBluetoothPermission>().InstancePerDependency();
builder.RegisterType<CameraPermission>().As<ICameraPermission>().InstancePerDependency();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using BrickController2.PlatformServices.Permission;
using static Microsoft.Maui.ApplicationModel.Permissions;

namespace BrickController2.Droid.PlatformServices.Permission;

internal class CameraPermission : Camera, ICameraPermission
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.TRANSMIT_IR" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<application android:label="BrickController2.Android" android:icon="@mipmap/ic_launcher"></application>
Expand Down
2 changes: 2 additions & 0 deletions BrickController2/BrickController2.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;
using Windows.Devices.Input;
using ZXing.Net.Maui.Controls;

namespace BrickController2.Windows;

Expand Down Expand Up @@ -45,6 +46,7 @@ protected override MauiApp CreateMauiApp()
handlers.AddHandler<SwipeView, CustomSwipeViewHandler>();
}
})
.UseBarcodeReader()
.ConfigureContainer(new AutofacServiceProviderFactory(), autofacBuilder =>
{
autofacBuilder.RegisterModule<PlatformServicesModule>();
Expand Down
2 changes: 2 additions & 0 deletions BrickController2/BrickController2.iOS/AppDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using BrickController2.iOS.UI.CustomHandlers;
using BrickController2.UI.Controls;
using BrickController2.iOS.UI.CustomRenderers;
using ZXing.Net.Maui.Controls;

namespace BrickController2.iOS
{
Expand All @@ -34,6 +35,7 @@ protected override MauiApp CreateMauiApp()
handlers.AddHandler<ExtendedSlider, ExtendedSliderHandler>();
handlers.AddHandler(typeof(ListView), typeof(NoAnimListViewRenderer));
})
.UseBarcodeReader()
.ConfigureContainer(new AutofacServiceProviderFactory(), autofacBuilder =>
{
autofacBuilder.RegisterModule(new PlatformServicesModule());
Expand Down
2 changes: 2 additions & 0 deletions BrickController2/BrickController2.iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
<string>Location access is required to use SBrick, BuWizz or Powered-Up devices.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Location access is required to use SBrick, BuWizz or Powered-Up devices.</string>
<key>NSCameraUsageDescription</key>
<string>Camera is required in order to import a creation via QR code from another application.</string>
<key>CFBundleShortVersionString</key>
<string>3.4</string>
<key>CFBundleVersion</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<SharedFileStorageService>().As<ISharedFileStorageService>().SingleInstance();
builder.RegisterType<ReadWriteExternalStoragePermission>().As<IReadWriteExternalStoragePermission>().InstancePerDependency();
builder.RegisterType<BluetoothPermission>().As<IBluetoothPermission>().InstancePerDependency();
builder.RegisterType<CameraPermission>().As<ICameraPermission>().InstancePerDependency();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using BrickController2.PlatformServices.Permission;
using static Microsoft.Maui.ApplicationModel.Permissions;

namespace BrickController2.iOS.PlatformServices.Permission;

internal class CameraPermission : Camera, ICameraPermission
{
}
2 changes: 2 additions & 0 deletions BrickController2/BrickController2/BrickController2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
<PackageReference Include="Microsoft.Maui.Controls" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" />
<PackageReference Include="Microsoft.Maui.Essentials" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="SQLiteNetExtensions" />
<PackageReference Include="SQLiteNetExtensions.Async" />
<PackageReference Include="SQLitePCLRaw.bundle_green" />
<PackageReference Include="ZXing.Net.Maui.Controls" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task ImportCreationAsync(string creationFilename)
var creationJson = await File.ReadAllTextAsync(creationFilename);
var creation = JsonConvert.DeserializeObject<Creation>(creationJson);

await ImportCreationAsync(creation);
await ImportCreationAsync(creation!);
}


Expand Down Expand Up @@ -146,7 +146,7 @@ public async Task ImportControllerProfileAsync(Creation creation, string control
var controllerProfileJson = await File.ReadAllTextAsync(controllerProfileFilename);
var controllerProfile = JsonConvert.DeserializeObject<ControllerProfile>(controllerProfileJson);

await ImportControllerProfileAsync(creation, controllerProfile);
await ImportControllerProfileAsync(creation, controllerProfile!);
}

public async Task ImportControllerProfileAsync(Creation creation, ControllerProfile controllerProfile)
Expand Down Expand Up @@ -360,7 +360,7 @@ public async Task ImportSequenceAsync(string sequenceFilename)
var sequenceJson = await File.ReadAllTextAsync(sequenceFilename);
var sequence = JsonConvert.DeserializeObject<Sequence>(sequenceJson);

await ImportSequenceAsync(sequence);
await ImportSequenceAsync(sequence!);
}

public async Task ImportSequenceAsync(Sequence sequence)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ public interface ISharingManager<TModel> where TModel : class, IShareable
/// Shares json model of <typeparamref name="TModel"/> to clipboard
/// </summary>
Task ShareToClipboardAsync(TModel model);


/// <summary>
/// Export the specified <paramref name="model"/> as JSON.
/// </summary>
Task<string> ShareAsync(TModel model);

/// <summary>
/// Imports the content of clipboard as json model of <typeparamref name="TModel"/>
/// </summary>
Task<TModel> ImportFromClipboardAsync();

/// <summary>
/// Imports the content of json model of <typeparamref name="TModel"/>
/// </summary>
TModel Import(string json);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace BrickController2.CreationManagement.Sharing;

internal class ShareablePayloadConverter<TModel> : JsonConverter<ShareablePayload<TModel>>
where TModel : class, IShareable
{
// reasonable value to optimize both json text readibility and pixel size of rendered QR
private const int MaxSize = 1024;
private readonly JsonSerializerSettings _settings;

public ShareablePayloadConverter(JsonSerializerSettings settings)
{
_settings = settings;
}

public override ShareablePayload<TModel> ReadJson(JsonReader reader, Type objectType, ShareablePayload<TModel>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
throw new JsonException($"Incorrect payload format. TokenType:{reader.TokenType}");

TModel payload = default!;

while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject)
return new(payload);

if (reader.TokenType != JsonToken.PropertyName)
throw new JsonException($"Incorrect payload format. TokenType:{reader.TokenType}");

var propertyName = reader.Value?.ToString();
switch (propertyName)
{
case ShareablePayload<TModel>.ContentTypeProperty:
// validate expected content type
var contentType = reader.ReadAsString();
if (contentType != TModel.Type)
throw new JsonException($"Unsuppported content type: {contentType}.");
break;
case ShareablePayload<TModel>.PayloadProperty:
// load payload
if (reader.Read())
{
payload = DeserializePayload(reader, serializer);
}
break;

default:
// unknown property
throw new JsonException($"Unexpected property: {propertyName}.");
}
}

throw new JsonException("Incorrect payload format.");
}

private static TModel DeserializePayload(JsonReader reader, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.StartObject:
// directly deserialize payload
return serializer.Deserialize<TModel>(reader)!;

case JsonToken.String:
// unzip Base64 payload string
{
using var input = new MemoryStream(Convert.FromBase64String((string)reader.Value!));
using var unzip = new GZipStream(input, CompressionMode.Decompress);
using var json = new StreamReader(unzip);
return (TModel)serializer.Deserialize(json, typeof(TModel))!;
}

default:
throw new JsonException($"Unexpected token type: {reader.TokenType}.");
}
}

public override void WriteJson(JsonWriter writer, ShareablePayload<TModel>? value, JsonSerializer serializer)
{
writer.WriteStartObject();
// write content type of the model
writer.WritePropertyName(ShareablePayload<TModel>.ContentTypeProperty);
writer.WriteValue(TModel.Type);
// write payload based on size autodetection
var payload = JsonConvert.SerializeObject(value!.Payload, _settings);

writer.WritePropertyName(ShareablePayload<TModel>.PayloadProperty);

// autodetect final format based on source payload size
if (payload.Length < MaxSize)
{
writer.WriteRawValue(payload);
}
else
{
using var output = new MemoryStream();
using var zip = new GZipStream(output, CompressionMode.Compress);
zip.Write(Encoding.UTF8.GetBytes(payload));
zip.Flush();

// zipped byte[] is writen as base64
writer.WriteValue(output.ToArray());
}

writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,36 @@ public class SharingManager<TModel> : ISharingManager<TModel> where TModel : cla
public SharingManager()
{
// default options for JSON
JsonOptions = new JsonSerializerSettings
JsonOptions = new()
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
};
// compact options using auto gzip converter
CompactJsonOptions = new(JsonOptions);
CompactJsonOptions.Converters.Add(new ShareablePayloadConverter<TModel>(CompactJsonOptions));
}

internal JsonSerializerSettings JsonOptions { get; }
internal JsonSerializerSettings CompactJsonOptions { get; }

/// <inheritdoc/>
public Task<string> ShareAsync(TModel model) => ShareAsync(model, CompactJsonOptions);

/// <summary>
/// Export the specified <paramref name="item"/> as serialized JSON model
/// </summary>
internal Task<string> ShareAsync(TModel model)
internal static Task<string> ShareAsync(TModel model, JsonSerializerSettings options)
{
var payload = new ShareablePayload<TModel>(model);
var json = JsonConvert.SerializeObject(payload, JsonOptions);
var json = JsonConvert.SerializeObject(payload, options);
return Task.FromResult(json);
}

/// <inheritdoc/>
public async Task ShareToClipboardAsync(TModel model)
{
var json = await ShareAsync(model);
var json = await ShareAsync(model, JsonOptions);
await MainThread.InvokeOnMainThreadAsync(() =>
{
Clipboard.SetTextAsync(json);
Expand All @@ -43,15 +51,18 @@ await MainThread.InvokeOnMainThreadAsync(() =>
public async Task<TModel> ImportFromClipboardAsync()
{
var json = await MainThread.InvokeOnMainThreadAsync(Clipboard.GetTextAsync);
return Import(json);
return Import(json, JsonOptions);
}

internal TModel Import(string? json)
/// <inheritdoc/>
public TModel Import(string json) => Import(json, CompactJsonOptions);

internal static TModel Import(string? json, JsonSerializerSettings options)
{
if (json is null)
throw new InvalidOperationException("No json data.");

var model = JsonConvert.DeserializeObject<ShareablePayload<TModel>>(json, JsonOptions);
var model = JsonConvert.DeserializeObject<ShareablePayload<TModel>>(json, options);

if (model?.Payload is null)
throw new InvalidOperationException("Invalid json data.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Maui.ApplicationModel;
using System.Threading.Tasks;

namespace BrickController2.PlatformServices.Permission;

public interface ICameraPermission
{
Task<PermissionStatus> CheckStatusAsync();
Task<PermissionStatus> RequestAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@
<data name="ImportSequence" xml:space="preserve">
<value>Eine Sequenz importieren</value>
</data>
<data name="ImportSuccessful" xml:space="preserve">
<value>Import erfolgreich:</value>
</data>
<data name="Information" xml:space="preserve">
<value>Information</value>
</data>
Expand Down Expand Up @@ -495,6 +498,9 @@
<data name="Scanning" xml:space="preserve">
<value>Scannen...</value>
</data>
<data name="ScanQr" xml:space="preserve">
<value>QR-Code scannen</value>
</data>
<data name="SearchingForDevices" xml:space="preserve">
<value>Geräte suchen</value>
</data>
Expand Down Expand Up @@ -528,6 +534,9 @@
<data name="Settings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="Share" xml:space="preserve">
<value>Teilen</value>
</data>
<data name="ShortChannel" xml:space="preserve">
<value>Ka</value>
</data>
Expand Down
Loading

0 comments on commit 2c0583c

Please sign in to comment.