Skip to content

Commit

Permalink
Merge pull request #76 from LittleBigRefresh/android
Browse files Browse the repository at this point in the history
MVP Android support
  • Loading branch information
jvyden authored Nov 3, 2024
2 parents fb09be3 + f23ae5a commit 6d85748
Show file tree
Hide file tree
Showing 38 changed files with 481 additions and 7 deletions.
18 changes: 18 additions & 0 deletions .idea/.idea.Refresher/.idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/.idea.Refresher/.idea/libraries/sentry_android_ndk_7_8_0.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Refresher.AndroidApp/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:label="@string/app_name" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" android:theme="@style/AppTheme">
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
49 changes: 49 additions & 0 deletions Refresher.AndroidApp/Logging/AndroidSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Android.OS;
using Android.Util;
using NotEnoughLogs;
using NotEnoughLogs.Sinks;

namespace Refresher.AndroidApp.Logging;

public class AndroidSink : ILoggerSink
{
private readonly Handler _handler = new(Looper.MainLooper!);

public void Log(LogLevel level, ReadOnlySpan<char> category, ReadOnlySpan<char> content)
{
LogPriority priority = level switch
{
LogLevel.Critical => LogPriority.Error,
LogLevel.Error => LogPriority.Error,
LogLevel.Warning => LogPriority.Warn,
LogLevel.Info => LogPriority.Info,
LogLevel.Debug => LogPriority.Debug,
LogLevel.Trace => LogPriority.Verbose,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null),
};

Android.Util.Log.WriteLine(priority, "Refresher." + category.ToString(), content.ToString());
string categoryStr = category.ToString();
string contentStr = content.ToString();
this._handler.Post(() =>
{
if (priority == LogPriority.Error)
{
new AlertDialog.Builder(PipelineActivity.Instance)
.SetTitle($"{categoryStr} Error")?
.SetMessage(contentStr)?
.SetNeutralButton("OK", (_, _) => {})?
.Show();
}
else if(priority == LogPriority.Warn)
{
Toast.MakeText(PipelineActivity.Instance, contentStr, ToastLength.Short)?.Show();
}
});
}

public void Log(LogLevel level, ReadOnlySpan<char> category, ReadOnlySpan<char> format, params object[] args)
{
this.Log(level, category, string.Format(format.ToString(), args));
}
}
78 changes: 78 additions & 0 deletions Refresher.AndroidApp/MainActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using _Microsoft.Android.Resource.Designer;
using Android.Content;
using Refresher.AndroidApp.Logging;
using Refresher.Core;
using Refresher.Core.Logging;
using Refresher.Core.Pipelines;
using SCEToolSharp;

using ConditionalAttribute = System.Diagnostics.ConditionalAttribute;

namespace Refresher.AndroidApp;

[Activity(Label = "@string/app_name", MainLauncher = true)]
public class MainActivity : RefresherActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);

// Set our view from the "main" layout resource
this.SetContentView(ResourceConstant.Layout.activity_main);

LinearLayout? mainContent = this.FindViewById<LinearLayout>(ResourceConstant.Id.MainContent);
if (mainContent == null)
throw new Exception("Main content not found");

this.AddButtonForPipeline<PS3PatchPipeline>(mainContent);
#if DEBUG
this.AddButtonForPipeline<ExamplePipeline>(mainContent);
#endif
this.AddSceToolSharpTestButton(mainContent);
}

private void AddButtonForPipeline<TPipeline>(LinearLayout layout) where TPipeline : Pipeline
{
Button button = new(this);
button.Text = typeof(TPipeline).Name;

button.Click += (_, _) =>
{
Intent intent = new(this, typeof(PipelineActivity));
intent.PutExtra("PipelineType", typeof(TPipeline).FullName);
this.StartActivity(intent);
};

layout.AddView(button);
}

[Conditional("DEBUG")]
private void AddSceToolSharpTestButton(LinearLayout layout)
{
Button button = new(this);
button.Text = "DEBUG: Test LibSceToolSharp";

button.Click += (_, _) =>
{
string initReturn;

try
{
initReturn = LibSceToolSharp.Init().ToString();
}
catch (Exception ex)
{
initReturn = ex.ToString();
}

new AlertDialog.Builder(this)
.SetTitle("LibSceToolSharp.Init() returns")?
.SetMessage(initReturn)?
.SetPositiveButton("Hell yeah", (_, _) => {})?
.SetNegativeButton("FUCK", (_, _) => {})?
.Show();
};

layout.AddView(button);
}
}
169 changes: 169 additions & 0 deletions Refresher.AndroidApp/PipelineActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using _Microsoft.Android.Resource.Designer;
using Android.OS;
using Android.Text;
using Android.Views;
using Refresher.Core;
using Refresher.Core.Logging;
using Refresher.Core.Pipelines;
using static Android.Views.ViewGroup.LayoutParams;

namespace Refresher.AndroidApp;

[Activity]
public class PipelineActivity : RefresherActivity
{
internal static PipelineActivity Instance { get; private set; }

private Pipeline? _pipeline;
private CancellationTokenSource? _cts;

private TextView _pipelineState = null!;
private LinearLayout _pipelineInputs = null!;
private ScrollView _logScroll = null!;
private TextView _log = null!;
private Button _button = null!;
private ProgressBar _progressBar = null!;
private ProgressBar _currentProgressBar = null!;

private readonly Handler _handler = new(Looper.MainLooper!);

public PipelineActivity()
{
Instance = this;
}

protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(ResourceConstant.Layout.activity_pipeline);

this._button = this.FindViewById<Button>(ResourceConstant.Id.ExecutePipelineButton)!;
this._button.Click += this.ExecutePipeline;

this._pipelineState = this.FindViewById<TextView>(ResourceConstant.Id.PipelineState)!;

this._progressBar = this.FindViewById<ProgressBar>(ResourceConstant.Id.ProgressBar)!;
this._currentProgressBar = this.FindViewById<ProgressBar>(ResourceConstant.Id.CurrentProgressBar)!;

this._logScroll = this.FindViewById<ScrollView>(ResourceConstant.Id.GlobalLogScroll)!;
this._log = this.FindViewById<TextView>(ResourceConstant.Id.GlobalLog)!;
State.Log += this.OnLog;

this._pipelineInputs = this.FindViewById<LinearLayout>(ResourceConstant.Id.PipelineInputs)!;

this.InitializePipeline();
this.UpdateFormStateLoop();
}

private void InitializePipeline()
{
this._cts = new CancellationTokenSource();

string? pipelineTypeName = this.Intent?.GetStringExtra("PipelineType");
if(pipelineTypeName == null)
throw new Exception("Pipeline type not specified");

Type? pipelineType = typeof(Pipeline).Assembly.GetType(pipelineTypeName);
if(pipelineType == null)
throw new Exception("Pipeline was not found");

Pipeline pipeline = (Pipeline)Activator.CreateInstance(pipelineType)!;
pipeline.Initialize();
this._pipeline = pipeline;

if (this.ActionBar != null)
{
this.ActionBar.Subtitle = pipeline.Name;
}
else
{
this.Title = "Refresher - " + pipeline.Name;
}

this._log.Text = string.Empty;

ViewGroup.LayoutParams layoutParams = new(MatchParent, WrapContent);
foreach (StepInput input in pipeline.RequiredInputs)
{
EditText view = new(this);
view.Hint = input.Name;
view.Tag = input.Id;
view.LayoutParameters = layoutParams;
view.InputType = InputTypes.ClassText | InputTypes.TextVariationNormal;

this._pipelineInputs.AddView(view);
}
}

private void ExecutePipeline(object? sender, EventArgs e)
{
if (this._pipeline == null)
return;

if (this._pipeline.State == PipelineState.Running)
{
this._cts?.Cancel();
return;
}

if (this._pipeline.State is PipelineState.Cancelled or PipelineState.Error or PipelineState.Finished)
{
this._pipeline.Reset();
}

this.UpdateFormState();

int inputCount = this._pipelineInputs.ChildCount;
for (int i = 0; i < inputCount; i++)
{
EditText child = (EditText)this._pipelineInputs.GetChildAt(i)!;
string id = (string)child.Tag!;
string value = child.Text!;

this._pipeline.Inputs.Add(id, value);
}

// State.Logger.LogInfo(LogType.Pipeline, "Starting pipeline task...");
Task.Run(async () =>
{
try
{
State.Logger.LogInfo(LogType.Pipeline, "Executing Pipeline...");
await this._pipeline.ExecuteAsync(this._cts?.Token ?? default);
this.UpdateFormState();
}
catch (Exception ex)
{
State.Logger.LogError(LogType.Pipeline, $"Error while running pipeline {this._pipeline.Name}: {ex.Message}");
}
}, this._cts?.Token ?? default);
}

private void UpdateFormState()
{
this._pipelineState.Text = this._pipeline?.State.ToString();
// this._pipelineState.Text = DateTimeOffset.Now.Ticks.ToString();

this._progressBar.Progress = (int)((this._pipeline?.Progress ?? 0) * 100);
this._currentProgressBar.Progress = (int)((this._pipeline?.CurrentProgress ?? 0) * 100);
}

private void UpdateFormStateLoop()
{
this.UpdateFormState();
this._handler.PostDelayed(this.UpdateFormStateLoop, this._pipeline?.State == PipelineState.Running ? 16 : 1000);
}

private void OnLog(RefresherLog log)
{
this._handler.Post(() =>
{
this._log.Text += $"[{log.Level}] [{log.Category}] {log.Content}\n";

this._logScroll.Post(() =>
{
this._logScroll.FullScroll(FocusSearchDirection.Down);
});
});
}
}
15 changes: 15 additions & 0 deletions Refresher.AndroidApp/Refresher.AndroidApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationId>com.littlebigrefresh.refresher</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Refresher.Core\Refresher.Core.csproj" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions Refresher.AndroidApp/RefresherActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Refresher.AndroidApp.Logging;
using Refresher.Core;
using Refresher.Core.Logging;

namespace Refresher.AndroidApp;

public abstract class RefresherActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
State.InitializeLogger([new AndroidSink(), new EventSink(), new SentryBreadcrumbSink()]);
}
}
Loading

0 comments on commit 6d85748

Please sign in to comment.