diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index c5f838ff8..8561fb4f1 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -233,6 +233,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Android/Renderers/CustomLabelRenderer.cs b/src/Android/Renderers/CustomLabelRenderer.cs
index 62287087d..838b9b967 100644
--- a/src/Android/Renderers/CustomLabelRenderer.cs
+++ b/src/Android/Renderers/CustomLabelRenderer.cs
@@ -1,10 +1,10 @@
-using System;
-using Bit.App.Controls;
-using System.ComponentModel;
-using Xamarin.Forms.Platform.Android;
+using System.ComponentModel;
using Android.Content;
-using Xamarin.Forms;
+using Android.OS;
+using Bit.App.Controls;
using Bit.Droid.Renderers;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
namespace Bit.Droid.Renderers
@@ -15,6 +15,19 @@ namespace Bit.Droid.Renderers
: base(context)
{ }
+ protected override void OnElementChanged(ElementChangedEventArgs
@@ -442,5 +443,6 @@
+
diff --git a/src/App/Controls/CustomLabel.cs b/src/App/Controls/CustomLabel.cs
index e822d3304..77d1fda79 100644
--- a/src/App/Controls/CustomLabel.cs
+++ b/src/App/Controls/CustomLabel.cs
@@ -1,5 +1,4 @@
-using System;
-using Xamarin.Forms;
+using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -8,6 +7,7 @@ namespace Bit.App.Controls
public CustomLabel()
{
}
+
+ public int? FontWeight { get; set; }
}
}
-
diff --git a/src/App/Pages/Settings/BlockAutofillUrisPage.xaml b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml
new file mode 100644
index 000000000..8514edcad
--- /dev/null
+++ b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
new file mode 100644
index 000000000..e8bf3b0cd
--- /dev/null
+++ b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using Bit.App.Styles;
+using Bit.App.Utilities;
+using Bit.Core.Utilities;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Bit.App.Pages
+{
+ public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
+ {
+ private readonly BlockAutofillUrisPageViewModel _vm;
+
+ public BlockAutofillUrisPage()
+ {
+ InitializeComponent();
+
+ _vm = BindingContext as BlockAutofillUrisPageViewModel;
+ _vm.Page = this;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ _vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
+
+ UpdatePlaceholder();
+ }
+
+ public override async Task UpdateOnThemeChanged()
+ {
+ await base.UpdateOnThemeChanged();
+
+ UpdatePlaceholder();
+ }
+
+ private void UpdatePlaceholder()
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ _emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
+ }
+ }
+}
diff --git a/src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs b/src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
new file mode 100644
index 000000000..a3f823990
--- /dev/null
+++ b/src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Bit.App.Abstractions;
+using Bit.App.Resources;
+using Bit.Core;
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
+using Xamarin.CommunityToolkit.ObjectModel;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Bit.App.Pages
+{
+ public class BlockAutofillUrisPageViewModel : BaseViewModel
+ {
+ private const char URI_SEPARARTOR = ',';
+ private const string URI_FORMAT = "https://domain.com";
+
+ private readonly IStateService _stateService;
+ private readonly IDeviceActionService _deviceActionService;
+
+ public BlockAutofillUrisPageViewModel()
+ {
+ _stateService = ServiceContainer.Resolve();
+ _deviceActionService = ServiceContainer.Resolve();
+
+ AddUriCommand = new AsyncCommand(AddUriAsync,
+ onException: ex => HandleException(ex),
+ allowsMultipleExecutions: false);
+
+ EditUriCommand = new AsyncCommand(EditUriAsync,
+ onException: ex => HandleException(ex),
+ allowsMultipleExecutions: false);
+ }
+
+ public ObservableRangeCollection BlockedUris { get; set; } = new ObservableRangeCollection();
+
+ public bool ShowList => BlockedUris.Any();
+
+ public ICommand AddUriCommand { get; }
+
+ public ICommand EditUriCommand { get; }
+
+ public async Task InitAsync()
+ {
+ var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
+ if (blockedUrisList?.Any() != true)
+ {
+ return;
+ }
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
+ TriggerPropertyChanged(nameof(ShowList));
+ });
+ }
+
+ private async Task AddUriAsync()
+ {
+ var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
+ {
+ Title = AppResources.NewUri,
+ Subtitle = AppResources.EnterURI,
+ ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
+ OkButtonText = AppResources.Save,
+ ValidateText = text => ValidateUris(text, true)
+ });
+ if (response?.Text is null)
+ {
+ return;
+ }
+
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
+ {
+ var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
+ BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
+ }
+
+ BlockedUris = new ObservableRangeCollection(BlockedUris.OrderBy(b => b.Uri));
+ TriggerPropertyChanged(nameof(BlockedUris));
+ TriggerPropertyChanged(nameof(ShowList));
+ });
+ await UpdateAutofillBlacklistedUrisAsync();
+ _deviceActionService.Toast(AppResources.URISaved);
+ }
+
+ private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
+ {
+ var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
+ {
+ Title = AppResources.EditURI,
+ Subtitle = AppResources.EnterURI,
+ Text = uriItemViewModel.Uri,
+ ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
+ OkButtonText = AppResources.Save,
+ ThirdButtonText = AppResources.Remove,
+ ValidateText = text => ValidateUris(text, false)
+ });
+ if (response is null)
+ {
+ return;
+ }
+
+ if (response.Value.ExecuteThirdAction)
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ BlockedUris.Remove(uriItemViewModel);
+ TriggerPropertyChanged(nameof(ShowList));
+ });
+ await UpdateAutofillBlacklistedUrisAsync();
+ _deviceActionService.Toast(AppResources.URIRemoved);
+ return;
+ }
+
+ var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ BlockedUris.Remove(uriItemViewModel);
+ BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
+ BlockedUris = new ObservableRangeCollection(BlockedUris.OrderBy(b => b.Uri));
+ TriggerPropertyChanged(nameof(BlockedUris));
+ TriggerPropertyChanged(nameof(ShowList));
+ });
+ await UpdateAutofillBlacklistedUrisAsync();
+ _deviceActionService.Toast(AppResources.URISaved);
+ }
+
+ private string ValidateUris(string uris, bool allowMultipleUris)
+ {
+ if (string.IsNullOrWhiteSpace(uris))
+ {
+ return string.Format(AppResources.FormatX, URI_FORMAT);
+ }
+
+ if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
+ {
+ return AppResources.CannotEditMultipleURIsAtOnce;
+ }
+
+ foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
+ {
+ var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
+ if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
+ !cleanedUri.StartsWith(Constants.AndroidAppProtocol))
+ {
+ return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
+ }
+
+ if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
+ {
+ return AppResources.InvalidURI;
+ }
+
+ if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
+ {
+ return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
+ }
+ }
+
+ return null;
+ }
+
+ private async Task UpdateAutofillBlacklistedUrisAsync()
+ {
+ await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
+ }
+ }
+
+ public class BlockAutofillUriItemViewModel : ExtendedViewModel
+ {
+ public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
+ {
+ Uri = uri;
+ EditUriCommand = new Command(() => editUriCommand.Execute(this));
+ }
+
+ public string Uri { get; }
+
+ public ICommand EditUriCommand { get; }
+ }
+}
diff --git a/src/App/Pages/Settings/OptionsPage.xaml b/src/App/Pages/Settings/OptionsPage.xaml
index 8a6a407b5..39901574d 100644
--- a/src/App/Pages/Settings/OptionsPage.xaml
+++ b/src/App/Pages/Settings/OptionsPage.xaml
@@ -153,22 +153,14 @@
StyleClass="box-footer-label, box-footer-label-switch" />
-
-
-
-
+
+
+
+
diff --git a/src/App/Pages/Settings/OptionsPage.xaml.cs b/src/App/Pages/Settings/OptionsPage.xaml.cs
index 6cb54d21d..63c02605c 100644
--- a/src/App/Pages/Settings/OptionsPage.xaml.cs
+++ b/src/App/Pages/Settings/OptionsPage.xaml.cs
@@ -1,6 +1,4 @@
-using Bit.App.Abstractions;
-using Bit.App.Resources;
-using Bit.Core.Abstractions;
+using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
@@ -44,17 +42,6 @@ namespace Bit.App.Pages
await _vm.InitAsync();
}
- protected async override void OnDisappearing()
- {
- base.OnDisappearing();
- await _vm.UpdateAutofillBlockedUris();
- }
-
- private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
- {
- await _vm.UpdateAutofillBlockedUris();
- }
-
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
diff --git a/src/App/Pages/Settings/OptionsPageViewModel.cs b/src/App/Pages/Settings/OptionsPageViewModel.cs
index ea36aad9c..8e0cbbc1d 100644
--- a/src/App/Pages/Settings/OptionsPageViewModel.cs
+++ b/src/App/Pages/Settings/OptionsPageViewModel.cs
@@ -1,12 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
-using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
+using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private bool _autofillSavePrompt;
- private string _autofillBlockedUris;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
@@ -84,6 +84,10 @@ namespace Bit.App.Pages
new KeyValuePair(null, AppResources.DefaultSystem)
};
LocalesOptions.AddRange(_i18nService.LocaleNames.ToList());
+
+ GoToBlockAutofillUrisCommand = new AsyncCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()),
+ onException: ex => HandleException(ex),
+ allowsMultipleExecutions: false);
}
public List> ClearClipboardOptions { get; set; }
@@ -192,25 +196,18 @@ namespace Bit.App.Pages
}
}
- public string AutofillBlockedUris
- {
- get => _autofillBlockedUris;
- set => SetProperty(ref _autofillBlockedUris, value);
- }
-
public bool ShowAndroidAutofillSettings
{
get => _showAndroidAutofillSettings;
set => SetProperty(ref _showAndroidAutofillSettings, value);
}
+ public ICommand GoToBlockAutofillUrisCommand { get; }
+
public async Task InitAsync()
{
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
- var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
- AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
-
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
@@ -288,41 +285,6 @@ namespace Bit.App.Pages
}
}
- public async Task UpdateAutofillBlockedUris()
- {
- if (_inited)
- {
- if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
- {
- await _stateService.SetAutofillBlacklistedUrisAsync(null);
- AutofillBlockedUris = null;
- return;
- }
- try
- {
- var csv = AutofillBlockedUris;
- var urisList = new List();
- foreach (var uri in csv.Split(','))
- {
- if (string.IsNullOrWhiteSpace(uri))
- {
- continue;
- }
- var cleanedUri = uri.Replace(System.Environment.NewLine, string.Empty).Trim();
- if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
- !cleanedUri.StartsWith(Constants.AndroidAppProtocol))
- {
- continue;
- }
- urisList.Add(cleanedUri);
- }
- await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
- AutofillBlockedUris = string.Join(", ", urisList);
- }
- catch { }
- }
- }
-
private async Task UpdateCurrentLocaleAsync()
{
if (!_inited)
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 82bb07c83..fedf8ac38 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -796,15 +796,6 @@ namespace Bit.App.Resources {
}
}
- ///
- /// Looks up a localized string similar to Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android"..
- ///
- public static string AutofillBlockedUrisDescription {
- get {
- return ResourceManager.GetString("AutofillBlockedUrisDescription", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Do you want to auto-fill or view this item?.
///
@@ -940,6 +931,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Auto-fill will not be offered for these URIs..
+ ///
+ public static string AutoFillWillNotBeOfferedForTheseURIs {
+ get {
+ return ResourceManager.GetString("AutoFillWillNotBeOfferedForTheseURIs", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Auto-fill with Bitwarden.
///
@@ -1228,6 +1228,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Block auto-fill.
+ ///
+ public static string BlockAutoFill {
+ get {
+ return ResourceManager.GetString("BlockAutoFill", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Brand.
///
@@ -1264,6 +1273,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Cannot edit multiple URIs at once.
+ ///
+ public static string CannotEditMultipleURIsAtOnce {
+ get {
+ return ResourceManager.GetString("CannotEditMultipleURIsAtOnce", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Cannot open the app "{0}"..
///
@@ -2146,6 +2164,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Edit URI.
+ ///
+ public static string EditURI {
+ get {
+ return ResourceManager.GetString("EditURI", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Email.
///
@@ -2299,6 +2326,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Enter URI.
+ ///
+ public static string EnterURI {
+ get {
+ return ResourceManager.GetString("EnterURI", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Enter the 6 digit verification code from your authenticator app..
///
@@ -2947,6 +2983,24 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Format: {0}.
+ ///
+ public static string FormatX {
+ get {
+ return ResourceManager.GetString("FormatX", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Format: {0}. Separate multiple URIs with a comma..
+ ///
+ public static string FormatXSeparateMultipleURIsWithAComma {
+ get {
+ return ResourceManager.GetString("FormatXSeparateMultipleURIsWithAComma", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Forwarded email alias.
///
@@ -3289,6 +3343,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Invalid format. Use https://, http://, or androidapp://.
+ ///
+ public static string InvalidFormatUseHttpsHttpOrAndroidApp {
+ get {
+ return ResourceManager.GetString("InvalidFormatUseHttpsHttpOrAndroidApp", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Invalid master password. Try again..
///
@@ -3307,6 +3370,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Invalid URI.
+ ///
+ public static string InvalidURI {
+ get {
+ return ResourceManager.GetString("InvalidURI", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Invalid verification code.
///
@@ -4218,6 +4290,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to New blocked URI.
+ ///
+ public static string NewBlockedURI {
+ get {
+ return ResourceManager.GetString("NewBlockedURI", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to New custom field.
///
@@ -6119,6 +6200,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to There are no blocked URIs.
+ ///
+ public static string ThereAreNoBlockedURIs {
+ get {
+ return ResourceManager.GetString("ThereAreNoBlockedURIs", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to There are no items in your vault that match "{0}".
///
@@ -6137,6 +6227,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to The URI {0} is already blocked.
+ ///
+ public static string TheURIXIsAlreadyBlocked {
+ get {
+ return ResourceManager.GetString("TheURIXIsAlreadyBlocked", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to 30 days.
///
@@ -6587,6 +6686,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to URI removed.
+ ///
+ public static string URIRemoved {
+ get {
+ return ResourceManager.GetString("URIRemoved", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to URIs.
///
@@ -6596,6 +6704,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to URI saved.
+ ///
+ public static string URISaved {
+ get {
+ return ResourceManager.GetString("URISaved", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to US.
///
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 9b10e05a3..7ad6718f6 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -1585,9 +1585,6 @@ Scanning will happen automatically.
Auto-fill blocked URIs
-
- Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android".
-
Ask to add login
@@ -2643,4 +2640,50 @@ Do you want to switch to this account?
Invalid API token
+
+ Block auto-fill
+
+
+ Auto-fill will not be offered for these URIs.
+
+
+ New blocked URI
+
+
+ URI saved
+
+
+ Invalid format. Use https://, http://, or androidapp://
+ https://, http://, androidapp:// should not be translated
+
+
+ Edit URI
+
+
+ Enter URI
+
+
+ Format: {0}. Separate multiple URIs with a comma.
+
+
+ Format: {0}
+
+
+ Invalid URI
+
+
+ URI saved
+
+
+ URI removed
+
+
+ There are no blocked URIs
+
+
+ The URI {0} is already blocked
+
+
+ Cannot edit multiple URIs at once
+
diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml
index 1d74ba518..2dac11bf7 100644
--- a/src/App/Styles/Base.xaml
+++ b/src/App/Styles/Base.xaml
@@ -428,6 +428,22 @@
+