diff --git a/Xamarin.Neo4j/.github/workflows/android.yml b/Xamarin.Neo4j/.github/workflows/android.yml new file mode 100644 index 0000000..0ba785f --- /dev/null +++ b/Xamarin.Neo4j/.github/workflows/android.yml @@ -0,0 +1,105 @@ +name: Android + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + track: + description: 'Play Store track' + required: true + default: 'internal' + type: choice + options: [internal, alpha, beta, production] + +env: + ANDROID_PROJECT: Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj + OUTPUT_DIR: Xamarin.Neo4j/Xamarin.Neo4j.Android/bin/Release/net10.0-android + +jobs: + build: + name: Build & Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: microsoft + java-version: '21' + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install MAUI Android workload + run: dotnet workload install maui-android + + - name: Restore + run: dotnet restore ${{ env.ANDROID_PROJECT }} + + # ── Version ────────────────────────────────────────────────────────────── + - name: Resolve version + id: ver + run: | + DISPLAY="1.0.$GITHUB_RUN_NUMBER" + echo "display=$DISPLAY" >> $GITHUB_OUTPUT + echo "code=$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT + echo "tag=v$DISPLAY" >> $GITHUB_OUTPUT + + # ── Keystore ───────────────────────────────────────────────────────────── + - name: Decode keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > ${{ runner.temp }}/pocketgraph.jks + + # ── Build APK (GitHub Releases) ─────────────────────────────────────────── + - name: Build signed APK + run: | + dotnet publish ${{ env.ANDROID_PROJECT }} \ + -c Release \ + -p:AndroidPackageFormat=apk \ + -p:AndroidKeyStore=true \ + -p:AndroidSigningKeyStore=${{ runner.temp }}/pocketgraph.jks \ + -p:AndroidSigningKeyAlias=${{ secrets.KEYSTORE_ALIAS }} \ + -p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_KEY_PASS }} \ + -p:AndroidSigningStorePass=${{ secrets.KEYSTORE_STORE_PASS }} \ + -p:ApplicationVersion=${{ steps.ver.outputs.code }} \ + -p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }} + + # ── Build AAB (Play Store) ──────────────────────────────────────────────── + - name: Build signed AAB + run: | + dotnet publish ${{ env.ANDROID_PROJECT }} \ + -c Release \ + -p:AndroidPackageFormat=aab \ + -p:AndroidKeyStore=true \ + -p:AndroidSigningKeyStore=${{ runner.temp }}/pocketgraph.jks \ + -p:AndroidSigningKeyAlias=${{ secrets.KEYSTORE_ALIAS }} \ + -p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_KEY_PASS }} \ + -p:AndroidSigningStorePass=${{ secrets.KEYSTORE_STORE_PASS }} \ + -p:ApplicationVersion=${{ steps.ver.outputs.code }} \ + -p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }} + + # ── GitHub Release ──────────────────────────────────────────────────────── + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.ver.outputs.tag }} + name: PocketGraph ${{ steps.ver.outputs.display }} + generate_release_notes: true + files: ${{ env.OUTPUT_DIR }}/*-Signed.apk + + # ── Play Store ──────────────────────────────────────────────────────────── + - name: Upload to Play Store + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + packageName: nl.resoftware.pocketgraph + releaseFiles: ${{ env.OUTPUT_DIR }}/*.aab + track: ${{ github.event.inputs.track || 'internal' }} + status: completed + whatsNewDirectory: distribution/whatsnew diff --git a/Xamarin.Neo4j/.gitignore b/Xamarin.Neo4j/.gitignore new file mode 100644 index 0000000..a4c056d --- /dev/null +++ b/Xamarin.Neo4j/.gitignore @@ -0,0 +1,5 @@ + +# Android signing — never commit +*.jks +*.keystore +*.env diff --git a/Xamarin.Neo4j/Xamarin.Neo4j.sln b/Xamarin.Neo4j/Xamarin.Neo4j.sln index 0e6f9bc..7f41efb 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j.sln +++ b/Xamarin.Neo4j/Xamarin.Neo4j.sln @@ -15,9 +15,11 @@ Global Ad-Hoc|iPhone = Ad-Hoc|iPhone AppStore|iPhone = AppStore|iPhone Debug|Any CPU = Debug|Any CPU + Debug|Android = Debug|Android Debug|iPhone = Debug|iPhone Debug|iPhoneSimulator = Debug|iPhoneSimulator Release|Any CPU = Release|Any CPU + Release|Android = Release|Android Release|iPhone = Release|iPhone Release|iPhoneSimulator = Release|iPhoneSimulator EndGlobalSection @@ -47,6 +49,9 @@ Global {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Build.0 = Debug|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.ActiveCfg = Debug|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.Build.0 = Debug|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.Deploy.0 = Debug|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.ActiveCfg = Debug|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Build.0 = Debug|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Deploy.0 = Debug|Any CPU @@ -56,6 +61,9 @@ Global {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.ActiveCfg = Release|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Build.0 = Release|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Deploy.0 = Release|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.ActiveCfg = Release|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.Build.0 = Release|Any CPU + {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.Deploy.0 = Release|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.ActiveCfg = Release|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Build.0 = Release|Any CPU {24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Deploy.0 = Release|Any CPU diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/CustomRenderers/QueryEditorHandler.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/CustomRenderers/QueryEditorHandler.cs new file mode 100644 index 0000000..2e55674 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/CustomRenderers/QueryEditorHandler.cs @@ -0,0 +1,218 @@ +// +// QueryEditorHandler.cs +// +// © Xamarin.Neo4j.Android +// + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Android.Content; +using Android.Graphics; +using Android.Text; +using Android.Text.Style; +using Android.Views; +using Android.Views.InputMethods; +using Android.Widget; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using Xamarin.Neo4j.Controls; + +namespace Xamarin.Neo4j.Android.CustomRenderers +{ + public class QueryEditorHandler : EditorHandler + { + private readonly string[] _keyWords = + [ + // Clauses + "CALL", "CREATE", "DELETE", "DETACH", "FOREACH", "LOAD", "MATCH", "MERGE", "OPTIONAL", "REMOVE", "RETURN", "SET", "START", "UNION", "UNWIND", "WITH", + + // Subclauses + "LIMIT", "ORDER", "SKIP", "WHERE", "YIELD", + + // Modifiers + "ASC", "ASCENDING", "ASSERT", "BY", "CSV", "DESC", "DESCENDING", "ON", + + // Expressions + "ALL", "CASE", "COUNT", "ELSE", "END", "EXISTS", "THEN", "WHEN", + + // Operators + "AND", "AS", "CONTAINS", "DISTINCT", "ENDS", "IN", "IS", "NOT", "OR", "STARTS", "XOR", + + // Schema + "CONSTRAINT", "CREATE", "DROP", "EXISTS", "INDEX", "NODE", "KEY", "UNIQUE", + + // Hints + "INDEX", "JOIN", "SCAN", "USING", + + // Literals + "FALSE", "NULL", "TRUE" + ]; + + protected override void ConnectHandler(MauiAppCompatEditText platformView) + { + base.ConnectHandler(platformView); + + // Disable autocorrect, autocapitalize, and spellcheck + platformView.InputType = InputTypes.ClassText + | InputTypes.TextFlagMultiLine + | InputTypes.TextFlagNoSuggestions; + + // Keyboard toolbar with symbol keys and Execute button + var toolbar = BuildToolbar(platformView); + platformView.FocusChange += (s, e) => + toolbar.Visibility = e.HasFocus ? ViewStates.Visible : ViewStates.Gone; + + // Syntax highlighting + platformView.AddTextChangedListener(new CypherTextWatcher(platformView, _keyWords)); + HighlightSyntax(platformView, _keyWords); + } + + private LinearLayout BuildToolbar(MauiAppCompatEditText platformView) + { + var context = platformView.Context; + + var toolbar = new LinearLayout(context) + { + Orientation = Orientation.Horizontal, + LayoutParameters = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MatchParent, + ViewGroup.LayoutParams.WrapContent) + }; + toolbar.SetBackgroundColor(Color.ParseColor("#F2F2F7")); + toolbar.SetPadding(8, 8, 8, 8); + toolbar.Visibility = ViewStates.Gone; + + // Scrollable symbol key strip + var scrollView = new HorizontalScrollView(context) + { + LayoutParameters = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f) + }; + scrollView.HorizontalScrollBarEnabled = false; + + var keyRow = new LinearLayout(context) { Orientation = Orientation.Horizontal }; + foreach (var key in new[] { "(", ")", "[", "]", ":", "-", "->", "<-" }) + { + var captured = key; + var btn = CreatePillButton(context, captured); + btn.Click += (s, e) => + { + var start = Math.Max(platformView.SelectionStart, 0); + var end = Math.Max(platformView.SelectionEnd, 0); + platformView.EditableText?.Replace(Math.Min(start, end), Math.Max(start, end), captured); + }; + keyRow.AddView(btn); + } + + scrollView.AddView(keyRow); + toolbar.AddView(scrollView); + + // Execute button + var executeBtn = CreatePillButton(context, "Execute"); + executeBtn.SetTextColor(Color.White); + executeBtn.SetBackgroundColor(Color.ParseColor("#007AFF")); + executeBtn.Click += (s, e) => + { + if (VirtualView is QueryEditor queryEditor) + { + queryEditor.RaiseExecuteClicked(); + var imm = (InputMethodManager)context.GetSystemService(Context.InputMethodService); + imm?.HideSoftInputFromWindow(platformView.WindowToken, 0); + } + }; + toolbar.AddView(executeBtn); + + // Attach toolbar to the parent view hierarchy + platformView.ViewTreeObserver.GlobalLayout += (s, e) => + { + if (platformView.Parent is ViewGroup parent && toolbar.Parent == null) + parent.AddView(toolbar); + }; + + return toolbar; + } + + private static Button CreatePillButton(Context context, string label) + { + return new Button(context) + { + Text = label, + LayoutParameters = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WrapContent, + ViewGroup.LayoutParams.WrapContent) + { + MarginStart = 4, + MarginEnd = 4 + } + }; + } + + // ── Syntax highlighting ─────────────────────────────────────────────── + + internal static void HighlightSyntax(MauiAppCompatEditText platformView, IEnumerable keywords) + { + var text = PreprocessText(platformView.Text ?? string.Empty); + var spannable = new SpannableStringBuilder(text); + + var keywordColor = Color.ParseColor("#899832"); + var literalColor = Color.ParseColor("#AE8B2D"); + + foreach (var word in keywords) + { + var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase); + foreach (Match match in regex.Matches(text)) + spannable.SetSpan(new ForegroundColorSpan(keywordColor), + match.Index, match.Index + match.Length, + SpanTypes.ExclusiveExclusive); + } + + ApplyQuoteHighlight(text, spannable, "'(.*?)'", literalColor); + ApplyQuoteHighlight(text, spannable, "\"(.*?)\"", literalColor); + + var selStart = platformView.SelectionStart; + var selEnd = platformView.SelectionEnd; + platformView.SetText(spannable, TextView.BufferType.Spannable); + if (selStart >= 0 && selEnd <= spannable.Length()) + platformView.SetSelection(selStart, selEnd); + } + + private static void ApplyQuoteHighlight(string text, SpannableStringBuilder spannable, + string pattern, Color color) + { + foreach (Match match in new Regex(pattern).Matches(text)) + spannable.SetSpan(new ForegroundColorSpan(color), + match.Index, match.Index + match.Length, + SpanTypes.ExclusiveExclusive); + } + + private static string PreprocessText(string text) => + text.Replace("\u2018", "'").Replace("\u2019", "'") + .Replace("\u201c", "\"").Replace("\u201d", "\""); + + // ── Inner helpers ───────────────────────────────────────────────────── + + private sealed class CypherTextWatcher : Java.Lang.Object, ITextWatcher + { + private readonly MauiAppCompatEditText _view; + private readonly string[] _keywords; + private bool _updating; + + public CypherTextWatcher(MauiAppCompatEditText view, string[] keywords) + { + _view = view; + _keywords = keywords; + } + + public void BeforeTextChanged(Java.Lang.ICharSequence s, int start, int count, int after) { } + public void OnTextChanged(Java.Lang.ICharSequence s, int start, int before, int count) { } + + public void AfterTextChanged(IEditable s) + { + if (_updating) return; + _updating = true; + try { HighlightSyntax(_view, _keywords); } + finally { _updating = false; } + } + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainActivity.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainActivity.cs index 347df4f..c0e2dae 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainActivity.cs +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainActivity.cs @@ -1,38 +1,74 @@ -using System; -using Acr.UserDialogs; using Android.App; using Android.Content.PM; -using Android.Runtime; +using Android.Content.Res; using Android.Views; -using Android.Widget; -using Android.OS; +using AndroidX.AppCompat.Widget; +using AndroidX.Core.View; +using Microsoft.Maui; +using Microsoft.Maui.ApplicationModel; +using AColor = Android.Graphics.Color; namespace Xamarin.Neo4j.Android { - [Activity(Label = "Xamarin.Neo4j", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] - public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity + [Activity( + Theme = "@style/Maui.SplashTheme", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | + ConfigChanges.UiMode | ConfigChanges.ScreenLayout | + ConfigChanges.SmallestScreenSize | ConfigChanges.Density + )] + public class MainActivity : MauiAppCompatActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnResume() { - TabLayoutResource = Resource.Layout.Tabbar; - ToolbarResource = Resource.Layout.Toolbar; + base.OnResume(); - UserDialogs.Init(this); + // Re-apply theme and nav bar colours when returning to the foreground. + // ConfigChanges.UiMode prevents Activity recreation on system theme change, + // so RequestedThemeChanged may not fire reliably — we re-derive the theme + // from the current configuration instead. + if (IPlatformApplication.Current?.Application is App app) + { + var nightMode = Resources.Configuration.UiMode & UiMode.NightMask; + var isDark = nightMode == UiMode.NightYes; - base.OnCreate(savedInstanceState); + // Status bar: match page background, icons contrast with it + if (Window != null) + { + var statusBarColor = isDark + ? AColor.ParseColor("#0c0c0c") + : AColor.ParseColor("#f5f5f5"); + Window.SetStatusBarColor(statusBarColor); - Essentials.Platform.Init(this, savedInstanceState); + var controller = new WindowInsetsControllerCompat(Window, Window.DecorView); + controller.AppearanceLightStatusBars = !isDark; + } - Forms.Forms.Init(this, savedInstanceState); - - LoadApplication(new App()); + MainThread.BeginInvokeOnMainThread(() => + app.UpdateTheme(isDark ? AppTheme.Dark : AppTheme.Light)); + } } - public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + public override void OnWindowFocusChanged(bool hasFocus) { - Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); + base.OnWindowFocusChanged(hasFocus); + if (hasFocus) + TintToolbarOverflow(Window?.DecorView as ViewGroup); + } - base.OnRequestPermissionsResult(requestCode, permissions, grantResults); + // MAUI creates its toolbar programmatically so theme-based tinting doesn't reach + // the overflow icon. Walk the view tree and apply the tint directly. + private static void TintToolbarOverflow(ViewGroup? parent) + { + if (parent == null) return; + for (var i = 0; i < parent.ChildCount; i++) + { + var child = parent.GetChildAt(i); + if (child is Toolbar toolbar) + toolbar.OverflowIcon?.SetTint(AColor.White); + else if (child is ViewGroup vg) + TintToolbarOverflow(vg); + } } } } diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainApplication.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainApplication.cs new file mode 100644 index 0000000..edbf934 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MainApplication.cs @@ -0,0 +1,17 @@ +using System; +using Android.App; +using Android.Runtime; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace Xamarin.Neo4j.Android +{ + [Application] + public class MainApplication : MauiApplication + { + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) { } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MauiProgram.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MauiProgram.cs new file mode 100644 index 0000000..ec672ca --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/MauiProgram.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls.Hosting; +using Microsoft.Maui.Hosting; +using Xamarin.Neo4j.Android.CustomRenderers; +using Xamarin.Neo4j.Android.Services; +using Xamarin.Neo4j.Services; +using Xamarin.Neo4j.Services.Interfaces; + +namespace Xamarin.Neo4j.Android +{ + public static class MauiProgram + { + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("iconize-fontawesome-brands.ttf", "FontAwesome5Brands-Regular"); + fonts.AddFont("iconize-fontawesome-solid.ttf", "FontAwesome5Free-Solid"); + fonts.AddFont("iconize-fontawesome-regular.ttf", "FontAwesome5Free-Regular"); + fonts.AddFont("roboto-mono-regular.ttf", "RobotoMono"); + }) + .ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + }); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Properties/AndroidManifest.xml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Properties/AndroidManifest.xml index 15c9055..d142390 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Properties/AndroidManifest.xml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Properties/AndroidManifest.xml @@ -1,5 +1,26 @@ - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-hdpi/appicon.png b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-hdpi/appicon.png new file mode 100644 index 0000000..9a475e1 Binary files /dev/null and b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-hdpi/appicon.png differ diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-mdpi/appicon.png b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-mdpi/appicon.png new file mode 100644 index 0000000..1e4b7c4 Binary files /dev/null and b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-mdpi/appicon.png differ diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xhdpi/appicon.png b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xhdpi/appicon.png new file mode 100644 index 0000000..96103bf Binary files /dev/null and b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xhdpi/appicon.png differ diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxhdpi/appicon.png b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxhdpi/appicon.png new file mode 100644 index 0000000..63b344b Binary files /dev/null and b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxhdpi/appicon.png differ diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxxhdpi/appicon.png b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxxhdpi/appicon.png new file mode 100644 index 0000000..a561b03 Binary files /dev/null and b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/mipmap-xxxhdpi/appicon.png differ diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values-night/colors.xml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000..92c4413 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values-night/colors.xml @@ -0,0 +1,5 @@ + + + #0c0c0c + #0c0c0c + diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/colors.xml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/colors.xml index d9f6e0b..92c65d1 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/colors.xml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/colors.xml @@ -1,7 +1,10 @@ - #FFFFFF - #3F51B5 - #303F9F - #FF4081 + #0c0c0c + #141414 + #0c0c0c + #e2e2e2 + #141414 + #f5f5f5 + #f5f5f5 diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/styles.xml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/styles.xml index 43b0a58..69f8d5c 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/styles.xml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Resources/values/styles.xml @@ -1,30 +1,43 @@ - - - - @style/AppCompatDialogStyle - + + + + + + + + - diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Security/NativeTrustManager.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Security/NativeTrustManager.cs new file mode 100644 index 0000000..d8a1d1e --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Security/NativeTrustManager.cs @@ -0,0 +1,52 @@ +// +// NativeTrustManager.cs +// +// © Xamarin.Neo4j.Android +// + +using System; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Java.Security; +using Java.Security.Cert; +using Javax.Net.Ssl; +using Neo4j.Driver; + +namespace Xamarin.Neo4j +{ + public class NativeTrustManager : TrustManager + { + private readonly IX509TrustManager _androidTrustManager; + + public NativeTrustManager() + { + var factory = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm); + factory.Init((KeyStore)null); + _androidTrustManager = factory.GetTrustManagers() + .OfType() + .First(); + } + + public override bool ValidateServerCertificate(Uri uri, X509Certificate2 certificate, X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + try + { + // The Xamarin binding of CertificateFactory.GenerateCertificate accepts a System.IO.Stream + var certFactory = CertificateFactory.GetInstance("X.509"); + using var stream = new MemoryStream(certificate.RawData); + var javaCert = certFactory.GenerateCertificate(stream) as Java.Security.Cert.X509Certificate; + + _androidTrustManager.CheckServerTrusted( + new Java.Security.Cert.X509Certificate[] { javaCert }, "RSA"); + return true; + } + catch (Exception ex) + { + throw new Exception($"Certificate validation failed for {uri.Host}: {ex.Message}", ex); + } + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/ScreenSizeService.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/ScreenSizeService.cs new file mode 100644 index 0000000..b9ccf37 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/ScreenSizeService.cs @@ -0,0 +1,25 @@ +// +// ScreenSizeService.cs +// +// © Xamarin.Neo4j.Android +// + +using Xamarin.Neo4j.Services.Interfaces; + +namespace Xamarin.Neo4j.Android.Services +{ + public class ScreenSizeService : IScreenSizeService + { + public int GetScreenHeight() + { + var metrics = global::Android.App.Application.Context.Resources.DisplayMetrics; + return (int)(metrics.HeightPixels / metrics.Density); + } + + public int GetScreenWidth() + { + var metrics = global::Android.App.Application.Context.Resources.DisplayMetrics; + return (int)(metrics.WidthPixels / metrics.Density); + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/TrustManagerService.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/TrustManagerService.cs new file mode 100644 index 0000000..b021c99 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/TrustManagerService.cs @@ -0,0 +1,19 @@ +// +// TrustManagerService.cs +// +// © Xamarin.Neo4j.Android +// + +using Neo4j.Driver; +using Xamarin.Neo4j.Services.Interfaces; + +namespace Xamarin.Neo4j.Android.Services +{ + public class TrustManagerService : ITrustManagerService + { + public TrustManager GetNativeTrustManager() + { + return new NativeTrustManager(); + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/VersionService.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/VersionService.cs new file mode 100644 index 0000000..516b8a8 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Services/VersionService.cs @@ -0,0 +1,32 @@ +// +// VersionService.cs +// +// © Xamarin.Neo4j.Android +// + +using Android.OS; +using Xamarin.Neo4j.Services.Interfaces; + +namespace Xamarin.Neo4j.Android.Services +{ + public class VersionService : IVersionService + { + public string GetVersion() + { + var context = global::Android.App.Application.Context; + var info = context.PackageManager.GetPackageInfo(context.PackageName, 0); + return info.VersionName; + } + + public string GetBuild() + { + var context = global::Android.App.Application.Context; + var info = context.PackageManager.GetPackageInfo(context.PackageName, 0); +#pragma warning disable CS0618 + return Build.VERSION.SdkInt >= BuildVersionCodes.P + ? info.LongVersionCode.ToString() + : info.VersionCode.ToString(); +#pragma warning restore CS0618 + } + } +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj index d2747d9..c3d8da6 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj @@ -1,73 +1,50 @@ - - + + - Debug - AnyCPU - {24C5C58E-C59D-4621-9BF0-2E3BAC547455} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Xamarin.Neo4j.Android + net10.0-android + Exe + true Xamarin.Neo4j.Android - True - Resources\Resource.designer.cs - Resource - Properties\AndroidManifest.xml - Resources - Assets - v10.0 - Xamarin.Android.Net.AndroidClientHandler - - - true - portable - false - bin\Debug - DEBUG; - prompt - 4 - None - - - true - pdbonly - true - bin\Release - prompt - 4 - true - false + nl.resoftware.pocketgraph + PocketGraph + 21 + latest + disable + + true + - - - - - + + - + + + + + + + + - - - + + + + + + + - - - + - - - - - - - - - {756232D0-DBB0-4AEB-B1CF-1F5CA12CAC85} - Xamarin.Neo4j - - - + diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/App.xaml.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/App.xaml.cs index 0b14e8e..2e885b0 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/App.xaml.cs +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/App.xaml.cs @@ -1,4 +1,6 @@ +using System; using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Xaml; using Xamarin.Neo4j.Pages; @@ -10,15 +12,23 @@ namespace Xamarin.Neo4j { public partial class App : Application { + public static event EventHandler ThemeChanged; + public App() { InitializeComponent(); SetTheme(Current.RequestedTheme); + RequestedThemeChanged += (s, e) => SetTheme(e.RequestedTheme); - MainPage = new NavigationPage(new RootPage()); + MainPage = new NavigationPage(new ConnectionsPage()); + + // SetTheme ran before MainPage was assigned, so apply bar colours now. + ApplyNavBarColors(); } + public void UpdateTheme(AppTheme theme) => SetTheme(theme); + private void SetTheme(AppTheme theme) { Resources = theme switch @@ -28,6 +38,20 @@ namespace Xamarin.Neo4j _ => new LightTheme() }; + + ApplyNavBarColors(); + ThemeChanged?.Invoke(this, EventArgs.Empty); + } + + // Called both from SetTheme and from MainActivity.OnResume so that + // theme changes while the app is backgrounded are picked up on return. + public void ApplyNavBarColors() + { + if (MainPage is NavigationPage navPage) + { + navPage.BarBackgroundColor = Color.FromArgb("#31333b"); + navPage.BarTextColor = Colors.White; + } } protected override void OnStart() diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml index 55315c8..1219f18 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml @@ -5,16 +5,9 @@ xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j" x:Class="Xamarin.Neo4j.Controls.ConnectionCell"> - - - - - - diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml.cs b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml.cs index 3ddb2d8..1ab5a8c 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml.cs +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/ConnectionCell.xaml.cs @@ -15,21 +15,6 @@ namespace Xamarin.Neo4j.Controls public ConnectionCell() { InitializeComponent(); - - isActiveIndicator.SetBinding(VisualElement.IsVisibleProperty, new Binding(nameof(IsActive), source: this)); } - - #region Bindable Properties - - public bool IsActive - { - get => (bool)GetValue(IsActiveProperty); - set => SetValue(IsActiveProperty, value); - } - - public static BindableProperty IsActiveProperty = BindableProperty.Create("IsActive", typeof(bool), - typeof(ConnectionCell), false, BindingMode.OneWay); - - #endregion } } diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/LicenseCell.xaml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/LicenseCell.xaml index 2192cfc..b952c54 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/LicenseCell.xaml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/LicenseCell.xaml @@ -1,13 +1,13 @@ - diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/QueryResultView.xaml b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/QueryResultView.xaml index 37dd27f..eb66b0a 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/QueryResultView.xaml +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j/Controls/QueryResultView.xaml @@ -2,28 +2,28 @@ - - -