diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 90c8504a5..f40ce596c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,7 +80,7 @@ jobs: dotnet-version: '8.0.x' - name: Set up MSBuild - uses: microsoft/setup-msbuild@031090342aeefe171e49f3820f3b52110c66e402 # v1.3.2 + uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3 # This step might be obsolete at some point as .NET MAUI workloads # are starting to come pre-installed on the GH Actions build agents. @@ -310,7 +310,7 @@ jobs: dotnet-version: '8.0.x' - name: Set up MSBuild - uses: microsoft/setup-msbuild@031090342aeefe171e49f3820f3b52110c66e402 # v1.3.2 + uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3 # This step might be obsolete at some point as .NET MAUI workloads # are starting to come pre-installed on the GH Actions build agents. @@ -366,13 +366,6 @@ jobs: $androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}"); - # Write-Output "########################################" - # Write-Output "##### Clean Android and App" - # Write-Output "########################################" - - # msbuild "$($androidPath)" "/t:Clean" "/p:Configuration=FDroid" - # msbuild "$($appPath)" "/t:Clean" "/p:Configuration=FDroid" - Write-Output "########################################" Write-Output "##### Backup project files" Write-Output "########################################" @@ -392,42 +385,6 @@ jobs: $xml.Save($androidManifest); - # Write-Output "########################################" - # Write-Output "##### Uninstall from App.csproj" - # Write-Output "########################################" - - # $xml=New-Object XML; - # $xml.Load($appPath); - - # $ns=New-Object System.Xml.XmlNamespaceManager($xml.NameTable); - # $ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI); - - # $firebaseNode=$xml.SelectSingleNode(` - # "/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns); - # $firebaseNode.ParentNode.RemoveChild($firebaseNode); - - # $daggerNode=$xml.SelectSingleNode(` - # "/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Google.Dagger']", $ns); - # $daggerNode.ParentNode.RemoveChild($daggerNode); - - # $safetyNetNode=$xml.SelectSingleNode(` - # "/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.GooglePlayServices.SafetyNet']", $ns); - # $safetyNetNode.ParentNode.RemoveChild($safetyNetNode); - - # $xml.Save($appPath); - - # Write-Output "########################################" - # Write-Output "##### Uninstall from Core.csproj" - # Write-Output "########################################" - - # $xml=New-Object XML; - # $xml.Load($corePath); - - # $appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']"); - # $appCenterNode.ParentNode.RemoveChild($appCenterNode); - - # $xml.Save($corePath); - - name: Restore packages run: dotnet restore @@ -811,7 +768,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@6fb7e99759b996fd142995636fd8c88c1fb8ecd9 # v1.16.1 + uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 5da9478ae..8424ca0e9 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -30,7 +30,7 @@ jobs: secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" - name: Download translations - uses: crowdin/github-action@6fb7e99759b996fd142995636fd8c88c1fb8ecd9 # v1.16.1 + uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95dc4f463..fbcfe24cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2.28.0 + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 with: workflow: build.yml workflow_conclusion: success @@ -76,7 +76,7 @@ jobs: - name: Dry Run - Download all artifacts if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2.28.0 + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 with: workflow: build.yml workflow_conclusion: success @@ -130,7 +130,7 @@ jobs: - name: Download F-Droid .apk artifact if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2.28.0 + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 with: workflow: build.yml workflow_conclusion: success @@ -139,7 +139,7 @@ jobs: - name: Dry Run - Download F-Droid .apk artifact if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2.28.0 + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 with: workflow: build.yml workflow_conclusion: success diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 92d7fe374..bb24c76e2 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -36,10 +36,19 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: main - repository: bitwarden/mobile + + - name: Check if RC branch exists + if: ${{ inputs.cut_rc_branch == true }} + run: | + remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) + if [[ "${remote_rc_branch_check}" -gt 0 ]]; then + echo "Remote RC branch exists." + echo "Please delete current RC branch before running again." + exit 1 + fi - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@d6f3f49f3345e29369fe57596a3ca8f94c4d2ca7 # v5.4.0 + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 with: gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} @@ -183,14 +192,22 @@ jobs: with: ref: main - - name: Check if RC branch exists + - name: Verify version has been updated + env: + NEW_VERSION: ${{ inputs.version_number }} run: | - remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) - if [[ "${remote_rc_branch_check}" -gt 0 ]]; then - echo "Remote RC branch exists." - echo "Please delete current RC branch before running again." - exit 1 - fi + CURRENT_VERSION=$(xmllint --xpath ' + string(/manifest/@*[local-name()="versionName" + and namespace-uri()="http://schemas.android.com/apk/res/android"]) + ' src/Android/Properties/AndroidManifest.xml) + + # Wait for version to change. + while [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] + do + echo "Waiting for version to be updated..." + sleep 10 + git pull --force + done - name: Cut RC branch run: | diff --git a/src/App/Platforms/Android/MainActivity.cs b/src/App/Platforms/Android/MainActivity.cs index 88fbceb8c..fe852fc7e 100644 --- a/src/App/Platforms/Android/MainActivity.cs +++ b/src/App/Platforms/Android/MainActivity.cs @@ -76,7 +76,8 @@ namespace Bit.Droid //We need to get and set the Options before calling OnCreate as that will "trigger" CreateWindow on App.xaml.cs _appOptions = GetOptions(); - ((Bit.App.App)Microsoft.Maui.Controls.Application.Current).SetOptions(_appOptions); + //This does not replace existing Options in App.xaml.cs if it exists already. It only updates properties in Options related with Autofill/CreateSend/etc.. + ((Bit.App.App)Microsoft.Maui.Controls.Application.Current).SetAndroidOptions(_appOptions); base.OnCreate(savedInstanceState); diff --git a/src/App/Platforms/iOS/AppDelegate.cs b/src/App/Platforms/iOS/AppDelegate.cs index c0a31b145..066eb54bc 100644 --- a/src/App/Platforms/iOS/AppDelegate.cs +++ b/src/App/Platforms/iOS/AppDelegate.cs @@ -460,21 +460,20 @@ namespace Bit.iOS _eventTimer?.Invalidate(); _eventTimer?.Dispose(); _eventTimer = null; - // TODO: Uncomment, this is just a test to see if this is causing the background crash on release when sending the app to background - //MainThread.BeginInvokeOnMainThread(() => - //{ - // try - // { - // _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => - // { - // _eventService?.UploadEventsAsync().FireAndForget(); - // }); - // } - // catch (Exception ex) - // { - // LoggerHelper.LogEvenIfCantBeResolved(ex); - // } - //}); + MainThread.BeginInvokeOnMainThread(() => + { + try + { + _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => + { + _eventService?.UploadEventsAsync().FireAndForget(); + }); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); } private async Task StopEventTimerAsync() @@ -484,20 +483,19 @@ namespace Bit.iOS _eventTimer?.Invalidate(); _eventTimer?.Dispose(); _eventTimer = null; - // TODO: Uncomment, this is just a test to see if this is causing the background crash on release when sending the app to background - //if (_eventBackgroundTaskId > 0) - //{ - // UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - // _eventBackgroundTaskId = 0; - //} - //_eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => - //{ - // UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - // _eventBackgroundTaskId = 0; - //}); - //await _eventService.UploadEventsAsync(); - //UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - //_eventBackgroundTaskId = 0; + if (_eventBackgroundTaskId > 0) + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + } + _eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + }); + await _eventService.UploadEventsAsync(); + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; } catch (Exception ex) { diff --git a/src/Core/App.xaml.cs b/src/Core/App.xaml.cs index 4f2fa1b3a..25b882d72 100644 --- a/src/Core/App.xaml.cs +++ b/src/Core/App.xaml.cs @@ -85,10 +85,27 @@ namespace Bit.App } } - //Allows setting Options from MainActivity before base.OnCreate - public void SetOptions(AppOptions appOptions) + /// + /// Allows setting Options from MainActivity before base.OnCreate + /// Note 1: This is only be used by Android due to way it's Lifecycle works + /// Note 2: This method does not replace existing Options in App.xaml.cs if it exists already. + /// It only updates properties in Options related with Autofill/CreateSend/etc.. + /// + /// Options created in Android MainActivity.cs + public void SetAndroidOptions(AppOptions appOptions) { - Options = appOptions ?? new AppOptions(); + if (Options == null) + { + Options = appOptions ?? new AppOptions(); + } + else if(appOptions != null) + { + Options.Uri = appOptions.Uri; + Options.MyVaultTile = appOptions.MyVaultTile; + Options.GeneratorTile = appOptions.GeneratorTile; + Options.FromAutofillFramework = appOptions.FromAutofillFramework; + Options.CreateSend = appOptions.CreateSend; + } } protected override Window CreateWindow(IActivationState activationState) diff --git a/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml b/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml index d84f941d1..99c56791c 100644 --- a/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml +++ b/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml @@ -1,5 +1,5 @@  - - + - - - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs b/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs index 297dc0e4b..2e3b4240a 100644 --- a/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs +++ b/src/Core/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs @@ -1,10 +1,14 @@ namespace Bit.App.Controls { - public partial class AuthenticatorViewCell : ExtendedGrid + public partial class AuthenticatorViewCell : BaseCipherViewCell { public AuthenticatorViewCell() { InitializeComponent(); } + + protected override CachedImage Icon => _iconImage; + + protected override IconLabel IconPlaceholder => _iconPlaceholderImage; } } diff --git a/src/Core/Controls/CipherViewCell/BaseCipherViewCell.cs b/src/Core/Controls/CipherViewCell/BaseCipherViewCell.cs new file mode 100644 index 000000000..15bd0dd7f --- /dev/null +++ b/src/Core/Controls/CipherViewCell/BaseCipherViewCell.cs @@ -0,0 +1,103 @@ +using Bit.App.Pages; + +namespace Bit.App.Controls +{ + public abstract class BaseCipherViewCell : ExtendedGrid + { + protected virtual CachedImage Icon { get; } + + protected virtual IconLabel IconPlaceholder { get; } + + // HACK: PM-5896 Fix for Background Crash on iOS + // While loading the cipher icon and the user sent the app to background + // the app was crashing sometimes when the "LoadingPlaceholder" or "ErrorPlaceholder" + // were being accessed, thus locked, and as soon the app got suspended by the OS + // the app would crash because there can't be any lock files by the app when it gets suspended. + // So, the approach has changed to reuse the IconLabel default icon to use it for these placeholders + // as well. In order to do that both icon controls change their visibility dynamically here reacting to + // CachedImage events and binding context changes. + + protected override void OnBindingContextChanged() + { + Icon.Source = null; + if (BindingContext is CipherItemViewModel cipherItemVM) + { + Icon.Source = cipherItemVM.IconImageSource; + if (!cipherItemVM.IconImageSuccesfullyLoaded) + { + UpdateIconImages(cipherItemVM.ShowIconImage); + } + } + + base.OnBindingContextChanged(); + } + + private void UpdateIconImages(bool showIcon) + { + MainThread.BeginInvokeOnMainThread(() => + { + if (!showIcon) + { + Icon.IsVisible = false; + IconPlaceholder.IsVisible = true; + return; + } + + IconPlaceholder.IsVisible = Icon.IsLoading; + }); + } + + public void Icon_Success(object sender, FFImageLoading.Maui.CachedImageEvents.SuccessEventArgs e) + { + if (BindingContext is CipherItemViewModel cipherItemVM) + { + cipherItemVM.IconImageSuccesfullyLoaded = true; + } + MainThread.BeginInvokeOnMainThread(() => + { + Icon.IsVisible = true; + IconPlaceholder.IsVisible = false; + }); + } + + public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e) + { + if (BindingContext is CipherItemViewModel cipherItemVM) + { + cipherItemVM.IconImageSuccesfullyLoaded = false; + } + MainThread.BeginInvokeOnMainThread(() => + { + Icon.IsVisible = false; + IconPlaceholder.IsVisible = true; + }); + } + } + + public class StubBaseCipherViewCellSoLinkerDoesntRemoveMethods : BaseCipherViewCell + { + protected override CachedImage Icon => new CachedImage(); + protected override IconLabel IconPlaceholder => new IconLabel(); + + public static void CallThisSoLinkerDoesntRemoveMethods() + { + var stub = new StubBaseCipherViewCellSoLinkerDoesntRemoveMethods(); + + try + { + stub.Icon_Success(stub, new FFImageLoading.Maui.CachedImageEvents.SuccessEventArgs(new FFImageLoading.Work.ImageInformation(), FFImageLoading.Work.LoadingResult.Disk)); + } + catch (Exception) + { + } + + try + { + stub.Icon_Error(stub, new FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs(new InvalidOperationException("stub"))); + } + catch (Exception) + { + } + } + } +} diff --git a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml index 6a492f47e..bbfdd41ee 100644 --- a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml +++ b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml @@ -1,5 +1,5 @@  - - - - + + @@ -121,4 +119,4 @@ SemanticProperties.Description="{u:I18n Options}" AutomationId="CipherOptionsButton" /> - \ No newline at end of file + \ No newline at end of file diff --git a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml.cs b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml.cs index bea341a46..fd163bce3 100644 --- a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml.cs +++ b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml.cs @@ -5,7 +5,7 @@ using Bit.Core.Utilities; namespace Bit.App.Controls { - public partial class CipherViewCell : ExtendedGrid + public partial class CipherViewCell : BaseCipherViewCell { private const int ICON_COLUMN_DEFAULT_WIDTH = 40; private const int ICON_IMAGE_DEFAULT_WIDTH = 22; @@ -23,6 +23,10 @@ namespace Bit.App.Controls _iconImage.HeightRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; } + protected override CachedImage Icon => _iconImage; + + protected override IconLabel IconPlaceholder => _iconPlaceholderImage; + public ICommand ButtonCommand { get => GetValue(ButtonCommandProperty) as ICommand; diff --git a/src/Core/MauiProgram.cs b/src/Core/MauiProgram.cs index 275e31470..43fdfd843 100644 --- a/src/Core/MauiProgram.cs +++ b/src/Core/MauiProgram.cs @@ -1,4 +1,5 @@ -using Camera.MAUI; +using Bit.App.Controls; +using Camera.MAUI; using CommunityToolkit.Maui; #if !UT using FFImageLoading.Maui; @@ -62,6 +63,13 @@ public static class MauiProgram builder.Logging.AddDebug(); #endif + ExplicitlyPreventThingsGetRemovedBecauseOfLinker(); + return builder; } + + private static void ExplicitlyPreventThingsGetRemovedBecauseOfLinker() + { + StubBaseCipherViewCellSoLinkerDoesntRemoveMethods.CallThisSoLinkerDoesntRemoveMethods(); + } } diff --git a/src/Core/Pages/Vault/CipherItemViewModel.cs b/src/Core/Pages/Vault/CipherItemViewModel.cs index 1ee850a23..28dee6780 100644 --- a/src/Core/Pages/Vault/CipherItemViewModel.cs +++ b/src/Core/Pages/Vault/CipherItemViewModel.cs @@ -38,5 +38,11 @@ namespace Bit.App.Pages return _iconImageSource; } } + + /// + /// Flag that indicates if FFImageLoading has successfully finished loading the image. + /// This is useful to check when the cell is being reused. + /// + public bool IconImageSuccesfullyLoaded { get; set; } } }