[EC-1002] [BEEEP] Add ability to change language in app (#2299)

* EC-1002 BEEEP Added ability to change language in app

* EC-1002 fix format

* EC-1002 Renamed IPreferencesStorageService to ISynchronousStorageService

* EC-1002 Moved get/set Locale to the StateService and added the StorageMediatorService to a new way to interact with the storage. Later the StateService will only interact with this mediator instead of directly with the storage services, with this we have more control inside the mediator and we can have both sync and async methods to interact with storages handled by the mediator
This commit is contained in:
Federico Maccaroni 2023-03-01 13:28:28 -03:00 committed by GitHub
parent 5164762f2e
commit 470e08f165
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 298 additions and 29 deletions

View file

@ -143,7 +143,8 @@ namespace Bit.Droid
var secureStorageService = new SecureStorageService(); var secureStorageService = new SecureStorageService();
var cryptoPrimitiveService = new CryptoPrimitiveService(); var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService); var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
var stateMigrationService = var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService); var clipboardService = new ClipboardService(stateService);
@ -158,6 +159,7 @@ namespace Bit.Droid
var cryptoService = new CryptoService(stateService, cryptoFunctionService); var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService); ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
ServiceContainer.Register<IMessagingService>("messagingService", messagingService); ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
ServiceContainer.Register<ILocalizeService>("localizeService", localizeService); ServiceContainer.Register<ILocalizeService>("localizeService", localizeService);
@ -165,6 +167,7 @@ namespace Bit.Droid
ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", cryptoPrimitiveService); ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", cryptoPrimitiveService);
ServiceContainer.Register<IStorageService>("storageService", mobileStorageService); ServiceContainer.Register<IStorageService>("storageService", mobileStorageService);
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService); ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
ServiceContainer.Register<IStorageMediatorService>(storageMediatorService);
ServiceContainer.Register<IStateService>("stateService", stateService); ServiceContainer.Register<IStateService>("stateService", stateService);
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService); ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService); ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
@ -197,7 +200,9 @@ namespace Bit.Droid
private void Bootstrap() private void Bootstrap()
{ {
(ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService).Init(); var locale = ServiceContainer.Resolve<IStateService>().GetLocale();
(ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService)
.Init(locale != null ? new System.Globalization.CultureInfo(locale) : null);
ServiceContainer.Resolve<IAuthService>("authService").Init(); ServiceContainer.Resolve<IAuthService>("authService").Init();
// Note: This is not awaited // Note: This is not awaited
var bootstrapTask = BootstrapAsync(); var bootstrapTask = BootstrapAsync();

View file

@ -45,11 +45,11 @@
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="http"/> <data android:scheme="http" />
</intent> </intent>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https"/> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View file

@ -80,6 +80,22 @@
Text="{u:I18n ClearClipboardDescription}" Text="{u:I18n ClearClipboardDescription}"
StyleClass="box-footer-label" /> StyleClass="box-footer-label" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n Language}"
StyleClass="box-label" />
<Picker
x:Name="_languagePicker"
ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
SelectedItem="{Binding SelectedLocale}"
ItemDisplayBinding="{Binding Value}"
StyleClass="box-value" />
</StackLayout>
<Label
Text="{u:I18n LanguageChangeRequiresAppRestart}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
<Label <Label

View file

@ -34,6 +34,7 @@ namespace Bit.App.Pages
_autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished); _autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished); _uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished); _clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_languagePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
@ -14,7 +15,8 @@ namespace Bit.App.Pages
{ {
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly II18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
private bool _autofillSavePrompt; private bool _autofillSavePrompt;
private string _autofillBlockedUris; private string _autofillBlockedUris;
@ -24,6 +26,7 @@ namespace Bit.App.Pages
private int _themeSelectedIndex; private int _themeSelectedIndex;
private int _autoDarkThemeSelectedIndex; private int _autoDarkThemeSelectedIndex;
private int _uriMatchSelectedIndex; private int _uriMatchSelectedIndex;
private KeyValuePair<string, string> _selectedLocale;
private bool _inited; private bool _inited;
private bool _updatingAutofill; private bool _updatingAutofill;
private bool _showAndroidAutofillSettings; private bool _showAndroidAutofillSettings;
@ -32,6 +35,8 @@ namespace Bit.App.Pages
{ {
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_i18nService = ServiceContainer.Resolve<II18nService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
PageTitle = AppResources.Options; PageTitle = AppResources.Options;
var iosIos = Device.RuntimePlatform == Device.iOS; var iosIos = Device.RuntimePlatform == Device.iOS;
@ -74,12 +79,18 @@ namespace Bit.App.Pages
new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact), new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never), new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never),
}; };
LocalesOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(null, AppResources.DefaultSystem)
};
LocalesOptions.AddRange(_i18nService.LocaleNames.ToList());
} }
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; } public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
public List<KeyValuePair<string, string>> ThemeOptions { get; set; } public List<KeyValuePair<string, string>> ThemeOptions { get; set; }
public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; } public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; }
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; } public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
public List<KeyValuePair<string, string>> LocalesOptions { get; }
public int ClearClipboardSelectedIndex public int ClearClipboardSelectedIndex
{ {
@ -133,6 +144,18 @@ namespace Bit.App.Pages
} }
} }
public KeyValuePair<string, string> SelectedLocale
{
get => _selectedLocale;
set
{
if (SetProperty(ref _selectedLocale, value))
{
UpdateCurrentLocaleAsync().FireAndForget();
}
}
}
public bool Favicon public bool Favicon
{ {
get => _favicon; get => _favicon;
@ -184,19 +207,30 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault(); AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null; AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false); AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
var theme = await _stateService.GetThemeAsync(); var theme = await _stateService.GetThemeAsync();
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme); ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark"; var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme); AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme);
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync(); var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
UriMatchSelectedIndex = defaultUriMatch == null ? 0 : UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch); UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
var clearClipboard = await _stateService.GetClearClipboardAsync(); var clearClipboard = await _stateService.GetClearClipboardAsync();
ClearClipboardSelectedIndex = ClearClipboardOptions.FindIndex(k => k.Key == clearClipboard); ClearClipboardSelectedIndex = ClearClipboardOptions.FindIndex(k => k.Key == clearClipboard);
var appLocale = _stateService.GetLocale();
SelectedLocale = appLocale == null ? LocalesOptions.First() : LocalesOptions.FirstOrDefault(kv => kv.Key == appLocale);
_inited = true; _inited = true;
} }
@ -288,5 +322,17 @@ namespace Bit.App.Pages
catch { } catch { }
} }
} }
private async Task UpdateCurrentLocaleAsync()
{
if (!_inited)
{
return;
}
_stateService.SetLocale(SelectedLocale.Key);
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, SelectedLocale.Value), AppResources.Language, AppResources.Ok);
}
} }
} }

View file

@ -1807,6 +1807,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Default (System).
/// </summary>
public static string DefaultSystem {
get {
return ResourceManager.GetString("DefaultSystem", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Default URI match detection. /// Looks up a localized string similar to Default URI match detection.
/// </summary> /// </summary>
@ -3373,6 +3382,33 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Language.
/// </summary>
public static string Language {
get {
return ResourceManager.GetString("Language", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Language change requires app restart.
/// </summary>
public static string LanguageChangeRequiresAppRestart {
get {
return ResourceManager.GetString("LanguageChangeRequiresAppRestart", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The language has been changed to {0}. Please restart the app to see the change.
/// </summary>
public static string LanguageChangeXDescription {
get {
return ResourceManager.GetString("LanguageChangeXDescription", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Last name. /// Looks up a localized string similar to Last name.
/// </summary> /// </summary>

View file

@ -2541,6 +2541,18 @@ Do you want to switch to this account?</value>
<data name="EnableCamerPermissionToUseTheScanner" xml:space="preserve"> <data name="EnableCamerPermissionToUseTheScanner" xml:space="preserve">
<value>Enable camera permission to use the scanner</value> <value>Enable camera permission to use the scanner</value>
</data> </data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="LanguageChangeXDescription" xml:space="preserve">
<value>The language has been changed to {0}. Please restart the app to see the change</value>
</data>
<data name="LanguageChangeRequiresAppRestart" xml:space="preserve">
<value>Language change requires app restart</value>
</data>
<data name="DefaultSystem" xml:space="preserve">
<value>Default (System)</value>
</data>
<data name="Important" xml:space="preserve"> <data name="Important" xml:space="preserve">
<value>Important</value> <value>Important</value>
</data> </data>

View file

@ -51,32 +51,32 @@ namespace Bit.App.Services
["bg"] = "български", ["bg"] = "български",
["ca"] = "català", ["ca"] = "català",
["cs"] = "čeština", ["cs"] = "čeština",
["da"] = "dansk", ["da"] = "Dansk",
["de"] = "Deutsch", ["de"] = "Deutsch",
["el"] = "Ελληνικά", ["el"] = "Ελληνικά",
["en"] = "English", ["en"] = "English",
["en-GB"] = "English (British)", ["en-GB"] = "English (British)",
["eo"] = "Esperanto", ["eo"] = "Esperanto",
["es"] = "español", ["es"] = "Español",
["et"] = "eesti", ["et"] = "eesti",
["fa"] = "فارسی", ["fa"] = "فارسی",
["fi"] = "suomi", ["fi"] = "suomi",
["fr"] = "français", ["fr"] = "Français",
["he"] = "עברית", ["he"] = "עברית",
["hi"] = "हिन्दी", ["hi"] = "हिन्दी",
["hr"] = "hrvatski", ["hr"] = "hrvatski",
["hu"] = "magyar", ["hu"] = "magyar",
["id"] = "Bahasa Indonesia", ["id"] = "Bahasa Indonesia",
["it"] = "italiano", ["it"] = "Italiano",
["ja"] = "日本語", ["ja"] = "日本語",
["ko"] = "한국어", ["ko"] = "한국어",
["lv"] = "Latvietis", ["lv"] = "Latvietis",
["ml"] = "മലയാളം", ["ml"] = "മലയാളം",
["nb"] = "norsk (bokmål)", ["nb"] = "norsk (bokmål)",
["nl"] = "Nederlands", ["nl"] = "Nederlands",
["pl"] = "polski", ["pl"] = "Polski",
["pt-BR"] = "português do Brasil", ["pt-BR"] = "Português do Brasil",
["pt-PT"] = "português", ["pt-PT"] = "Português",
["ro"] = "română", ["ro"] = "română",
["ru"] = "русский", ["ru"] = "русский",
["sk"] = "slovenčina", ["sk"] = "slovenčina",
@ -100,10 +100,16 @@ namespace Bit.App.Services
throw new Exception("I18n already inited."); throw new Exception("I18n already inited.");
} }
_inited = true; _inited = true;
SetCurrentCulture(culture);
}
public void SetCurrentCulture(CultureInfo culture)
{
if (culture != null) if (culture != null)
{ {
Culture = culture; Culture = culture;
} }
AppResources.Culture = Culture; AppResources.Culture = Culture;
Thread.CurrentThread.CurrentCulture = Culture; Thread.CurrentThread.CurrentCulture = Culture;
Thread.CurrentThread.CurrentUICulture = Culture; Thread.CurrentThread.CurrentUICulture = Culture;

View file

@ -6,7 +6,7 @@ using Newtonsoft.Json.Serialization;
namespace Bit.App.Services namespace Bit.App.Services
{ {
public class PreferencesStorageService : IStorageService public class PreferencesStorageService : IStorageService, ISynchronousStorageService
{ {
public static string KeyFormat = "bwPreferencesStorage:{0}"; public static string KeyFormat = "bwPreferencesStorage:{0}";
@ -22,57 +22,72 @@ namespace Bit.App.Services
_sharedName = sharedName; _sharedName = sharedName;
} }
public Task<T> GetAsync<T>(string key) public Task<T> GetAsync<T>(string key) => Task.FromResult(Get<T>(key));
public Task SaveAsync<T>(string key, T obj)
{
Save(key, obj);
return Task.CompletedTask;
}
public Task RemoveAsync(string key)
{
Remove(key);
return Task.CompletedTask;
}
public T Get<T>(string key)
{ {
var formattedKey = string.Format(KeyFormat, key); var formattedKey = string.Format(KeyFormat, key);
if (!Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) if (!Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName))
{ {
return Task.FromResult(default(T)); return default(T);
} }
var objType = typeof(T); var objType = typeof(T);
if (objType == typeof(string)) if (objType == typeof(string))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName);
return Task.FromResult((T)(object)val); return (T)(object)val;
} }
else if (objType == typeof(bool) || objType == typeof(bool?)) else if (objType == typeof(bool) || objType == typeof(bool?))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(bool), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(bool), _sharedName);
return Task.FromResult(ChangeType<T>(val)); return ChangeType<T>(val);
} }
else if (objType == typeof(int) || objType == typeof(int?)) else if (objType == typeof(int) || objType == typeof(int?))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(int), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(int), _sharedName);
return Task.FromResult(ChangeType<T>(val)); return ChangeType<T>(val);
} }
else if (objType == typeof(long) || objType == typeof(long?)) else if (objType == typeof(long) || objType == typeof(long?))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(long), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(long), _sharedName);
return Task.FromResult(ChangeType<T>(val)); return ChangeType<T>(val);
} }
else if (objType == typeof(double) || objType == typeof(double?)) else if (objType == typeof(double) || objType == typeof(double?))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(double), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(double), _sharedName);
return Task.FromResult(ChangeType<T>(val)); return ChangeType<T>(val);
} }
else if (objType == typeof(DateTime) || objType == typeof(DateTime?)) else if (objType == typeof(DateTime) || objType == typeof(DateTime?))
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(DateTime), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(DateTime), _sharedName);
return Task.FromResult(ChangeType<T>(val)); return ChangeType<T>(val);
} }
else else
{ {
var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName);
return Task.FromResult(JsonConvert.DeserializeObject<T>(val, _jsonSettings)); return JsonConvert.DeserializeObject<T>(val, _jsonSettings);
} }
} }
public Task SaveAsync<T>(string key, T obj) public void Save<T>(string key, T obj)
{ {
if (obj == null) if (obj == null)
{ {
return RemoveAsync(key); Remove(key);
return;
} }
var formattedKey = string.Format(KeyFormat, key); var formattedKey = string.Format(KeyFormat, key);
@ -106,17 +121,15 @@ namespace Bit.App.Services
Xamarin.Essentials.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings), Xamarin.Essentials.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings),
_sharedName); _sharedName);
} }
return Task.FromResult(0);
} }
public Task RemoveAsync(string key) public void Remove(string key)
{ {
var formattedKey = string.Format(KeyFormat, key); var formattedKey = string.Format(KeyFormat, key);
if (Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) if (Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName))
{ {
Xamarin.Essentials.Preferences.Remove(formattedKey, _sharedName); Xamarin.Essentials.Preferences.Remove(formattedKey, _sharedName);
} }
return Task.FromResult(0);
} }
private static T ChangeType<T>(object value) private static T ChangeType<T>(object value)

View file

@ -9,6 +9,7 @@ namespace Bit.Core.Abstractions
CultureInfo Culture { get; set; } CultureInfo Culture { get; set; }
StringComparer StringComparer { get; } StringComparer StringComparer { get; }
Dictionary<string, string> LocaleNames { get; } Dictionary<string, string> LocaleNames { get; }
void SetCurrentCulture(CultureInfo culture);
string T(string id, string p1 = null, string p2 = null, string p3 = null); string T(string id, string p1 = null, string p2 = null, string p3 = null);
string Translate(string id, string p1 = null, string p2 = null, string p3 = null); string Translate(string id, string p1 = null, string p2 = null, string p3 = null);
} }

View file

@ -6,6 +6,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Services;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
@ -165,5 +166,7 @@ namespace Bit.Core.Abstractions
Task<string> GetAvatarColorAsync(string userId = null); Task<string> GetAvatarColorAsync(string userId = null);
Task<string> GetPreLoginEmailAsync(); Task<string> GetPreLoginEmailAsync();
Task SetPreLoginEmailAsync(string value); Task SetPreLoginEmailAsync(string value);
string GetLocale();
void SetLocale(string locale);
} }
} }

View file

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Bit.Core.Services;
namespace Bit.Core.Abstractions
{
public interface IStorageMediatorService
{
T Get<T>(string key);
void Save<T>(string key, T obj);
void Remove(string key);
Task<T> GetAsync<T>(string key, StorageMediatorOptions options = default);
Task SaveAsync<T>(string key, T obj, StorageMediatorOptions options = default);
Task RemoveAsync(string key, StorageMediatorOptions options = default);
}
}

View file

@ -0,0 +1,9 @@
namespace Bit.Core.Abstractions
{
public interface ISynchronousStorageService
{
T Get<T>(string key);
void Save<T>(string key, T obj);
void Remove(string key);
}
}

View file

@ -46,6 +46,7 @@
/// which is used to handle Apple Watch state logic /// which is used to handle Apple Watch state logic
/// </summary> /// </summary>
public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch"; public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch";
public const string AppLocaleKey = "appLocale";
public const int SelectFileRequestCode = 42; public const int SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43; public const int SelectFilePermissionRequestCode = 43;
public const int SaveFileRequestCode = 44; public const int SaveFileRequestCode = 44;

View file

@ -14,8 +14,11 @@ namespace Bit.Core.Services
{ {
public class StateService : IStateService public class StateService : IStateService
{ {
// TODO: Refactor this removing all storage services and use the IStorageMediatorService instead
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly IStorageService _secureStorageService; private readonly IStorageService _secureStorageService;
private readonly IStorageMediatorService _storageMediatorService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private State _state; private State _state;
@ -25,10 +28,12 @@ namespace Bit.Core.Services
public StateService(IStorageService storageService, public StateService(IStorageService storageService,
IStorageService secureStorageService, IStorageService secureStorageService,
IStorageMediatorService storageMediatorService,
IMessagingService messagingService) IMessagingService messagingService)
{ {
_storageService = storageService; _storageService = storageService;
_secureStorageService = secureStorageService; _secureStorageService = secureStorageService;
_storageMediatorService = storageMediatorService;
_messagingService = messagingService; _messagingService = messagingService;
} }
@ -891,6 +896,16 @@ namespace Bit.Core.Services
await SetValueAsync(Constants.AutoDarkThemeKey, value, await GetDefaultStorageOptionsAsync()); await SetValueAsync(Constants.AutoDarkThemeKey, value, await GetDefaultStorageOptionsAsync());
} }
public string GetLocale()
{
return _storageMediatorService.Get<string>(Constants.AppLocaleKey);
}
public void SetLocale(string locale)
{
_storageMediatorService.Save(Constants.AppLocaleKey, locale);
}
public async Task<bool?> GetAddSitePromptShownAsync(string userId = null) public async Task<bool?> GetAddSitePromptShownAsync(string userId = null)
{ {
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
@ -1222,11 +1237,13 @@ namespace Bit.Core.Services
// Helpers // Helpers
[Obsolete("Use IStorageMediatorService instead")]
private async Task<T> GetValueAsync<T>(string key, StorageOptions options) private async Task<T> GetValueAsync<T>(string key, StorageOptions options)
{ {
return await GetStorageService(options).GetAsync<T>(key); return await GetStorageService(options).GetAsync<T>(key);
} }
[Obsolete("Use IStorageMediatorService instead")]
private async Task SetValueAsync<T>(string key, T value, StorageOptions options) private async Task SetValueAsync<T>(string key, T value, StorageOptions options)
{ {
if (value == null) if (value == null)
@ -1237,11 +1254,17 @@ namespace Bit.Core.Services
await GetStorageService(options).SaveAsync(key, value); await GetStorageService(options).SaveAsync(key, value);
} }
[Obsolete("Use IStorageMediatorService instead")]
private IStorageService GetStorageService(StorageOptions options) private IStorageService GetStorageService(StorageOptions options)
{ {
return options.UseSecureStorage.GetValueOrDefault(false) ? _secureStorageService : _storageService; return options.UseSecureStorage.GetValueOrDefault(false) ? _secureStorageService : _storageService;
} }
private async Task<string> ComposeKeyAsync(Func<string, string> userDependantKey, string userId = null)
{
return userDependantKey(userId ?? await GetActiveUserIdAsync());
}
private async Task<Account> GetAccountAsync(StorageOptions options) private async Task<Account> GetAccountAsync(StorageOptions options)
{ {
await CheckStateAsync(); await CheckStateAsync();
@ -1437,6 +1460,12 @@ namespace Bit.Core.Services
return requestedOptions; return requestedOptions;
} }
/// <summary>
/// Gets the default options for storage.
/// If it's only used for composing the constant key with the user id
/// then use <see cref="ComposeKeyAsync(Func{string, string}, string)"/> instead
/// which saves time if the user id is already known
/// </summary>
private async Task<StorageOptions> GetDefaultStorageOptionsAsync() private async Task<StorageOptions> GetDefaultStorageOptionsAsync()
{ {
return new StorageOptions() return new StorageOptions()

View file

@ -0,0 +1,8 @@
namespace Bit.Core.Services
{
public struct StorageMediatorOptions
{
public bool UseSecureStorage { get; set; }
public bool AllowSaveNull { get; set; }
}
}

View file

@ -0,0 +1,62 @@
using System.Threading.Tasks;
using Bit.Core.Abstractions;
namespace Bit.Core.Services
{
public class StorageMediatorService : IStorageMediatorService
{
private readonly IStorageService _storageService;
private readonly IStorageService _secureStorageService;
private readonly ISynchronousStorageService _synchronousStorageService;
public StorageMediatorService(IStorageService storageService,
IStorageService secureStorageService,
ISynchronousStorageService synchronousStorageService)
{
_storageService = storageService;
_secureStorageService = secureStorageService;
_synchronousStorageService = synchronousStorageService;
}
public T Get<T>(string key)
{
return _synchronousStorageService.Get<T>(key);
}
public void Save<T>(string key, T obj)
{
_synchronousStorageService.Save<T>(key, obj);
}
public void Remove(string key)
{
_synchronousStorageService.Remove(key);
}
public Task<T> GetAsync<T>(string key, StorageMediatorOptions options = default)
{
return GetAsyncStorage(options).GetAsync<T>(key);
}
public async Task SaveAsync<T>(string key, T obj, StorageMediatorOptions options = default)
{
if (obj is null && !options.AllowSaveNull)
{
await GetAsyncStorage(options).RemoveAsync(key);
return;
}
await GetAsyncStorage(options).SaveAsync<T>(key, obj);
}
public Task RemoveAsync(string key, StorageMediatorOptions options = default)
{
return GetAsyncStorage(options).RemoveAsync(key);
}
IStorageService GetAsyncStorage(StorageMediatorOptions options)
{
return options.UseSecureStorage ? _secureStorageService : _storageService;
}
}
}

View file

@ -102,7 +102,8 @@ namespace Bit.iOS.Core.Utilities
() => ServiceContainer.Resolve<IAppIdService>("appIdService").GetAppIdAsync()); () => ServiceContainer.Resolve<IAppIdService>("appIdService").GetAppIdAsync());
var cryptoPrimitiveService = new CryptoPrimitiveService(); var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService); var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
var stateMigrationService = var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var deviceActionService = new DeviceActionService(); var deviceActionService = new DeviceActionService();
@ -115,6 +116,7 @@ namespace Bit.iOS.Core.Utilities
var cryptoService = new CryptoService(stateService, cryptoFunctionService); var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService); ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
ServiceContainer.Register<IMessagingService>("messagingService", messagingService); ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
ServiceContainer.Register<ILocalizeService>("localizeService", localizeService); ServiceContainer.Register<ILocalizeService>("localizeService", localizeService);
@ -122,6 +124,7 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", cryptoPrimitiveService); ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", cryptoPrimitiveService);
ServiceContainer.Register<IStorageService>("storageService", mobileStorageService); ServiceContainer.Register<IStorageService>("storageService", mobileStorageService);
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService); ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
ServiceContainer.Register<IStorageMediatorService>(storageMediatorService);
ServiceContainer.Register<IStateService>("stateService", stateService); ServiceContainer.Register<IStateService>("stateService", stateService);
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService); ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService); ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
@ -146,7 +149,9 @@ namespace Bit.iOS.Core.Utilities
public static void Bootstrap(Func<Task> postBootstrapFunc = null) public static void Bootstrap(Func<Task> postBootstrapFunc = null)
{ {
(ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService).Init(); var locale = ServiceContainer.Resolve<IStateService>().GetLocale();
(ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService)
.Init(locale != null ? new System.Globalization.CultureInfo(locale) : null);
ServiceContainer.Resolve<IAuthService>("authService").Init(); ServiceContainer.Resolve<IAuthService>("authService").Init();
(ServiceContainer. (ServiceContainer.
Resolve<IPlatformUtilsService>("platformUtilsService") as MobilePlatformUtilsService).Init(); Resolve<IPlatformUtilsService>("platformUtilsService") as MobilePlatformUtilsService).Init();