From e116f46982ce747b767a0a5c912504a135ab787e Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Tue, 10 Jul 2018 18:04:28 -0700 Subject: [PATCH 001/254] Remove old circle.yml file --- circle.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 circle.yml diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 7cfe8890..00000000 --- a/circle.yml +++ /dev/null @@ -1,22 +0,0 @@ -dependencies: - pre: - - curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg - - sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg - - sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main" > /etc/apt/sources.list.d/dotnetdev.list' - - sudo apt-get install apt-transport-https - - sudo apt-get update - - sudo apt-get install dotnet-sdk-2.0.0 - - sudo apt-get install xsltproc - override: - - dotnet restore - - dotnet build src/LaunchDarkly.Common -f netstandard1.6 - - dotnet build src/LaunchDarkly.Common -f netstandard2.0 - - dotnet build src/LaunchDarkly.Xamarin -f netstandard1.6 - - dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 -test: - override: - - dotnet add tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj package dotnet-xunit - - dotnet restore - - mkdir -p $CIRCLE_TEST_REPORTS/junit - - cd tests/LaunchDarkly.Xamarin.Tests; dotnet xunit -xml xunit.xml - - xsltproc -o $CIRCLE_TEST_REPORTS/junit/junit.xml tests/LaunchDarkly.Xamarin.Tests/xunit-to-junit.xslt tests/LaunchDarkly.Xamarin.Tests/xunit.xml From a03fa0d1b37b01ba7faca546f348f393393fe8a0 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Thu, 12 Jul 2018 11:24:57 -0700 Subject: [PATCH 002/254] Send back flagVersion in events when it is present (#5) Also allow users to have empty string keys --- src/LaunchDarkly.Xamarin/FeatureFlag.cs | 48 ++-- src/LaunchDarkly.Xamarin/LdClient.cs | 12 +- .../FeatureFlagTests.cs | 29 +++ .../FlagCacheManagerTests.cs | 212 +++++++++--------- .../LdClientTests.cs | 86 +++---- 5 files changed, 210 insertions(+), 177 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlag.cs b/src/LaunchDarkly.Xamarin/FeatureFlag.cs index 7d847c02..bd37ecbf 100644 --- a/src/LaunchDarkly.Xamarin/FeatureFlag.cs +++ b/src/LaunchDarkly.Xamarin/FeatureFlag.cs @@ -1,25 +1,27 @@ using System; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using LaunchDarkly.Common; - -namespace LaunchDarkly.Xamarin -{ - public class FeatureFlag : IEquatable - { - public JToken value; - public int version; - public bool trackEvents; - public int? variation; - public long? debugEventsUntilDate; - - public bool Equals(FeatureFlag otherFlag) - { + +namespace LaunchDarkly.Xamarin +{ + public class FeatureFlag : IEquatable + { + public JToken value; + public int version; + public int? flagVersion; + public bool trackEvents; + public int? variation; + public long? debugEventsUntilDate; + + public bool Equals(FeatureFlag otherFlag) + { return JToken.DeepEquals(value, otherFlag.value) && version == otherFlag.version + && flagVersion == otherFlag.flagVersion && trackEvents == otherFlag.trackEvents && variation == otherFlag.variation - && debugEventsUntilDate == otherFlag.debugEventsUntilDate; - } + && debugEventsUntilDate == otherFlag.debugEventsUntilDate; + } } internal class FeatureFlagEvent : IFlagEventProperties @@ -39,10 +41,10 @@ public FeatureFlagEvent(string key, FeatureFlag featureFlag) _featureFlag = featureFlag; _key = key; } - - public string Key => _key; - public int Version => _featureFlag.version; - public bool TrackEvents => _featureFlag.trackEvents; - public long? DebugEventsUntilDate => _featureFlag.debugEventsUntilDate; - } -} + + public string Key => _key; + public int Version => _featureFlag.flagVersion ?? _featureFlag.version; + public bool TrackEvents => _featureFlag.trackEvents; + public long? DebugEventsUntilDate => _featureFlag.debugEventsUntilDate; + } +} diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1c8261f8..d7824bdc 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -67,8 +67,8 @@ public sealed class LdClient : ILdMobileClient deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - // If you pass in a null user or user with an empty key, one will be assigned to them. - if (user == null || String.IsNullOrEmpty(user.Key)) + // If you pass in a null user or user with a null key, one will be assigned to them. + if (user == null || user.Key == null) { User = UserWithUniqueKey(user); } @@ -427,7 +427,7 @@ public async Task IdentifyAsync(User user) } User userWithKey = null; - if (String.IsNullOrEmpty(user.Key)) + if (user.Key == null) { userWithKey = UserWithUniqueKey(user); } @@ -503,11 +503,11 @@ void IDisposable.Dispose() void Dispose(bool disposing) { - if (disposing) + if (disposing) { - Log.InfoFormat("The mobile client is being disposed"); + Log.InfoFormat("The mobile client is being disposed"); updateProcessor.Dispose(); - eventProcessor.Dispose(); + eventProcessor.Dispose(); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs new file mode 100644 index 00000000..bd990c54 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class FeatureFlagEventTests + { + [Fact] + public void ReturnsFlagVersionAsVersion() + { + var flag = new FeatureFlag(); + flag.flagVersion = 123; + flag.version = 456; + var flagEvent = new FeatureFlagEvent("my-flag", flag); + Assert.Equal(123, flagEvent.Version); + } + + [Fact] + public void FallsBackToVersionAsVersion() + { + var flag = new FeatureFlag(); + flag.version = 456; + var flagEvent = new FeatureFlagEvent("my-flag", flag); + Assert.Equal(456, flagEvent.Version); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs index c326ceed..3b0df506 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs @@ -1,106 +1,106 @@ -using System; -using LaunchDarkly.Client; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class FlagCacheManagerTests - { - IUserFlagCache deviceCache = new UserFlagInMemoryCache(); - IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); - FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); - - User user = User.WithKey("someKey"); - - IFlagCacheManager ManagerWithCachedFlags() - { - var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); - flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); - return flagCacheManager; - } - - [Fact] - public void CacheFlagsShouldStoreFlagsInDeviceCache() - { - var flagCacheManager = ManagerWithCachedFlags(); - var cachedDeviceFlags = deviceCache.RetrieveFlags(user); - Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); - Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); - } - - [Fact] - public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() - { - var flagCacheManager = ManagerWithCachedFlags(); - var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); - Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); - Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); - } - - [Fact] - public void ShouldBeAbleToRemoveFlagForUser() - { - var manager = ManagerWithCachedFlags(); - manager.RemoveFlagForUser("int-key", user); - Assert.Null(manager.FlagForUser("int-key", user)); - } - - [Fact] - public void ShouldBeAbleToUpdateFlagForUser() - { - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(5); - updatedFeatureFlag.version = 12; - flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); - var updatedFlagFromCache = flagCacheManager.FlagForUser("int-flag", user); - Assert.Equal(5, updatedFlagFromCache.value.ToObject()); - Assert.Equal(12, updatedFeatureFlag.version); - } - - [Fact] - public void UpdateFlagUpdatesTheFlagOnListenerManager() - { - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(7); - flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); - - Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); - } - - [Fact] - public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() - { - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - listener.FeatureFlags["int-flag"] = JToken.FromObject(1); - Assert.True(listener.FeatureFlags.ContainsKey("int-flag")); - - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(7); - flagCacheManager.RemoveFlagForUser("int-flag", user); - - Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); - } - - [Fact] - public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() - { - var flagCacheManager = ManagerWithCachedFlags(); - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - - var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); - flagCacheManager.CacheFlagsFromService(updatedFlags, user); - - Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); - } - } -} +using System; +using LaunchDarkly.Client; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class FlagCacheManagerTests + { + IUserFlagCache deviceCache = new UserFlagInMemoryCache(); + IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); + FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); + + User user = User.WithKey("someKey"); + + IFlagCacheManager ManagerWithCachedFlags() + { + var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); + flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); + return flagCacheManager; + } + + [Fact] + public void CacheFlagsShouldStoreFlagsInDeviceCache() + { + var flagCacheManager = ManagerWithCachedFlags(); + var cachedDeviceFlags = deviceCache.RetrieveFlags(user); + Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); + Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); + Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + } + + [Fact] + public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() + { + var flagCacheManager = ManagerWithCachedFlags(); + var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); + Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); + Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); + Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + } + + [Fact] + public void ShouldBeAbleToRemoveFlagForUser() + { + var manager = ManagerWithCachedFlags(); + manager.RemoveFlagForUser("int-key", user); + Assert.Null(manager.FlagForUser("int-key", user)); + } + + [Fact] + public void ShouldBeAbleToUpdateFlagForUser() + { + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(5); + updatedFeatureFlag.version = 12; + flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); + var updatedFlagFromCache = flagCacheManager.FlagForUser("int-flag", user); + Assert.Equal(5, updatedFlagFromCache.value.ToObject()); + Assert.Equal(12, updatedFeatureFlag.version); + } + + [Fact] + public void UpdateFlagUpdatesTheFlagOnListenerManager() + { + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(7); + flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); + + Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); + } + + [Fact] + public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() + { + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + listener.FeatureFlags["int-flag"] = JToken.FromObject(1); + Assert.True(listener.FeatureFlags.ContainsKey("int-flag")); + + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(7); + flagCacheManager.RemoveFlagForUser("int-flag", user); + + Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); + } + + [Fact] + public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() + { + var flagCacheManager = ManagerWithCachedFlags(); + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + + var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); + flagCacheManager.CacheFlagsFromService(updatedFlags, user); + + Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 37c493cb..be383811 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -1,61 +1,61 @@ using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json; +using System.Collections.Generic; +using LaunchDarkly.Client; +using Newtonsoft.Json; using Xunit; namespace LaunchDarkly.Xamarin.Tests { public class DefaultLdClientTests - { - static readonly string appKey = "some app key"; - static readonly string flagKey = "some flag key"; - - LdClient Client() + { + static readonly string appKey = "some app key"; + static readonly string flagKey = "some flag key"; + + LdClient Client() { if (LdClient.Instance == null) { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); return LdClient.Init(configuration, user); } - return LdClient.Instance; - } - + return LdClient.Instance; + } + [Fact] - public void CanCreateClientWithConfigAndUser() - { + public void CanCreateClientWithConfigAndUser() + { Assert.NotNull(Client()); } [Fact] - public void DefaultBoolVariationFlag() - { + public void DefaultBoolVariationFlag() + { Assert.False(Client().BoolVariation(flagKey)); } [Fact] public void DefaultStringVariationFlag() - { - Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); + { + Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); } [Fact] public void DefaultFloatVariationFlag() - { + { Assert.Equal(0, Client().FloatVariation(flagKey)); } [Fact] public void DefaultIntVariationFlag() - { + { Assert.Equal(0, Client().IntVariation(flagKey)); } [Fact] public void DefaultJSONVariationFlag() - { + { Assert.Null(Client().JsonVariation(flagKey, null)); } @@ -63,7 +63,7 @@ public void DefaultJSONVariationFlag() public void DefaultAllFlagsShouldBeEmpty() { var client = Client(); - client.Identify(User.WithKey("some other user key with no flags")); + client.Identify(User.WithKey("some other user key with no flags")); Assert.Equal(0, client.AllFlags().Count); client.Identify(User.WithKey("user1Key")); } @@ -74,23 +74,23 @@ public void DefaultValueReturnedIfTypeBackIsDifferent() var client = Client(); Assert.Equal(0, client.IntVariation("string-flag", 0)); Assert.False(client.BoolVariation("float-flag", false)); - } - + } + [Fact] - public void IdentifyUpdatesTheUser() + public void IdentifyUpdatesTheUser() { - var client = Client(); - var updatedUser = User.WithKey("some new key"); - client.Identify(updatedUser); - Assert.Equal(client.User, updatedUser); + var client = Client(); + var updatedUser = User.WithKey("some new key"); + client.Identify(updatedUser); + Assert.Equal(client.User, updatedUser); } [Fact] - public void SharedClientIsTheOnlyClientAvailable() + public void SharedClientIsTheOnlyClientAvailable() { - var client = Client(); - var config = Configuration.Default(appKey); - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + var client = Client(); + var config = Configuration.Default(appKey); + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); } [Fact] @@ -112,27 +112,27 @@ public void ConnectionManagerShouldKnowIfOnlineOrNot() connMgr.Connect(true); Assert.False(client.IsOffline()); connMgr.Connect(false); - Assert.False(client.Online); + Assert.False(client.Online); } [Fact] public void ConnectionChangeShouldStopUpdateProcessor() - { + { var client = Client(); var connMgr = client.Config.ConnectionManager as MockConnectionManager; connMgr.ConnectionChanged += (bool obj) => client.Online = obj; connMgr.Connect(false); var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; - Assert.False(mockUpdateProc.IsRunning); + Assert.False(mockUpdateProc.IsRunning); } [Fact] - public void UserWithMissingKeyWillHaveUniqueKeySet() + public void UserWithNullKeyWillHaveUniqueKeySet() { LdClient.Instance = null; - var userWithoutKey = User.WithKey(String.Empty); - var config = StubbedConfigAndUserBuilder.Config(userWithoutKey, "someOtherAppKey"); - var client = LdClient.Init(config, userWithoutKey); + var userWithNullKey = User.WithKey(null); + var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); + var client = LdClient.Init(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); LdClient.Instance = null; } @@ -140,8 +140,10 @@ public void UserWithMissingKeyWillHaveUniqueKeySet() [Fact] public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() { + var client = Client(); LdClient.Instance.Identify(User.WithKey("a new user's key")); - LdClient.Instance.Identify(User.WithKey(String.Empty)); + var userWithNullKey = User.WithKey(null); + LdClient.Instance.Identify(userWithNullKey); Assert.Equal(MockDeviceInfo.key, LdClient.Instance.User.Key); LdClient.Instance = null; } @@ -200,6 +202,6 @@ public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerMan client.UnregisterFeatureFlagListener("user2-flag", listener); listenerMgr.FlagWasUpdated("user2-flag", 12); Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); - } + } } } From 56165a0dac1298df45e55c4b728557e05b366b96 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Thu, 12 Jul 2018 21:00:38 -0700 Subject: [PATCH 003/254] Send default feature flag event when flag.value is null (#6) Also return default value instead of null value. --- src/LaunchDarkly.Xamarin/LdClient.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index d7824bdc..189202b8 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -334,13 +334,21 @@ JToken Variation(string featureKey, JToken defaultValue) if (flag != null) { featureFlagEvent = new FeatureFlagEvent(featureKey, flag); - featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, - User, - flag.variation, - flag.value, - defaultValue); + var value = flag.value; + if (value == null) { + featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, + User, + defaultValue); + value = defaultValue; + } else { + featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, + User, + flag.variation, + flag.value, + defaultValue); + } eventProcessor.SendEvent(featureRequestEvent); - return flag.value; + return value; } Log.InfoFormat("Unknown feature flag {0}; returning default value", From d0ca44e9ba7d3ea747bf988fef82513c64507abc Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Fri, 13 Jul 2018 07:15:51 -0700 Subject: [PATCH 004/254] Create License.txt --- License.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 License.txt diff --git a/License.txt b/License.txt new file mode 100644 index 00000000..f8503553 --- /dev/null +++ b/License.txt @@ -0,0 +1,13 @@ +Copyright 2018 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 2644d7f5e063e91ee52471af200a8a0cb66fbac9 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Fri, 13 Jul 2018 15:10:02 -0700 Subject: [PATCH 005/254] Add test that we return default value for off variation (#7) --- .../FeatureFlagsFromService.json | 5 +++++ tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs | 7 +++++++ .../MobilePollingProcessorTests.cs | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json index cb9f23d4..37a9b008 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json +++ b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json @@ -29,5 +29,10 @@ "version": 456, "variation": 1, "trackEvents": false + }, + "off-flag": { + "value": null, + "version": 456, + "trackEvents": false } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index be383811..25f9f7cf 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -76,6 +76,13 @@ public void DefaultValueReturnedIfTypeBackIsDifferent() Assert.False(client.BoolVariation("float-flag", false)); } + [Fact] + public void DefaultValueReturnedIfFlagIsOff() + { + var client = Client(); + Assert.Equal(123, client.IntVariation("off-flag", 123)); + } + [Fact] public void IdentifyUpdatesTheUser() { diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs index a946fd5f..3a9552ce 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs @@ -32,7 +32,7 @@ public void StartWaitsUntilFlagCacheFilled() var initTask = processor.Start(); var unused = initTask.Wait(TimeSpan.FromSeconds(1)); var flags = mockFlagCacheManager.FlagsForUser(user); - Assert.Equal(5, flags.Count); + Assert.Equal(6, flags.Count); } } } From 7fdf036084335bb3b7a7f01fe866e66d6ed619f9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:39:59 -0700 Subject: [PATCH 006/254] misc cleanup of project files --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 7 ------- .../LaunchDarkly.Xamarin.Tests.csproj | 5 ----- 2 files changed, 12 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2adb9c96..2f398394 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -4,7 +4,6 @@ netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk - netstandard2.0 @@ -24,12 +23,6 @@ - - - - - - diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 33b64dce..a4a01263 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -27,11 +27,6 @@ - - - - - Always From 8924b5cb7ce7347d03666eef2bb816d249c88980 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:40:22 -0700 Subject: [PATCH 007/254] rm usage that won't work in older target frameworks --- src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs index 40bdc3a2..1cd8c9b9 100644 --- a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs @@ -129,7 +129,7 @@ Task IStreamProcessor.HandleMessage(StreamManager streamManager, string messageT break; } - return Task.CompletedTask; + return Task.FromResult(true); } void PatchFeatureFlag(string flagKey, FeatureFlag featureFlag) From b3287b691aa74423c65abc3c22c312b609be60fa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:54:26 -0700 Subject: [PATCH 008/254] version 1.0.0-beta8 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2f398394..2d110ff1 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,6 +1,7 @@ + 1.0.0-beta8 netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk From dcc7f9fe007546862578ccf2ddb6696b337231d9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 16:01:23 -0700 Subject: [PATCH 009/254] version 1.0.0-beta9 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2d110ff1..702070f3 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta8 + 1.0.0-beta9 netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk From c2f674a49f67482d139de5ce0b582559d49b43c5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 16:53:38 -0700 Subject: [PATCH 010/254] clean up unnecessary static references and make tests stable --- src/LaunchDarkly.Xamarin/LdClient.cs | 25 ++++++------ .../LdClientTests.cs | 39 ++++++++++--------- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 27 +++++++++++++ 3 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 189202b8..c404b742 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -148,7 +148,7 @@ public static LdClient Init(Configuration config, User user) if (Instance.Online) { - StartUpdateProcessor(); + Instance.StartUpdateProcessor(); } return Instance; @@ -173,7 +173,7 @@ public static Task InitAsync(Configuration config, User user) if (Instance.Online) { - Task t = StartUpdateProcessorAsync(); + Task t = Instance.StartUpdateProcessorAsync(); return t.ContinueWith((result) => Instance); } else @@ -192,16 +192,15 @@ static void CreateInstance(Configuration configuration, User user) Instance.Version); } - static void StartUpdateProcessor() + void StartUpdateProcessor() { - var initTask = Instance.updateProcessor.Start(); - var configuration = Instance.Config as Configuration; - var unused = initTask.Wait(configuration.StartWaitTime); + var initTask = updateProcessor.Start(); + var unused = initTask.Wait(Config.StartWaitTime); } - static Task StartUpdateProcessorAsync() + Task StartUpdateProcessorAsync() { - return Instance.updateProcessor.Start(); + return updateProcessor.Start(); } void SetupConnectionManager() @@ -300,7 +299,7 @@ JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jto { Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", jtokenType, - returnedFlagValue.GetType(), + returnedFlagValue.Type, featureKey); return defaultValue; @@ -403,8 +402,12 @@ public void Track(string eventName) /// public bool Initialized() { - bool isInited = Instance != null; - return isInited && Online; + //bool isInited = Instance != null; + //return isInited && Online; + // TODO: This method needs to be fixed to actually check whether the update processor has initialized. + // The previous logic (above) was meaningless because this method is not static, so by definition you + // do have a client instance if we've gotten here. But that doesn't mean it is initialized. + return Online; } /// diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 25f9f7cf..c1b38d9d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -13,14 +13,9 @@ public class DefaultLdClientTests LdClient Client() { - if (LdClient.Instance == null) - { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); - return LdClient.Init(configuration, user); - } - - return LdClient.Instance; + User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); + var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + return TestUtil.CreateClient(configuration, user); } [Fact] @@ -95,9 +90,20 @@ public void IdentifyUpdatesTheUser() [Fact] public void SharedClientIsTheOnlyClientAvailable() { - var client = Client(); - var config = Configuration.Default(appKey); - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + lock (TestUtil.ClientInstanceLock) + { + User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); + var config = StubbedConfigAndUserBuilder.Config(user, appKey); + var client = LdClient.Init(config, user); + try + { + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + } + finally + { + LdClient.Instance = null; + } + } } [Fact] @@ -136,23 +142,20 @@ public void ConnectionChangeShouldStopUpdateProcessor() [Fact] public void UserWithNullKeyWillHaveUniqueKeySet() { - LdClient.Instance = null; var userWithNullKey = User.WithKey(null); var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); - var client = LdClient.Init(config, userWithNullKey); + var client = TestUtil.CreateClient(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); - LdClient.Instance = null; } [Fact] public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() { var client = Client(); - LdClient.Instance.Identify(User.WithKey("a new user's key")); + client.Identify(User.WithKey("a new user's key")); var userWithNullKey = User.WithKey(null); - LdClient.Instance.Identify(userWithNullKey); - Assert.Equal(MockDeviceInfo.key, LdClient.Instance.User.Key); - LdClient.Instance = null; + client.Identify(userWithNullKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); } [Fact] diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs new file mode 100644 index 00000000..8624f1b0 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + class TestUtil + { + // Any tests that are going to access the static LdClient.Instance must hold this lock, + // to avoid interfering with tests that use CreateClient. + public static readonly object ClientInstanceLock = new object(); + + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can + // instantiate their own independent clients. Application code cannot do this because + // the LdClient.Instance setter has internal scope. + public static LdClient CreateClient(Configuration config, User user) + { + lock (ClientInstanceLock) + { + LdClient client = LdClient.Init(config, user); + LdClient.Instance = null; + return client; + } + } + } +} From 27c0a396b28454a55910e1cd161b7ca74286dcfe Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 17:37:30 -0700 Subject: [PATCH 011/254] break out and simplify basic flag evaluation tests --- .../LdClientEvaluationTests.cs | 137 ++++++++++++++++++ .../LdClientTests.cs | 75 +--------- .../StubbedConfigAndUserBuilder.cs | 22 --- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 39 ++++- 4 files changed, 177 insertions(+), 96 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs new file mode 100644 index 00000000..78d9fe9a --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class LdClientEvaluationTests + { + static readonly string appKey = "some app key"; + static readonly string nonexistentFlagKey = "some flag key"; + static readonly User user = User.WithKey("userkey"); + + private static LdClient ClientWithFlagsJson(string flagsJson) + { + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + return TestUtil.CreateClient(config, user); + } + + [Fact] + public void BoolVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(true)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.True(client.BoolVariation("flag-key", false)); + } + + [Fact] + public void BoolVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.False(client.BoolVariation(nonexistentFlagKey)); + } + + [Fact] + public void IntVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(3, client.IntVariation("flag-key", 0)); + } + + [Fact] + public void IntVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal(1, client.IntVariation(nonexistentFlagKey, 1)); + } + + [Fact] + public void FloatVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2.5f)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(2.5f, client.FloatVariation("flag-key", 0)); + } + + [Fact] + public void FloatVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal(0.5f, client.FloatVariation(nonexistentFlagKey, 0.5f)); + } + + [Fact] + public void StringVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal("string value", client.StringVariation("flag-key", "")); + } + + [Fact] + public void StringVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal("d", client.StringVariation(nonexistentFlagKey, "d")); + } + + [Fact] + public void JsonVariationReturnsValue() + { + var jsonValue = new JObject { { "thing", new JValue("stuff") } }; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue); + var client = ClientWithFlagsJson(flagsJson); + + var defaultValue = new JValue(3); + Assert.Equal(jsonValue, client.JsonVariation("flag-key", defaultValue)); + } + + [Fact] + public void JsonVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); + } + + [Fact] + public void AllFlagsReturnsAllFlagValues() + { + var flagsJson = @"{""flag1"":{""value"":""a""},""flag2"":{""value"":""b""}}"; + var client = ClientWithFlagsJson(flagsJson); + + var result = client.AllFlags(); + Assert.Equal(2, result.Count); + Assert.Equal(new JValue("a"), result["flag1"]); + Assert.Equal(new JValue("b"), result["flag2"]); + } + + [Fact] + public void DefaultValueReturnedIfValueTypeIsDifferent() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var client = TestUtil.CreateClient(config, user); + + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } + + [Fact] + public void DefaultValueReturnedIfFlagValueIsNull() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", null); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var client = TestUtil.CreateClient(config, user); + + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index c1b38d9d..41b2794d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using LaunchDarkly.Client; -using Newtonsoft.Json; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -9,12 +7,11 @@ namespace LaunchDarkly.Xamarin.Tests public class DefaultLdClientTests { static readonly string appKey = "some app key"; - static readonly string flagKey = "some flag key"; LdClient Client() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, JSONReader.FeatureFlagJSON()); return TestUtil.CreateClient(configuration, user); } @@ -24,60 +21,6 @@ public void CanCreateClientWithConfigAndUser() Assert.NotNull(Client()); } - [Fact] - public void DefaultBoolVariationFlag() - { - Assert.False(Client().BoolVariation(flagKey)); - } - - [Fact] - public void DefaultStringVariationFlag() - { - Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); - } - - [Fact] - public void DefaultFloatVariationFlag() - { - Assert.Equal(0, Client().FloatVariation(flagKey)); - } - - [Fact] - public void DefaultIntVariationFlag() - { - Assert.Equal(0, Client().IntVariation(flagKey)); - } - - [Fact] - public void DefaultJSONVariationFlag() - { - Assert.Null(Client().JsonVariation(flagKey, null)); - } - - [Fact] - public void DefaultAllFlagsShouldBeEmpty() - { - var client = Client(); - client.Identify(User.WithKey("some other user key with no flags")); - Assert.Equal(0, client.AllFlags().Count); - client.Identify(User.WithKey("user1Key")); - } - - [Fact] - public void DefaultValueReturnedIfTypeBackIsDifferent() - { - var client = Client(); - Assert.Equal(0, client.IntVariation("string-flag", 0)); - Assert.False(client.BoolVariation("float-flag", false)); - } - - [Fact] - public void DefaultValueReturnedIfFlagIsOff() - { - var client = Client(); - Assert.Equal(123, client.IntVariation("off-flag", 123)); - } - [Fact] public void IdentifyUpdatesTheUser() { @@ -93,7 +36,7 @@ public void SharedClientIsTheOnlyClientAvailable() lock (TestUtil.ClientInstanceLock) { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var config = StubbedConfigAndUserBuilder.Config(user, appKey); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); var client = LdClient.Init(config, user); try { @@ -105,17 +48,7 @@ public void SharedClientIsTheOnlyClientAvailable() } } } - - [Fact] - public void CanFetchFlagFromInMemoryCache() - { - var client = Client(); - bool boolFlag = client.BoolVariation("boolean-flag", true); - Assert.True(boolFlag); - int intFlag = client.IntVariation("int-flag", 0); - Assert.Equal(15, intFlag); - } - + [Fact] public void ConnectionManagerShouldKnowIfOnlineOrNot() { @@ -143,7 +76,7 @@ public void ConnectionChangeShouldStopUpdateProcessor() public void UserWithNullKeyWillHaveUniqueKeySet() { var userWithNullKey = User.WithKey(null); - var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); + var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, "someOtherAppKey", "{}"); var client = TestUtil.CreateClient(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs index 3e5a2beb..5622e9d3 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs @@ -7,28 +7,6 @@ namespace LaunchDarkly.Xamarin.Tests { public static class StubbedConfigAndUserBuilder { - public static Configuration Config(User user, string appKey) - { - var stubbedFlagCache = JSONReader.StubbedFlagCache(user); - - // overriding the default implementation of dependencies for testing purposes - var mockOnlineConnectionManager = new MockConnectionManager(true); - var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); - var mockPollingProcessor = new MockPollingProcessor(); - var mockPersister = new MockPersister(); - var mockDeviceInfo = new MockDeviceInfo(); - var featureFlagListener = new FeatureFlagListenerManager(); - - Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(mockFlagCacheManager) - .WithConnectionManager(mockOnlineConnectionManager) - .WithUpdateProcessor(mockPollingProcessor) - .WithPersister(mockPersister) - .WithDeviceInfo(mockDeviceInfo) - .WithFeatureFlagListenerManager(featureFlagListener); - return configuration; - } - public static User UserWithAllPropertiesFilledIn(string key) { var user = User.WithKey(key); diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 8624f1b0..3db8735e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin.Tests { @@ -23,5 +23,38 @@ public static LdClient CreateClient(Configuration config, User user) return client; } } + + public static string JsonFlagsWithSingleFlag(string flagKey, JToken value) + { + JObject fo = new JObject { { "value", value } }; + JObject o = new JObject { { flagKey, fo } }; + return JsonConvert.SerializeObject(o); + } + + public static Configuration ConfigWithFlagsJson(User user, string appKey, string flagsJson) + { + var flags = JsonConvert.DeserializeObject>(flagsJson); + IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); + if (user != null && user.Key != null) + { + stubbedFlagCache.CacheFlagsForUser(flags, user); + } + + var mockOnlineConnectionManager = new MockConnectionManager(true); + var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); + var mockPollingProcessor = new MockPollingProcessor(); + var mockPersister = new MockPersister(); + var mockDeviceInfo = new MockDeviceInfo(); + var featureFlagListener = new FeatureFlagListenerManager(); + + Configuration configuration = Configuration.Default(appKey) + .WithFlagCacheManager(mockFlagCacheManager) + .WithConnectionManager(mockOnlineConnectionManager) + .WithUpdateProcessor(mockPollingProcessor) + .WithPersister(mockPersister) + .WithDeviceInfo(mockDeviceInfo) + .WithFeatureFlagListenerManager(featureFlagListener); + return configuration; + } } } From e4f61094487e439064a30e0cf50c8bd02479f4c6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 18:06:07 -0700 Subject: [PATCH 012/254] get rid of test fixture files --- .../FeatureFlag.json | 33 ------------ .../FeatureFlagsFromService.json | 38 -------------- .../FlagCacheManagerTests.cs | 20 +++++--- .../LaunchDarkly.Xamarin.Tests/JSONReader.cs | 50 ------------------- .../LaunchDarkly.Xamarin.Tests.csproj | 8 --- .../LdClientTests.cs | 2 +- .../MobilePollingProcessorTests.cs | 10 +++- .../MobileStreamingProcessorTests.cs | 9 +++- .../MockFeatureFlagRequestor.cs | 10 +++- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 7 ++- .../UserFlagCacheTests.cs | 17 +++---- 11 files changed, 49 insertions(+), 155 deletions(-) delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json deleted file mode 100644 index 0363fa5a..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "boolean-flag": { - "value": true, - "version": 123, - "variation": 0, - "trackEvents": true, - "debugEventsUntilDate": 1525196668210 - }, - "json-flag": { - "value": {"some-key": "some-value"}, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "float-flag": { - "value": 3.14159, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "int-flag": { - "value": 5, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "string-flag": { - "value": "string value", - "version": 456, - "variation": 1, - "trackEvents": false - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json deleted file mode 100644 index 37a9b008..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "boolean-flag": { - "value": true, - "version": 123, - "variation": 0, - "trackEvents": true, - "debugEventsUntilDate": 1525196668210 - }, - "json-flag": { - "value": {"some-key": "a new value"}, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "float-flag": { - "value": 13.14159, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "int-flag": { - "value": 15, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "string-flag": { - "value": "markw@magenic.com", - "version": 456, - "variation": 1, - "trackEvents": false - }, - "off-flag": { - "value": null, - "version": 456, - "trackEvents": false - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs index 3b0df506..574cbfd0 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs @@ -1,5 +1,4 @@ -using System; -using LaunchDarkly.Client; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; using Xunit; @@ -7,6 +6,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class FlagCacheManagerTests { + private const string initialFlagsJson = "{" + + "\"int-flag\":{\"value\":15}," + + "\"float-flag\":{\"value\":13.5}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\"}" + + "}"; + IUserFlagCache deviceCache = new UserFlagInMemoryCache(); IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); @@ -16,7 +21,8 @@ public class FlagCacheManagerTests IFlagCacheManager ManagerWithCachedFlags() { var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); - flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); + var flags = TestUtil.DecodeFlagsJson(initialFlagsJson); + flagCacheManager.CacheFlagsFromService(flags, user); return flagCacheManager; } @@ -27,7 +33,7 @@ public void CacheFlagsShouldStoreFlagsInDeviceCache() var cachedDeviceFlags = deviceCache.RetrieveFlags(user); Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + Assert.Equal(13.5, cachedDeviceFlags["float-flag"].value.ToObject()); } [Fact] @@ -37,7 +43,7 @@ public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + Assert.Equal(13.5, cachedDeviceFlags["float-flag"].value.ToObject()); } [Fact] @@ -97,8 +103,8 @@ public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() var listener = new TestListener(); listenerManager.RegisterListener(listener, "int-flag"); - var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); - flagCacheManager.CacheFlagsFromService(updatedFlags, user); + var newFlagsJson = "{\"int-flag\":{\"value\":5}}"; + flagCacheManager.CacheFlagsFromService(TestUtil.DecodeFlagsJson(newFlagsJson), user); Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs b/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs deleted file mode 100644 index 52469e89..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json; - -namespace LaunchDarkly.Xamarin.Tests -{ - public static class JSONReader - { - public static string FeatureFlagJSON() - { - return JSONTextFromFile("FeatureFlag.json"); - } - - public static string FeatureFlagJSONFromService() - { - return JSONTextFromFile("FeatureFlagsFromService.json"); - } - - public static string JSONTextFromFile(string filename) - { - return System.IO.File.ReadAllText(filename); - } - - internal static IUserFlagCache StubbedFlagCache(User user) - { - // stub json into the FlagCache - IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); - if (String.IsNullOrEmpty(user.Key)) - return stubbedFlagCache; - - stubbedFlagCache.CacheFlagsForUser(StubbedFlagsDictionary(), user); - return stubbedFlagCache; - } - - internal static IDictionary StubbedFlagsDictionary() - { - var text = FeatureFlagJSONFromService(); - var flags = JsonConvert.DeserializeObject>(text); - return flags; - } - - internal static IDictionary UpdatedStubbedFlagsDictionary() - { - var text = FeatureFlagJSON(); - var flags = JsonConvert.DeserializeObject>(text); - return flags; - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index a4a01263..903ade3f 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -27,12 +27,4 @@ - - - Always - - - Always - - diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 41b2794d..09990da8 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -11,7 +11,7 @@ public class DefaultLdClientTests LdClient Client() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, JSONReader.FeatureFlagJSON()); + var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); return TestUtil.CreateClient(configuration, user); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs index 3a9552ce..1728fa34 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs @@ -6,12 +6,18 @@ namespace LaunchDarkly.Xamarin.Tests { public class MobilePollingProcessorTests { + private const string flagsJson = "{" + + "\"int-flag\":{\"value\":15}," + + "\"float-flag\":{\"value\":13.5}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\"}" + + "}"; + IFlagCacheManager mockFlagCacheManager; User user; IMobileUpdateProcessor Processor() { - var mockFeatureFlagRequestor = new MockFeatureFlagRequestor(); + var mockFeatureFlagRequestor = new MockFeatureFlagRequestor(flagsJson); var stubbedFlagCache = new UserFlagInMemoryCache(); mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); user = User.WithKey("user1Key"); @@ -32,7 +38,7 @@ public void StartWaitsUntilFlagCacheFilled() var initTask = processor.Start(); var unused = initTask.Wait(TimeSpan.FromSeconds(1)); var flags = mockFlagCacheManager.FlagsForUser(user); - Assert.Equal(6, flags.Count); + Assert.Equal(3, flags.Count); } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index dccd107d..83f99c41 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -11,6 +11,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class MobileStreamingProcessorTests { + private const string initialFlagsJson = "{" + + "\"int-flag\":{\"value\":15,\"version\":100}," + + "\"float-flag\":{\"value\":13.5,\"version\":100}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\",\"version\":100}" + + "}"; + User user = User.WithKey("user key"); EventSourceMock mockEventSource; TestEventSourceFactory eventSourceFactory; @@ -150,8 +156,7 @@ string DeleteFlagWithLowerVersion() void PUTMessageSentToProcessor() { - string jsonData = JSONReader.FeatureFlagJSONFromService(); - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(jsonData, null), "put"); + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); mockEventSource.RaiseMessageRcvd(eventArgs); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs index 09cc07c6..59b6c964 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs @@ -4,6 +4,13 @@ namespace LaunchDarkly.Xamarin.Tests { internal class MockFeatureFlagRequestor : IFeatureFlagRequestor { + private readonly string _jsonFlags; + + public MockFeatureFlagRequestor(string jsonFlags) + { + _jsonFlags = jsonFlags; + } + public void Dispose() { @@ -11,8 +18,7 @@ public void Dispose() public Task FeatureFlagsAsync() { - var jsonText = JSONReader.FeatureFlagJSONFromService(); - var response = new WebResponse(200, jsonText, null); + var response = new WebResponse(200, _jsonFlags, null); return Task.FromResult(response); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 3db8735e..25a3da41 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -31,9 +31,14 @@ public static string JsonFlagsWithSingleFlag(string flagKey, JToken value) return JsonConvert.SerializeObject(o); } + public static IDictionary DecodeFlagsJson(string flagsJson) + { + return JsonConvert.DeserializeObject>(flagsJson); + } + public static Configuration ConfigWithFlagsJson(User user, string appKey, string flagsJson) { - var flags = JsonConvert.DeserializeObject>(flagsJson); + var flags = DecodeFlagsJson(flagsJson); IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); if (user != null && user.Key != null) { diff --git a/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs b/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs index 86683063..4bfffcaa 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using LaunchDarkly.Client; -using Newtonsoft.Json; +using LaunchDarkly.Client; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -16,14 +12,13 @@ public class UserFlagCacheTests [Fact] public void CanCacheFlagsInMemory() { - var text = JSONReader.FeatureFlagJSON(); - var flags = JsonConvert.DeserializeObject>(text); + var jsonFlags = @"{""flag1"":{""value"":1},""flag2"":{""value"":2}}"; + var flags = TestUtil.DecodeFlagsJson(jsonFlags); inMemoryCache.CacheFlagsForUser(flags, user1); var flagsRetrieved = inMemoryCache.RetrieveFlags(user1); - Assert.Equal(flags.Count, flagsRetrieved.Count); - var secondFlag = flags.Values.ToList()[1]; - var secondFlagRetrieved = flagsRetrieved.Values.ToList()[1]; - Assert.Equal(secondFlag, secondFlagRetrieved); + Assert.Equal(2, flagsRetrieved.Count); + Assert.Equal(flags["flag1"], flagsRetrieved["flag1"]); + Assert.Equal(flags["flag2"], flagsRetrieved["flag2"]); } } } From 5ef9ea2abc3463e37e30aa401ce2618d6f143ab2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 18:17:14 -0700 Subject: [PATCH 013/254] fix default value logic --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index c404b742..6d017d34 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -334,7 +334,7 @@ JToken Variation(string featureKey, JToken defaultValue) { featureFlagEvent = new FeatureFlagEvent(featureKey, flag); var value = flag.value; - if (value == null) { + if (value == null || value.Type == JTokenType.Null) { featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, User, defaultValue); From c43c4bb324478bf44f2f01964746410ceecaff93 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 18 Jul 2018 15:43:54 -0700 Subject: [PATCH 014/254] add tests for event generation --- src/LaunchDarkly.Xamarin/Configuration.cs | 14 ++ src/LaunchDarkly.Xamarin/Factory.cs | 6 +- .../LdClientEventTests.cs | 137 ++++++++++++++++++ .../MockEventProcessor.cs | 19 +++ tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 2 +- 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs create mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index c278b592..8a584731 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Common.Logging; +using LaunchDarkly.Client; namespace LaunchDarkly.Xamarin { @@ -125,6 +126,7 @@ public class Configuration : IMobileConfiguration internal IFlagCacheManager FlagCacheManager { get; set; } internal IConnectionManager ConnectionManager { get; set; } + internal IEventProcessor EventProcessor { get; set; } internal IMobileUpdateProcessor MobileUpdateProcessor { get; set; } internal ISimplePersistance Persister { get; set; } internal IDeviceInfo DeviceInfo { get; set; } @@ -567,6 +569,18 @@ public static Configuration WithConnectionManager(this Configuration configurati return configuration; } + /// + /// Sets the IEventProcessor instance, used internally for stubbing mock instances. + /// + /// Configuration. + /// Event processor. + /// the same Configuration instance + public static Configuration WithEventProcessor(this Configuration configuration, IEventProcessor eventProcessor) + { + configuration.EventProcessor = eventProcessor; + return configuration; + } + /// /// Determines whether to use the Report method for networking requests /// diff --git a/src/LaunchDarkly.Xamarin/Factory.cs b/src/LaunchDarkly.Xamarin/Factory.cs index 56eb4369..5fb139c0 100644 --- a/src/LaunchDarkly.Xamarin/Factory.cs +++ b/src/LaunchDarkly.Xamarin/Factory.cs @@ -72,8 +72,12 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return updateProcessor; } - internal static IEventProcessor CreateEventProcessor(IBaseConfiguration configuration) + internal static IEventProcessor CreateEventProcessor(Configuration configuration) { + if (configuration.EventProcessor != null) + { + return configuration.EventProcessor; + } if (configuration.Offline) { Log.InfoFormat("Was configured to be offline, starting service with NullEventProcessor"); diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs new file mode 100644 index 00000000..5e8c58ce --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -0,0 +1,137 @@ +using LaunchDarkly.Client; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class LdClientEventTests + { + private static readonly User user = User.WithKey("userkey"); + private MockEventProcessor eventProcessor = new MockEventProcessor(); + + public LdClient MakeClient(User user, string flagsJson) + { + Configuration config = TestUtil.ConfigWithFlagsJson(user, "appkey", flagsJson); + config.WithEventProcessor(eventProcessor); + return TestUtil.CreateClient(config, user); + } + + [Fact] + public void IdentifySendsIdentifyEvent() + { + using (LdClient client = MakeClient(user, "{}")) + { + User user1 = User.WithKey("userkey1"); + client.Identify(user1); + Assert.Collection(eventProcessor.Events, e => + { + IdentifyEvent ie = Assert.IsType(e); + Assert.Equal(user1.Key, ie.User.Key); + }); + } + } + + [Fact] + public void TrackSendsCustomEvent() + { + using (LdClient client = MakeClient(user, "{}")) + { + JToken data = new JValue("hi"); + client.Track("eventkey", data); + Assert.Collection(eventProcessor.Events, e => + { + CustomEvent ce = Assert.IsType(e); + Assert.Equal("eventkey", ce.Key); + Assert.Equal(user.Key, ce.User.Key); + Assert.Equal(data, ce.JsonData); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForValidFlag() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""trackEvents"":true, ""debugEventsUntilDate"":2000 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("a", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Equal(2000, fe.DebugEventsUntilDate); + }); + } + } + + [Fact] + public void FeatureEventUsesFlagVersionIfProvided() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""flagVersion"":1500 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("a", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1500, fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForDefaultValue() + { + string flagsJson = @"{""flag"":{ + ""value"":null,""variation"":null,""version"":1000 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("b", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForUnknownFlag() + { + using (LdClient client = MakeClient(user, "{}")) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("b", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs new file mode 100644 index 00000000..1f0d3bc5 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class MockEventProcessor : IEventProcessor + { + public List Events = new List(); + + public void SendEvent(Event e) + { + Events.Add(e); + } + + public void Flush() { } + + public void Dispose() { } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 25a3da41..224c7bb2 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin.Tests { - class TestUtil + public class TestUtil { // Any tests that are going to access the static LdClient.Instance must hold this lock, // to avoid interfering with tests that use CreateClient. From 537886f0413a0acff47438cbc3c5789b8a778fc4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 19 Jul 2018 11:44:58 -0700 Subject: [PATCH 015/254] throw exception if user is null; assign unique key if key is null or empty --- src/LaunchDarkly.Xamarin/LdClient.cs | 92 ++++++++----------- .../LdClientTests.cs | 48 +++++++++- 2 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6d017d34..f55c443d 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -58,7 +58,16 @@ public sealed class LdClient : ILdMobileClient LdClient() { } LdClient(Configuration configuration, User user) - { + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + if (user == null) + { + throw new ArgumentNullException("user"); + } + Config = configuration; connectionLock = new SemaphoreSlim(1, 1); @@ -67,8 +76,8 @@ public sealed class LdClient : ILdMobileClient deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - // If you pass in a null user or user with a null key, one will be assigned to them. - if (user == null || user.Key == null) + // If you pass in a user with a null or blank key, one will be assigned to them. + if (String.IsNullOrEmpty(user.Key)) { User = UserWithUniqueKey(user); } @@ -91,7 +100,7 @@ public sealed class LdClient : ILdMobileClient /// /// This constructor will wait and block on the current thread until initialization and the /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use + /// in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more specific /// to instantiate the single instance of LdClient @@ -99,7 +108,8 @@ public sealed class LdClient : ILdMobileClient /// /// The singleton LdClient instance. /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static LdClient Init(string mobileKey, User user) { var config = Configuration.Default(mobileKey); @@ -110,7 +120,7 @@ public static LdClient Init(string mobileKey, User user) /// /// Creates and returns new LdClient singleton instance, then starts the workflow for /// fetching feature flags. This constructor should be used if you do not want to wait - /// for the IUpdateProcessor instance to finish initializing and receive the first response + /// for the client to finish initializing and receive the first response /// from the LaunchDarkly service. /// /// This is the creation point for LdClient, you must use this static method or the more specific @@ -119,7 +129,8 @@ public static LdClient Init(string mobileKey, User user) /// /// The singleton LdClient instance. /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static async Task InitAsync(string mobileKey, User user) { var config = Configuration.Default(mobileKey); @@ -133,7 +144,7 @@ public static async Task InitAsync(string mobileKey, User user) /// /// This constructor will wait and block on the current thread until initialization and the /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use + /// in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic /// to instantiate the single instance of LdClient @@ -141,7 +152,8 @@ public static async Task InitAsync(string mobileKey, User user) /// /// The singleton LdClient instance. /// The client configuration object - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static LdClient Init(Configuration config, User user) { CreateInstance(config, user); @@ -166,7 +178,8 @@ public static LdClient Init(Configuration config, User user) /// /// The singleton LdClient instance. /// The client configuration object - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static Task InitAsync(Configuration config, User user) { CreateInstance(config, user); @@ -184,8 +197,10 @@ public static Task InitAsync(Configuration config, User user) static void CreateInstance(Configuration configuration, User user) { - if (Instance != null) - throw new Exception("LdClient instance already exists."); + if (Instance != null) + { + throw new Exception("LdClient instance already exists."); + } Instance = new LdClient(configuration, user); Log.InfoFormat("Initialized LaunchDarkly Client {0}", @@ -318,17 +333,7 @@ JToken Variation(string featureKey, JToken defaultValue) Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); return defaultValue; } - - if (User == null || User.Key == null) - { - Log.Warn("Feature flag evaluation called with null user or null user key. Returning default"); - featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, - User, - defaultValue); - eventProcessor.SendEvent(featureRequestEvent); - return defaultValue; - } - + var flag = flagCacheManager.FlagForUser(featureKey, User); if (flag != null) { @@ -372,11 +377,6 @@ public IDictionary AllFlags() Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); return null; } - if (User == null || User.Key == null) - { - Log.Warn("AllFlags() called with null user or null user key. Returning null"); - return null; - } return flagCacheManager.FlagsForUser(User) .ToDictionary(p => p.Key, p => p.Value.value); @@ -385,11 +385,6 @@ public IDictionary AllFlags() /// public void Track(string eventName, JToken data) { - if (User == null || User.Key == null) - { - Log.Warn("Track called with null user or null user key"); - } - eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); } @@ -433,19 +428,14 @@ public async Task IdentifyAsync(User user) { if (user == null) { - Log.Warn("Identify called with null user"); - return; + throw new ArgumentNullException("user"); } - User userWithKey = null; - if (user.Key == null) + User userWithKey = user; + if (String.IsNullOrEmpty(user.Key)) { userWithKey = UserWithUniqueKey(user); } - else - { - userWithKey = user; - } await connectionLock.WaitAsync(); try @@ -488,22 +478,14 @@ void ClearUpdateProcessor() } } - User UserWithUniqueKey(User user = null) + User UserWithUniqueKey(User user) { string uniqueId = deviceInfo.UniqueDeviceId(); - - if (user != null) - { - var updatedUser = new User(user) - { - Key = uniqueId, - Anonymous = true - }; - - return updatedUser; - } - - return new User(uniqueId); + return new User(user) + { + Key = uniqueId, + Anonymous = true + }; } void IDisposable.Dispose() diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 09990da8..69c7dacf 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -21,6 +21,19 @@ public void CanCreateClientWithConfigAndUser() Assert.NotNull(Client()); } + [Fact] + public void CannotCreateClientWithNullConfig() + { + Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + } + + [Fact] + public void CannotCreateClientWithNullUser() + { + Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); + Assert.Throws(() => LdClient.Init(config, null)); + } + [Fact] public void IdentifyUpdatesTheUser() { @@ -30,6 +43,21 @@ public void IdentifyUpdatesTheUser() Assert.Equal(client.User, updatedUser); } + [Fact] + public void IdentifyWithNullUserThrowsException() + { + var client = Client(); + Assert.Throws(() => client.Identify(null)); + } + + [Fact] + public void IdentifyAsyncWithNullUserThrowsException() + { + var client = Client(); + Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); + // note that exceptions thrown out of an async task are always wrapped in AggregateException + } + [Fact] public void SharedClientIsTheOnlyClientAvailable() { @@ -82,7 +110,16 @@ public void UserWithNullKeyWillHaveUniqueKeySet() } [Fact] - public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() + public void UserWithEmptyKeyWillHaveUniqueKeySet() + { + var userWithEmptyKey = User.WithKey(""); + var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, "someOtherAppKey", "{}"); + var client = TestUtil.CreateClient(config, userWithEmptyKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); + } + + [Fact] + public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() { var client = Client(); client.Identify(User.WithKey("a new user's key")); @@ -91,6 +128,15 @@ public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() Assert.Equal(MockDeviceInfo.key, client.User.Key); } + [Fact] + public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() + { + var client = Client(); + var userWithEmptyKey = User.WithKey(""); + client.Identify(userWithEmptyKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); + } + [Fact] public void UpdatingKeylessUserWillGenerateNewUserWithSameValues() { From 8d299c7172bae06f36dd7a61fe39bf8b36ab1c4c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 11:33:40 -0700 Subject: [PATCH 016/254] don't use strong naming for LaunchDarkly.Xamarin --- .circleci/config.yml | 4 ---- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 8 ++------ src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs | 7 +------ .../LaunchDarkly.Xamarin.Tests.csproj | 10 +++------- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a102ad9f..93ae6397 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,11 +9,7 @@ jobs: docker: - image: microsoft/dotnet:2.0-sdk-jessie steps: - - run: - name: install packages - command: apt-get -q update && apt-get install -qy awscli - checkout - - run: aws s3 cp s3://launchdarkly-pastebin/ci/dotnet/LaunchDarkly.Xamarin.snk LaunchDarkly.Xamarin.snk - run: dotnet restore - run: dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 - run: dotnet test tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj -f netcoreapp2.0 diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 702070f3..2b6ea266 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,12 +1,9 @@ - 1.0.0-beta9 + 1.0.0-beta10 netstandard1.6;netstandard2.0;net45 - true - ..\..\LaunchDarkly.Xamarin.snk - false @@ -15,14 +12,13 @@ obj\Release\netstandard2.0 - false - + diff --git a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs index 184d6064..7d76c4d2 100644 --- a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs +++ b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs @@ -1,10 +1,5 @@ using System; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests,PublicKey=" + -"0024000004800000940000000602000000240000525341310004000001000100" + -"058a1dbccbc342759dc98b1eaba4467bfdea062629f212cf7c669ff26b4e2ff3" + -"c408292487bc349b8a687d73033ff14dbf861e1eea23303a5b5d13b1db034799" + -"13bd120ba372cf961d27db9f652631565f4e8aff4a79e11cfe713833157ecb5d" + -"cbc02d772967d919f8f06fbee227a664dc591932d5b05f4da1c8439702ecfdb1")] +[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests")] diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 903ade3f..aa036d1b 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -3,21 +3,17 @@ netcoreapp2.0 2.0.0 - true - ..\..\LaunchDarkly.Xamarin.snk - true - - + - - + + From df9e61e55fe2a36778cc4b861df61a42dd5d9e10 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:09:48 -0700 Subject: [PATCH 017/254] skip connectivity check in .NET Standard --- .../MobileConnectionManager.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs index 9dcfa92d..3d9cccfa 100644 --- a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs +++ b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs @@ -9,8 +9,13 @@ internal class MobileConnectionManager : IConnectionManager internal MobileConnectionManager() { - isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; - Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + UpdateConnectedStatus(); + try + { + Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + } + catch (NotImplementedInReferenceAssemblyException) + { } } bool isConnected; @@ -22,12 +27,24 @@ bool IConnectionManager.IsConnected isConnected = value; } } - - + void Connectivity_ConnectivityChanged(ConnectivityChangedEventArgs e) { - isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; + UpdateConnectedStatus(); ConnectionChanged?.Invoke(isConnected); } + + private void UpdateConnectedStatus() + { + try + { + isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; + } + catch (NotImplementedInReferenceAssemblyException) + { + // .NET Standard has no way to detect network connectivity + isConnected = true; + } + } } } From 3e4f4e12bb9ead3012d35f087159eb8b29ed7fe4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:11:19 -0700 Subject: [PATCH 018/254] fix name of config setter --- src/LaunchDarkly.Xamarin/Configuration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 8a584731..3ae604df 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -239,7 +239,7 @@ public static class ConfigurationExtensions /// the configuration /// the base URI as a string /// the same Configuration instance - public static Configuration WithUri(this Configuration configuration, string uri) + public static Configuration WithBaseUri(this Configuration configuration, string uri) { if (uri != null) configuration.BaseUri = new Uri(uri); @@ -253,7 +253,7 @@ public static Configuration WithUri(this Configuration configuration, string uri /// the configuration /// the base URI /// the same Configuration instance - public static Configuration WithUri(this Configuration configuration, Uri uri) + public static Configuration WithBaseUri(this Configuration configuration, Uri uri) { if (uri != null) configuration.BaseUri = uri; From fb5621a9f840b56c5053553336d4b3c2b6765a5a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:53:30 -0700 Subject: [PATCH 019/254] use platform reference instead of package reference --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2b6ea266..a38613e1 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -14,8 +14,8 @@ + - From 52bc60ae4083bbf2b2fae44923d6274fd55301d9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:58:36 -0700 Subject: [PATCH 020/254] skip using preferences API in .NET Standard --- .../SimpleMobileDevicePersistance.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs index 022905f6..5a0f86e1 100644 --- a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs +++ b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs @@ -7,12 +7,23 @@ internal class SimpleMobileDevicePersistance : ISimplePersistance { public void Save(string key, string value) { - Preferences.Set(key, value); + try + { + Preferences.Set(key, value); + } + catch (NotImplementedInReferenceAssemblyException) { } } public string GetValue(string key) { - return Preferences.Get(key, null); + try + { + return Preferences.Get(key, null); + } + catch (NotImplementedInReferenceAssemblyException) + { + return null; + } } } } From a9d5167179153958c1722a3624acf551b925d3a0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 13:20:11 -0700 Subject: [PATCH 021/254] beta11 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index a38613e1..6b163c85 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta10 + 1.0.0-beta11 netstandard1.6;netstandard2.0;net45 From fca4ca08c2774b9d5dac551d09b8d73fe4ee0af9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 13:24:44 -0700 Subject: [PATCH 022/254] fix test --- tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index 03e2ac60..250deb68 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -11,7 +11,7 @@ public class ConfigurationTest public void CanOverrideConfiguration() { var config = Configuration.Default("AnyOtherSdkKey") - .WithUri("https://app.AnyOtherEndpoint.com") + .WithBaseUri("https://app.AnyOtherEndpoint.com") .WithEventQueueCapacity(99) .WithPollingInterval(TimeSpan.FromMinutes(1)); From a8ffc36f669fd1b540f58d0c1efd1ef0d4250aef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:13:08 -0700 Subject: [PATCH 023/254] rm note about signing --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index b6617ad5..d2065665 100644 --- a/README.md +++ b/README.md @@ -49,26 +49,6 @@ Contributing See [Contributing](https://github.com/launchdarkly/xamarin-client/blob/master/CONTRIBUTING.md). -Signing -------- -The artifacts generated from this repo are signed by LaunchDarkly. The public key file is in this repo at `LaunchDarkly.Xamarin.pk` as well as here: - -``` -Public Key: -00240000048000009400000006020000 -00240000525341310004000001000100 -058a1dbccbc342759dc98b1eaba4467b -fdea062629f212cf7c669ff26b4e2ff3 -c408292487bc349b8a687d73033ff14d -bf861e1eea23303a5b5d13b1db034799 -13bd120ba372cf961d27db9f65263156 -5f4e8aff4a79e11cfe713833157ecb5d -cbc02d772967d919f8f06fbee227a664 -dc591932d5b05f4da1c8439702ecfdb1 - -Public Key Token: 90b24964a3dfb906f86add69004e6885 -``` - About LaunchDarkly ----------- From d32bdf609b43b6ee41fb9861a18c54bfbf189d89 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:56:18 -0700 Subject: [PATCH 024/254] don't set updateProcessor to null --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index f55c443d..8b0f4fa1 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -474,7 +474,7 @@ void ClearUpdateProcessor() if (updateProcessor != null) { updateProcessor.Dispose(); - updateProcessor = null; + updateProcessor = new NullUpdateProcessor(); } } From 23c94c1a1d3285a0b6dda896e23c62ddff4575a9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:58:10 -0700 Subject: [PATCH 025/254] more cleanup/simplification of test code --- .../LdClientTests.cs | 234 ++++++++++-------- .../MockComponents.cs | 178 +++++++++++++ .../MockConnectionManager.cs | 34 --- .../MockEventProcessor.cs | 19 -- .../MockFeatureFlagRequestor.cs | 25 -- .../MockFlagCacheManager.cs | 53 ---- .../MockPollingProcessor.cs | 30 --- .../StubbedConfigAndUserBuilder.cs | 58 ----- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 20 +- 9 files changed, 319 insertions(+), 332 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 69c7dacf..ef74713a 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -7,55 +7,55 @@ namespace LaunchDarkly.Xamarin.Tests public class DefaultLdClientTests { static readonly string appKey = "some app key"; + static readonly User simpleUser = User.WithKey("user-key"); LdClient Client() { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - return TestUtil.CreateClient(configuration, user); + var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + return TestUtil.CreateClient(configuration, simpleUser); } - - [Fact] - public void CanCreateClientWithConfigAndUser() - { - Assert.NotNull(Client()); - } - + [Fact] public void CannotCreateClientWithNullConfig() { - Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + Assert.Throws(() => LdClient.Init((Configuration)null, simpleUser)); } [Fact] public void CannotCreateClientWithNullUser() { - Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); Assert.Throws(() => LdClient.Init(config, null)); } [Fact] public void IdentifyUpdatesTheUser() { - var client = Client(); - var updatedUser = User.WithKey("some new key"); - client.Identify(updatedUser); - Assert.Equal(client.User, updatedUser); + using (var client = Client()) + { + var updatedUser = User.WithKey("some new key"); + client.Identify(updatedUser); + Assert.Equal(client.User, updatedUser); + } } [Fact] public void IdentifyWithNullUserThrowsException() { - var client = Client(); - Assert.Throws(() => client.Identify(null)); + using (var client = Client()) + { + Assert.Throws(() => client.Identify(null)); + } } [Fact] public void IdentifyAsyncWithNullUserThrowsException() { - var client = Client(); - Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); - // note that exceptions thrown out of an async task are always wrapped in AggregateException + using (var client = Client()) + { + Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); + // note that exceptions thrown out of an async task are always wrapped in AggregateException + } } [Fact] @@ -63,16 +63,17 @@ public void SharedClientIsTheOnlyClientAvailable() { lock (TestUtil.ClientInstanceLock) { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - var client = LdClient.Init(config, user); - try + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + using (var client = LdClient.Init(config, simpleUser)) { - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); - } - finally - { - LdClient.Instance = null; + try + { + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, simpleUser)); + } + finally + { + LdClient.Instance = null; + } } } } @@ -80,117 +81,150 @@ public void SharedClientIsTheOnlyClientAvailable() [Fact] public void ConnectionManagerShouldKnowIfOnlineOrNot() { - var client = Client(); - var connMgr = client.Config.ConnectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(true); - Assert.False(client.IsOffline()); - connMgr.Connect(false); - Assert.False(client.Online); + using (var client = Client()) + { + var connMgr = client.Config.ConnectionManager as MockConnectionManager; + connMgr.ConnectionChanged += (bool obj) => client.Online = obj; + connMgr.Connect(true); + Assert.False(client.IsOffline()); + connMgr.Connect(false); + Assert.False(client.Online); + } } [Fact] public void ConnectionChangeShouldStopUpdateProcessor() { - var client = Client(); - var connMgr = client.Config.ConnectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(false); - var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; - Assert.False(mockUpdateProc.IsRunning); + using (var client = Client()) + { + var connMgr = client.Config.ConnectionManager as MockConnectionManager; + connMgr.ConnectionChanged += (bool obj) => client.Online = obj; + connMgr.Connect(false); + var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; + Assert.False(mockUpdateProc.IsRunning); + } } [Fact] public void UserWithNullKeyWillHaveUniqueKeySet() { var userWithNullKey = User.WithKey(null); - var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, "someOtherAppKey", "{}"); - var client = TestUtil.CreateClient(config, userWithNullKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, userWithNullKey)) + { + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void UserWithEmptyKeyWillHaveUniqueKeySet() { var userWithEmptyKey = User.WithKey(""); - var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, "someOtherAppKey", "{}"); - var client = TestUtil.CreateClient(config, userWithEmptyKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, userWithEmptyKey)) + { + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() { - var client = Client(); - client.Identify(User.WithKey("a new user's key")); var userWithNullKey = User.WithKey(null); - client.Identify(userWithNullKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(userWithNullKey); + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() { - var client = Client(); var userWithEmptyKey = User.WithKey(""); - client.Identify(userWithEmptyKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); - } - - [Fact] - public void UpdatingKeylessUserWillGenerateNewUserWithSameValues() - { - var updatedUser = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn(String.Empty); - var client = Client(); - var previousUser = client.User; - client.Identify(updatedUser); - Assert.NotEqual(updatedUser, previousUser); - Assert.Equal(updatedUser.Avatar, previousUser.Avatar); - Assert.Equal(updatedUser.Country, previousUser.Country); - Assert.Equal(updatedUser.Email, previousUser.Email); - Assert.Equal(updatedUser.FirstName, previousUser.FirstName); - Assert.Equal(updatedUser.LastName, previousUser.LastName); - Assert.Equal(updatedUser.Name, previousUser.Name); - Assert.Equal(updatedUser.IpAddress, previousUser.IpAddress); - Assert.Equal(updatedUser.SecondaryKey, previousUser.SecondaryKey); - Assert.Equal(updatedUser.Custom["somePrivateAttr1"], previousUser.Custom["somePrivateAttr1"]); - Assert.Equal(updatedUser.Custom["somePrivateAttr2"], previousUser.Custom["somePrivateAttr2"]); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(userWithEmptyKey); + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] - public void UpdatingKeylessUserSetsAnonymousToTrue() - { - var updatedUser = User.WithKey(null); - var client = Client(); - var previousUser = client.User; - client.Identify(updatedUser); - Assert.True(client.User.Anonymous); + public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() + { + var user = User.WithKey("") + .AndSecondaryKey("secondary") + .AndIpAddress("10.0.0.1") + .AndCountry("US") + .AndFirstName("John") + .AndLastName("Doe") + .AndName("John Doe") + .AndAvatar("images.google.com/myAvatar") + .AndEmail("test@example.com") + .AndCustomAttribute("attr", "value"); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(user); + User newUser = client.User; + Assert.NotEqual(user.Key, newUser.Key); + Assert.Equal(user.Avatar, newUser.Avatar); + Assert.Equal(user.Country, newUser.Country); + Assert.Equal(user.Email, newUser.Email); + Assert.Equal(user.FirstName, newUser.FirstName); + Assert.Equal(user.LastName, newUser.LastName); + Assert.Equal(user.Name, newUser.Name); + Assert.Equal(user.IpAddress, newUser.IpAddress); + Assert.Equal(user.SecondaryKey, newUser.SecondaryKey); + Assert.Equal(user.Custom["attr"], newUser.Custom["attr"]); + Assert.True(newUser.Anonymous); + } } - + [Fact] public void CanRegisterListener() { - var client = Client(); - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); - client.RegisterFeatureFlagListener("user1-flag", listener); - listenerMgr.FlagWasUpdated("user1-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user1-flag"].ToObject()); + using (var client = Client()) + { + var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; + var listener = new TestListener(); + client.RegisterFeatureFlagListener("user1-flag", listener); + listenerMgr.FlagWasUpdated("user1-flag", 7); + Assert.Equal(7, listener.FeatureFlags["user1-flag"].ToObject()); + } } [Fact] public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerManager() { - var client = Client(); - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); - client.RegisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user2-flag"]); - - client.UnregisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 12); - Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); + using (var client = Client()) + { + var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; + var listener = new TestListener(); + client.RegisterFeatureFlagListener("user2-flag", listener); + listenerMgr.FlagWasUpdated("user2-flag", 7); + Assert.Equal(7, listener.FeatureFlags["user2-flag"]); + + client.UnregisterFeatureFlagListener("user2-flag", listener); + listenerMgr.FlagWasUpdated("user2-flag", 12); + Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); + } } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs b/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs new file mode 100644 index 00000000..83645b1f --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + internal class MockConnectionManager : IConnectionManager + { + public Action ConnectionChanged; + + public MockConnectionManager(bool isOnline) + { + isConnected = isOnline; + } + + bool isConnected; + public bool IsConnected + { + get + { + return isConnected; + } + + set + { + isConnected = value; + } + } + + public void Connect(bool online) + { + IsConnected = online; + ConnectionChanged?.Invoke(IsConnected); + } + } + + internal class MockDeviceInfo : IDeviceInfo + { + private readonly string key; + + public MockDeviceInfo(string key) + { + this.key = key; + } + + public string UniqueDeviceId() + { + return key; + } + } + + internal class MockEventProcessor : IEventProcessor + { + public List Events = new List(); + + public void SendEvent(Event e) + { + Events.Add(e); + } + + public void Flush() { } + + public void Dispose() { } + } + + internal class MockFeatureFlagRequestor : IFeatureFlagRequestor + { + private readonly string _jsonFlags; + + public MockFeatureFlagRequestor(string jsonFlags) + { + _jsonFlags = jsonFlags; + } + + public void Dispose() + { + + } + + public Task FeatureFlagsAsync() + { + var response = new WebResponse(200, _jsonFlags, null); + return Task.FromResult(response); + } + } + + internal class MockFlagCacheManager : IFlagCacheManager + { + private readonly IUserFlagCache _flagCache; + + public MockFlagCacheManager(IUserFlagCache flagCache) + { + _flagCache = flagCache; + } + + public void CacheFlagsFromService(IDictionary flags, User user) + { + _flagCache.CacheFlagsForUser(flags, user); + } + + public FeatureFlag FlagForUser(string flagKey, User user) + { + var flags = FlagsForUser(user); + FeatureFlag featureFlag; + if (flags.TryGetValue(flagKey, out featureFlag)) + { + return featureFlag; + } + + return null; + } + + public IDictionary FlagsForUser(User user) + { + return _flagCache.RetrieveFlags(user); + } + + public void RemoveFlagForUser(string flagKey, User user) + { + var flagsForUser = FlagsForUser(user); + flagsForUser.Remove(flagKey); + + CacheFlagsFromService(flagsForUser, user); + } + + public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user) + { + var flagsForUser = FlagsForUser(user); + flagsForUser[flagKey] = featureFlag; + + CacheFlagsFromService(flagsForUser, user); + } + } + + internal class MockPersister : ISimplePersistance + { + private IDictionary map = new Dictionary(); + + public string GetValue(string key) + { + if (!map.ContainsKey(key)) + return null; + + return map[key]; + } + + public void Save(string key, string value) + { + map[key] = value; + } + } + + internal class MockPollingProcessor : IMobileUpdateProcessor + { + public bool IsRunning + { + get; + set; + } + + public void Dispose() + { + IsRunning = false; + } + + public bool Initialized() + { + return IsRunning; + } + + public Task Start() + { + IsRunning = true; + return Task.FromResult(true); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs b/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs deleted file mode 100644 index cb53ed37..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockConnectionManager : IConnectionManager - { - public Action ConnectionChanged; - - public MockConnectionManager(bool isOnline) - { - isConnected = isOnline; - } - - bool isConnected; - public bool IsConnected - { - get - { - return isConnected; - } - - set - { - isConnected = value; - } - } - - public void Connect(bool online) - { - IsConnected = online; - ConnectionChanged?.Invoke(IsConnected); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs deleted file mode 100644 index 1f0d3bc5..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using LaunchDarkly.Client; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MockEventProcessor : IEventProcessor - { - public List Events = new List(); - - public void SendEvent(Event e) - { - Events.Add(e); - } - - public void Flush() { } - - public void Dispose() { } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs deleted file mode 100644 index 59b6c964..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockFeatureFlagRequestor : IFeatureFlagRequestor - { - private readonly string _jsonFlags; - - public MockFeatureFlagRequestor(string jsonFlags) - { - _jsonFlags = jsonFlags; - } - - public void Dispose() - { - - } - - public Task FeatureFlagsAsync() - { - var response = new WebResponse(200, _jsonFlags, null); - return Task.FromResult(response); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs deleted file mode 100644 index 936c1f46..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using LaunchDarkly.Client; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockFlagCacheManager : IFlagCacheManager - { - private readonly IUserFlagCache _flagCache; - - public MockFlagCacheManager(IUserFlagCache flagCache) - { - _flagCache = flagCache; - } - - public void CacheFlagsFromService(IDictionary flags, User user) - { - _flagCache.CacheFlagsForUser(flags, user); - } - - public FeatureFlag FlagForUser(string flagKey, User user) - { - var flags = FlagsForUser(user); - FeatureFlag featureFlag; - if (flags.TryGetValue(flagKey, out featureFlag)) - { - return featureFlag; - } - - return null; - } - - public IDictionary FlagsForUser(User user) - { - return _flagCache.RetrieveFlags(user); - } - - public void RemoveFlagForUser(string flagKey, User user) - { - var flagsForUser = FlagsForUser(user); - flagsForUser.Remove(flagKey); - - CacheFlagsFromService(flagsForUser, user); - } - - public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user) - { - var flagsForUser = FlagsForUser(user); - flagsForUser[flagKey] = featureFlag; - - CacheFlagsFromService(flagsForUser, user); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs deleted file mode 100644 index 5bbd9105..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MockPollingProcessor : IMobileUpdateProcessor - { - public bool IsRunning - { - get; - set; - } - - public void Dispose() - { - IsRunning = false; - } - - public bool Initialized() - { - return IsRunning; - } - - public Task Start() - { - IsRunning = true; - return Task.FromResult(true); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs deleted file mode 100644 index 5622e9d3..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin.Tests -{ - public static class StubbedConfigAndUserBuilder - { - public static User UserWithAllPropertiesFilledIn(string key) - { - var user = User.WithKey(key); - user.SecondaryKey = "secondaryKey"; - user.IpAddress = "10.0.0.1"; - user.Country = "US"; - user.FirstName = "John"; - user.LastName = "Doe"; - user.Name = user.FirstName + " " + user.LastName; - user.Avatar = "images.google.com/myAvatar"; - user.Email = "someEmail@google.com"; - user.Custom = new Dictionary - { - {"somePrivateAttr1", JToken.FromObject("attributeValue1")}, - {"somePrivateAttr2", JToken.FromObject("attributeValue2")}, - }; - - return user; - } - } - - public class MockPersister : ISimplePersistance - { - private IDictionary map = new Dictionary(); - - public string GetValue(string key) - { - if (!map.ContainsKey(key)) - return null; - - return map[key]; - } - - public void Save(string key, string value) - { - map[key] = value; - } - } - - public class MockDeviceInfo : IDeviceInfo - { - public const string key = "someUniqueKey"; - - public string UniqueDeviceId() - { - return key; - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 224c7bb2..b4e20d86 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -44,21 +44,15 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string { stubbedFlagCache.CacheFlagsForUser(flags, user); } - - var mockOnlineConnectionManager = new MockConnectionManager(true); - var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); - var mockPollingProcessor = new MockPollingProcessor(); - var mockPersister = new MockPersister(); - var mockDeviceInfo = new MockDeviceInfo(); - var featureFlagListener = new FeatureFlagListenerManager(); Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(mockFlagCacheManager) - .WithConnectionManager(mockOnlineConnectionManager) - .WithUpdateProcessor(mockPollingProcessor) - .WithPersister(mockPersister) - .WithDeviceInfo(mockDeviceInfo) - .WithFeatureFlagListenerManager(featureFlagListener); + .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) + .WithConnectionManager(new MockConnectionManager(true)) + .WithEventProcessor(new MockEventProcessor()) + .WithUpdateProcessor(new MockPollingProcessor()) + .WithPersister(new MockPersister()) + .WithDeviceInfo(new MockDeviceInfo("")) + .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); return configuration; } } From c0cd164a9250dcd0049a2d205fbaa951c20a8ee8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 16:42:53 -0700 Subject: [PATCH 026/254] make timeout a parameter instead of a config property --- src/LaunchDarkly.Xamarin/Configuration.cs | 25 ------- src/LaunchDarkly.Xamarin/ILdMobileClient.cs | 6 +- src/LaunchDarkly.Xamarin/LdClient.cs | 65 +++++++------------ .../LdClientTests.cs | 6 +- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 5 +- 5 files changed, 32 insertions(+), 75 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 8a584731..6c777ff5 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -60,12 +60,6 @@ public class Configuration : IMobileConfiguration /// public TimeSpan PollingInterval { get; internal set; } /// - /// How long the client constructor will block awaiting a successful connection to - /// LaunchDarkly. Setting this to 0 will not block and will cause the constructor to return - /// immediately. The default value is 5 seconds. - /// - public TimeSpan StartWaitTime { get; internal set; } - /// /// The timeout when reading data from the EventSource API. The default value is 5 minutes. /// public TimeSpan ReadTimeout { get; internal set; } @@ -157,10 +151,6 @@ public class Configuration : IMobileConfiguration /// private static readonly TimeSpan DefaultEventQueueFrequency = TimeSpan.FromSeconds(5); /// - /// Default value for . - /// - private static readonly TimeSpan DefaultStartWaitTime = TimeSpan.FromSeconds(5); - /// /// Default value for . /// private static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); @@ -205,7 +195,6 @@ public static Configuration Default(string mobileKey) EventQueueCapacity = DefaultEventQueueCapacity, EventQueueFrequency = DefaultEventQueueFrequency, PollingInterval = DefaultPollingInterval, - StartWaitTime = DefaultStartWaitTime, ReadTimeout = DefaultReadTimeout, ReconnectTime = DefaultReconnectTime, HttpClientTimeout = DefaultHttpClientTimeout, @@ -382,20 +371,6 @@ public static Configuration WithPollingInterval(this Configuration configuration return configuration; } - /// - /// Sets how long the client constructor will block awaiting a successful connection to - /// LaunchDarkly. Setting this to 0 will not block and will cause the constructor to return - /// immediately. The default value is 5 seconds. - /// - /// the configuration - /// the length of time to wait - /// the same Configuration instance - public static Configuration WithStartWaitTime(this Configuration configuration, TimeSpan startWaitTime) - { - configuration.StartWaitTime = startWaitTime; - return configuration; - } - /// /// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made. /// diff --git a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs index faac2ecc..39e7b26d 100644 --- a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs +++ b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs @@ -75,8 +75,10 @@ public interface ILdMobileClient : ILdCommonClient /// /// Gets or sets the online status of the client. /// - /// The setter will block and wait on the current thread for the Update processor - /// to either be stopped or started. + /// The setter is equivalent to calling . If you are going from offline + /// to online, and you want to wait until the connection has been established, call + /// and then use await or call Wait() on + /// its return value. /// /// true if online; otherwise, false. bool Online { get; set; } diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index f55c443d..59de7ce4 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -99,8 +99,8 @@ public sealed class LdClient : ILdMobileClient /// fetching feature flags. /// /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use . + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more specific /// to instantiate the single instance of LdClient @@ -110,11 +110,14 @@ public sealed class LdClient : ILdMobileClient /// The mobile key given to you by LaunchDarkly. /// The user needed for client operations. Must not be null. /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static LdClient Init(string mobileKey, User user) + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) { var config = Configuration.Default(mobileKey); - return Init(config, user); + return Init(config, user, maxWaitTime); } /// @@ -143,8 +146,8 @@ public static async Task InitAsync(string mobileKey, User user) /// fetching Feature Flags. /// /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use . + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic /// to instantiate the single instance of LdClient @@ -154,13 +157,20 @@ public static async Task InitAsync(string mobileKey, User user) /// The client configuration object /// The user needed for client operations. Must not be null. /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static LdClient Init(Configuration config, User user) + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) { CreateInstance(config, user); if (Instance.Online) { - Instance.StartUpdateProcessor(); + if (!Instance.StartUpdateProcessor(maxWaitTime)) + { + Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", + maxWaitTime.TotalMilliseconds); + } } return Instance; @@ -207,10 +217,10 @@ static void CreateInstance(Configuration configuration, User user) Instance.Version); } - void StartUpdateProcessor() + bool StartUpdateProcessor(TimeSpan maxWaitTime) { var initTask = updateProcessor.Start(); - var unused = initTask.Wait(Config.StartWaitTime); + return initTask.Wait(maxWaitTime); } Task StartUpdateProcessorAsync() @@ -233,13 +243,10 @@ void SetupConnectionManager() /// public bool Online { - get - { - return online; - } + get => online; set { - SetOnlineAsync(value).Wait(); + var doNotAwaitResult = SetOnlineAsync(value); } } @@ -451,12 +458,6 @@ public async Task IdentifyAsync(User user) eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); } - void RestartUpdateProcessor() - { - ClearAndSetUpdateProcessor(); - StartUpdateProcessor(); - } - async Task RestartUpdateProcessorAsync() { ClearAndSetUpdateProcessor(); @@ -525,22 +526,6 @@ public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener l flagListenerManager.UnregisterListener(listener, flagKey); } - internal void EnterBackground() - { - // if using Streaming, processor needs to be reset - if (Config.IsStreamingEnabled) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - RestartUpdateProcessor(); - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); - } - else - { - PingPollingProcessor(); - } - } - internal async Task EnterBackgroundAsync() { // if using Streaming, processor needs to be reset @@ -557,12 +542,6 @@ internal async Task EnterBackgroundAsync() } } - internal void EnterForeground() - { - ResetProcessorForForeground(); - RestartUpdateProcessor(); - } - internal async Task EnterForegroundAsync() { ResetProcessorForForeground(); diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 69c7dacf..7c4ea07d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -24,14 +24,14 @@ public void CanCreateClientWithConfigAndUser() [Fact] public void CannotCreateClientWithNullConfig() { - Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"), TimeSpan.Zero)); } [Fact] public void CannotCreateClientWithNullUser() { Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); - Assert.Throws(() => LdClient.Init(config, null)); + Assert.Throws(() => LdClient.Init(config, null, TimeSpan.Zero)); } [Fact] @@ -65,7 +65,7 @@ public void SharedClientIsTheOnlyClientAvailable() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - var client = LdClient.Init(config, user); + var client = LdClient.Init(config, user, TimeSpan.Zero); try { Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 224c7bb2..af8e9005 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using LaunchDarkly.Client; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,7 +19,7 @@ public static LdClient CreateClient(Configuration config, User user) { lock (ClientInstanceLock) { - LdClient client = LdClient.Init(config, user); + LdClient client = LdClient.Init(config, user, TimeSpan.Zero); LdClient.Instance = null; return client; } From 5305820b0f179aba2b7e52994b0b282d79883481 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Mon, 23 Jul 2018 16:47:54 -0700 Subject: [PATCH 027/254] Remove @ashanbrown from codeowners --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 44429ee1..8b137891 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @ashanbrown + From 77816f2e1fb4ee54ce462957982d6037a275616f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 11:38:51 -0700 Subject: [PATCH 028/254] propagate exception if polling task fails permanently --- src/LaunchDarkly.Xamarin/LdClient.cs | 27 +++++++++++++++++-- .../MobilePollingProcessor.cs | 10 ++++--- .../LdClientTests.cs | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1b76f165..2ee0ff77 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -220,7 +220,14 @@ static void CreateInstance(Configuration configuration, User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { var initTask = updateProcessor.Start(); - return initTask.Wait(maxWaitTime); + try + { + return initTask.Wait(maxWaitTime); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } } Task StartUpdateProcessorAsync() @@ -427,7 +434,14 @@ public void Flush() /// public void Identify(User user) { - IdentifyAsync(user).Wait(); + try + { + IdentifyAsync(user).Wait(); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } } /// @@ -587,5 +601,14 @@ async Task PingPollingProcessorAsync() await pollingProcessor.PingAndWait(); } } + + private Exception UnwrapAggregateException(AggregateException e) + { + if (e.InnerExceptions.Count == 1) + { + return e.InnerExceptions[0]; + } + return e; + } } } \ No newline at end of file diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs index 65f13572..b36e341c 100644 --- a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs +++ b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs @@ -19,8 +19,8 @@ internal class MobilePollingProcessor : IMobileUpdateProcessor private readonly TimeSpan pollingInterval; private readonly TaskCompletionSource _startTask; private readonly TaskCompletionSource _stopTask; - private static int UNINITIALIZED = 0; - private static int INITIALIZED = 1; + private const int UNINITIALIZED = 0; + private const int INITIALIZED = 1; private int _initialized = UNINITIALIZED; private volatile bool _disposed; @@ -80,7 +80,7 @@ private async Task UpdateTaskAsync() _flagCacheManager.CacheFlagsFromService(flagsDictionary, user); //We can't use bool in CompareExchange because it is not a reference type. - if (Interlocked.CompareExchange(ref _initialized, INITIALIZED, UNINITIALIZED) == 0) + if (Interlocked.CompareExchange(ref _initialized, INITIALIZED, UNINITIALIZED) == UNINITIALIZED) { _startTask.SetResult(true); Log.Info("Initialized LaunchDarkly Polling Processor."); @@ -91,6 +91,10 @@ private async Task UpdateTaskAsync() { Log.ErrorFormat("Error Updating features: '{0}'", Util.ExceptionMessage(ex)); Log.Error("Received 401 error, no further polling requests will be made since SDK key is invalid"); + if (_initialized == UNINITIALIZED) + { + _startTask.SetException(ex); + } ((IDisposable)this).Dispose(); } catch (Exception ex) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index f164bcd9..e9cb9d63 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -44,7 +44,7 @@ public void IdentifyWithNullUserThrowsException() { using (var client = Client()) { - Assert.Throws(() => client.Identify(null)); + Assert.Throws(() => client.Identify(null)); } } From 05ae673a930ce3ecaa32421dacbe3bf8a09bd568 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 13:39:16 -0700 Subject: [PATCH 029/254] validate maxWaitTime --- src/LaunchDarkly.Xamarin/LdClient.cs | 5 +++++ .../LdClientTests.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1b76f165..d32d055e 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -162,6 +162,11 @@ public static async Task InitAsync(string mobileKey, User user) /// an uninitialized state. public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) { + if (maxWaitTime.Ticks < 0 && maxWaitTime != Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(maxWaitTime)); + } + CreateInstance(config, user); if (Instance.Online) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index f164bcd9..85ba446f 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -28,6 +28,27 @@ public void CannotCreateClientWithNullUser() Assert.Throws(() => LdClient.Init(config, null, TimeSpan.Zero)); } + [Fact] + public void CannotCreateClientWithNegativeWaitTime() + { + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.FromMilliseconds(-2))); + } + + [Fact] + public void CanCreateClientWithInfiniteWaitTime() + { + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + try + { + using (var client = LdClient.Init(config, simpleUser, System.Threading.Timeout.InfiniteTimeSpan)) { } + } + finally + { + LdClient.Instance = null; + } + } + [Fact] public void IdentifyUpdatesTheUser() { From f2ac11e38f0ddafbe54fef641c2a291c56c27deb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 13:51:41 -0700 Subject: [PATCH 030/254] send an initial identify event when client is created --- src/LaunchDarkly.Xamarin/LdClient.cs | 1152 +++++++++-------- .../LdClientEventTests.cs | 109 +- 2 files changed, 636 insertions(+), 625 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index d32d055e..323c5b3b 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -1,596 +1,598 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Common.Logging; -using LaunchDarkly.Client; -using LaunchDarkly.Common; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin -{ - /// - /// A client for the LaunchDarkly API. Client instances are thread-safe. Your application should instantiate - /// a single LdClient for the lifetime of their application. - /// - public sealed class LdClient : ILdMobileClient - { - private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); - - /// - /// The singleton instance used by your application throughout its lifetime, can only be created once. - /// - /// Use the designated static method - /// to set this LdClient instance. - /// - /// The LdClient instance. - public static LdClient Instance { get; internal set; } - - /// - /// The Configuration instance used to setup the LdClient. - /// - /// The Configuration instance. - public Configuration Config { get; private set; } - - /// - /// The User for the LdClient operations. - /// - /// The User. - public User User { get; private set; } - - object myLockObjForConnectionChange = new object(); - object myLockObjForUserUpdate = new object(); - - IFlagCacheManager flagCacheManager; - IConnectionManager connectionManager; - IMobileUpdateProcessor updateProcessor; - IEventProcessor eventProcessor; - ISimplePersistance persister; - IDeviceInfo deviceInfo; - EventFactory eventFactory = EventFactory.Default; - IFeatureFlagListenerManager flagListenerManager; - - SemaphoreSlim connectionLock; - - // private constructor prevents initialization of this class - // without using WithConfigAnduser(config, user) - LdClient() { } - - LdClient(Configuration configuration, User user) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Common.Logging; +using LaunchDarkly.Client; +using LaunchDarkly.Common; +using Newtonsoft.Json.Linq; + +namespace LaunchDarkly.Xamarin +{ + /// + /// A client for the LaunchDarkly API. Client instances are thread-safe. Your application should instantiate + /// a single LdClient for the lifetime of their application. + /// + public sealed class LdClient : ILdMobileClient + { + private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); + + /// + /// The singleton instance used by your application throughout its lifetime, can only be created once. + /// + /// Use the designated static method + /// to set this LdClient instance. + /// + /// The LdClient instance. + public static LdClient Instance { get; internal set; } + + /// + /// The Configuration instance used to setup the LdClient. + /// + /// The Configuration instance. + public Configuration Config { get; private set; } + + /// + /// The User for the LdClient operations. + /// + /// The User. + public User User { get; private set; } + + object myLockObjForConnectionChange = new object(); + object myLockObjForUserUpdate = new object(); + + IFlagCacheManager flagCacheManager; + IConnectionManager connectionManager; + IMobileUpdateProcessor updateProcessor; + IEventProcessor eventProcessor; + ISimplePersistance persister; + IDeviceInfo deviceInfo; + EventFactory eventFactory = EventFactory.Default; + IFeatureFlagListenerManager flagListenerManager; + + SemaphoreSlim connectionLock; + + // private constructor prevents initialization of this class + // without using WithConfigAnduser(config, user) + LdClient() { } + + LdClient(Configuration configuration, User user) { if (configuration == null) { throw new ArgumentNullException("configuration"); - } + } if (user == null) { throw new ArgumentNullException("user"); - } - - Config = configuration; - - connectionLock = new SemaphoreSlim(1, 1); - - persister = Factory.CreatePersister(configuration); - deviceInfo = Factory.CreateDeviceInfo(configuration); - flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - - // If you pass in a user with a null or blank key, one will be assigned to them. - if (String.IsNullOrEmpty(user.Key)) - { - User = UserWithUniqueKey(user); - } - else - { - User = user; - } - - flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); - connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); - eventProcessor = Factory.CreateEventProcessor(configuration); - - SetupConnectionManager(); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching feature flags. - /// - /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, up to the specified timeout. - /// If you would rather this happen in an async fashion you can use . - /// - /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - /// The maximum length of time to wait for the client to initialize. - /// If this time elapses, the method will not throw an exception but will return the client in - /// an uninitialized state. - public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) - { - var config = Configuration.Default(mobileKey); - - return Init(config, user, maxWaitTime); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching feature flags. This constructor should be used if you do not want to wait - /// for the client to finish initializing and receive the first response - /// from the LaunchDarkly service. - /// - /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static async Task InitAsync(string mobileKey, User user) - { - var config = Configuration.Default(mobileKey); - - return await InitAsync(config, user); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching Feature Flags. - /// - /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, up to the specified timeout. - /// If you would rather this happen in an async fashion you can use . - /// - /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The client configuration object - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - /// The maximum length of time to wait for the client to initialize. - /// If this time elapses, the method will not throw an exception but will return the client in - /// an uninitialized state. - public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) - { + } + + Config = configuration; + + connectionLock = new SemaphoreSlim(1, 1); + + persister = Factory.CreatePersister(configuration); + deviceInfo = Factory.CreateDeviceInfo(configuration); + flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); + + // If you pass in a user with a null or blank key, one will be assigned to them. + if (String.IsNullOrEmpty(user.Key)) + { + User = UserWithUniqueKey(user); + } + else + { + User = user; + } + + flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); + connectionManager = Factory.CreateConnectionManager(configuration); + updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); + eventProcessor = Factory.CreateEventProcessor(configuration); + + eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(User)); + + SetupConnectionManager(); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching feature flags. + /// + /// This constructor will wait and block on the current thread until initialization and the + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . + /// + /// This is the creation point for LdClient, you must use this static method or the more specific + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The mobile key given to you by LaunchDarkly. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) + { + var config = Configuration.Default(mobileKey); + + return Init(config, user, maxWaitTime); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching feature flags. This constructor should be used if you do not want to wait + /// for the client to finish initializing and receive the first response + /// from the LaunchDarkly service. + /// + /// This is the creation point for LdClient, you must use this static method or the more specific + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The mobile key given to you by LaunchDarkly. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + public static async Task InitAsync(string mobileKey, User user) + { + var config = Configuration.Default(mobileKey); + + return await InitAsync(config, user); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching Feature Flags. + /// + /// This constructor will wait and block on the current thread until initialization and the + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . + /// + /// This is the creation point for LdClient, you must use this static method or the more basic + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The client configuration object + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) + { if (maxWaitTime.Ticks < 0 && maxWaitTime != Timeout.InfiniteTimeSpan) { throw new ArgumentOutOfRangeException(nameof(maxWaitTime)); - } - - CreateInstance(config, user); - - if (Instance.Online) - { + } + + CreateInstance(config, user); + + if (Instance.Online) + { if (!Instance.StartUpdateProcessor(maxWaitTime)) { Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", maxWaitTime.TotalMilliseconds); - } - } - - return Instance; - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching Feature Flags. This constructor should be used if you do not want to wait - /// for the IUpdateProcessor instance to finish initializing and receive the first response - /// from the LaunchDarkly service. - /// - /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The client configuration object - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static Task InitAsync(Configuration config, User user) - { - CreateInstance(config, user); - - if (Instance.Online) - { - Task t = Instance.StartUpdateProcessorAsync(); - return t.ContinueWith((result) => Instance); - } - else - { - return Task.FromResult(Instance); - } - } - - static void CreateInstance(Configuration configuration, User user) - { + } + } + + return Instance; + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching Feature Flags. This constructor should be used if you do not want to wait + /// for the IUpdateProcessor instance to finish initializing and receive the first response + /// from the LaunchDarkly service. + /// + /// This is the creation point for LdClient, you must use this static method or the more basic + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The client configuration object + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + public static Task InitAsync(Configuration config, User user) + { + CreateInstance(config, user); + + if (Instance.Online) + { + Task t = Instance.StartUpdateProcessorAsync(); + return t.ContinueWith((result) => Instance); + } + else + { + return Task.FromResult(Instance); + } + } + + static void CreateInstance(Configuration configuration, User user) + { if (Instance != null) - { + { throw new Exception("LdClient instance already exists."); - } - - Instance = new LdClient(configuration, user); - Log.InfoFormat("Initialized LaunchDarkly Client {0}", - Instance.Version); - } - - bool StartUpdateProcessor(TimeSpan maxWaitTime) - { - var initTask = updateProcessor.Start(); - return initTask.Wait(maxWaitTime); - } - - Task StartUpdateProcessorAsync() - { - return updateProcessor.Start(); - } - - void SetupConnectionManager() - { - if (connectionManager is MobileConnectionManager mobileConnectionManager) - { - mobileConnectionManager.ConnectionChanged += MobileConnectionManager_ConnectionChanged; - Log.InfoFormat("The mobile client connection changed online to {0}", - connectionManager.IsConnected); - } - online = connectionManager.IsConnected; - } - - bool online; - /// - public bool Online - { - get => online; - set - { - var doNotAwaitResult = SetOnlineAsync(value); - } - } - - public async Task SetOnlineAsync(bool value) - { - await connectionLock.WaitAsync(); - online = value; - try - { - if (online) - { - await RestartUpdateProcessorAsync(); - } - else - { - ClearUpdateProcessor(); - } - } - finally - { - connectionLock.Release(); - } - - return; - } - - void MobileConnectionManager_ConnectionChanged(bool isOnline) - { - Online = isOnline; - } - - /// - public bool BoolVariation(string key, bool defaultValue = false) - { - return VariationWithType(key, defaultValue, JTokenType.Boolean).Value(); - } - - /// - public string StringVariation(string key, string defaultValue) - { - var value = VariationWithType(key, defaultValue, JTokenType.String); - if (value != null) - { - return value.Value(); - } - - return null; - } - - /// - public float FloatVariation(string key, float defaultValue = 0) - { - return VariationWithType(key, defaultValue, JTokenType.Float).Value(); - } - - /// - public int IntVariation(string key, int defaultValue = 0) - { - return VariationWithType(key, defaultValue, JTokenType.Integer).Value(); - } - - /// - public JToken JsonVariation(string key, JToken defaultValue) - { - return VariationWithType(key, defaultValue, null); - } - - JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jtokenType) - { - var returnedFlagValue = Variation(featureKey, defaultValue); - if (returnedFlagValue != null && jtokenType != null && !returnedFlagValue.Type.Equals(jtokenType)) - { - Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", - jtokenType, - returnedFlagValue.Type, - featureKey); - - return defaultValue; - } - - return returnedFlagValue; - } - - JToken Variation(string featureKey, JToken defaultValue) - { - FeatureFlagEvent featureFlagEvent = FeatureFlagEvent.Default(featureKey); - FeatureRequestEvent featureRequestEvent; - - if (!Initialized()) - { - Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); - return defaultValue; - } - - var flag = flagCacheManager.FlagForUser(featureKey, User); - if (flag != null) - { - featureFlagEvent = new FeatureFlagEvent(featureKey, flag); - var value = flag.value; - if (value == null || value.Type == JTokenType.Null) { - featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, - User, - defaultValue); - value = defaultValue; - } else { - featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, - User, - flag.variation, - flag.value, - defaultValue); - } - eventProcessor.SendEvent(featureRequestEvent); - return value; - } - - Log.InfoFormat("Unknown feature flag {0}; returning default value", - featureKey); - featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, - User, - defaultValue); - eventProcessor.SendEvent(featureRequestEvent); - return defaultValue; - } - - /// - public IDictionary AllFlags() - { - if (IsOffline()) - { - Log.Warn("AllFlags() was called when client is in offline mode. Returning null."); - return null; - } - if (!Initialized()) - { - Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); - return null; - } - - return flagCacheManager.FlagsForUser(User) - .ToDictionary(p => p.Key, p => p.Value.value); - } - - /// - public void Track(string eventName, JToken data) - { - eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); - } - - /// - public void Track(string eventName) - { - Track(eventName, null); - } - - /// - public bool Initialized() - { - //bool isInited = Instance != null; - //return isInited && Online; - // TODO: This method needs to be fixed to actually check whether the update processor has initialized. - // The previous logic (above) was meaningless because this method is not static, so by definition you - // do have a client instance if we've gotten here. But that doesn't mean it is initialized. - return Online; - } - - /// - public bool IsOffline() - { - return !online; - } - - /// - public void Flush() - { - eventProcessor.Flush(); - } - - /// - public void Identify(User user) - { - IdentifyAsync(user).Wait(); - } - - /// - public async Task IdentifyAsync(User user) - { - if (user == null) - { - throw new ArgumentNullException("user"); - } - - User userWithKey = user; - if (String.IsNullOrEmpty(user.Key)) - { - userWithKey = UserWithUniqueKey(user); - } - - await connectionLock.WaitAsync(); - try - { - User = userWithKey; - await RestartUpdateProcessorAsync(); - } - finally - { - connectionLock.Release(); - } - - eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); - } - - async Task RestartUpdateProcessorAsync() - { - ClearAndSetUpdateProcessor(); - await StartUpdateProcessorAsync(); - } - - void ClearAndSetUpdateProcessor() - { - ClearUpdateProcessor(); - updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager); - } - - void ClearUpdateProcessor() - { - if (updateProcessor != null) - { - updateProcessor.Dispose(); - updateProcessor = new NullUpdateProcessor(); - } - } - - User UserWithUniqueKey(User user) - { - string uniqueId = deviceInfo.UniqueDeviceId(); + } + + Instance = new LdClient(configuration, user); + Log.InfoFormat("Initialized LaunchDarkly Client {0}", + Instance.Version); + } + + bool StartUpdateProcessor(TimeSpan maxWaitTime) + { + var initTask = updateProcessor.Start(); + return initTask.Wait(maxWaitTime); + } + + Task StartUpdateProcessorAsync() + { + return updateProcessor.Start(); + } + + void SetupConnectionManager() + { + if (connectionManager is MobileConnectionManager mobileConnectionManager) + { + mobileConnectionManager.ConnectionChanged += MobileConnectionManager_ConnectionChanged; + Log.InfoFormat("The mobile client connection changed online to {0}", + connectionManager.IsConnected); + } + online = connectionManager.IsConnected; + } + + bool online; + /// + public bool Online + { + get => online; + set + { + var doNotAwaitResult = SetOnlineAsync(value); + } + } + + public async Task SetOnlineAsync(bool value) + { + await connectionLock.WaitAsync(); + online = value; + try + { + if (online) + { + await RestartUpdateProcessorAsync(); + } + else + { + ClearUpdateProcessor(); + } + } + finally + { + connectionLock.Release(); + } + + return; + } + + void MobileConnectionManager_ConnectionChanged(bool isOnline) + { + Online = isOnline; + } + + /// + public bool BoolVariation(string key, bool defaultValue = false) + { + return VariationWithType(key, defaultValue, JTokenType.Boolean).Value(); + } + + /// + public string StringVariation(string key, string defaultValue) + { + var value = VariationWithType(key, defaultValue, JTokenType.String); + if (value != null) + { + return value.Value(); + } + + return null; + } + + /// + public float FloatVariation(string key, float defaultValue = 0) + { + return VariationWithType(key, defaultValue, JTokenType.Float).Value(); + } + + /// + public int IntVariation(string key, int defaultValue = 0) + { + return VariationWithType(key, defaultValue, JTokenType.Integer).Value(); + } + + /// + public JToken JsonVariation(string key, JToken defaultValue) + { + return VariationWithType(key, defaultValue, null); + } + + JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jtokenType) + { + var returnedFlagValue = Variation(featureKey, defaultValue); + if (returnedFlagValue != null && jtokenType != null && !returnedFlagValue.Type.Equals(jtokenType)) + { + Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", + jtokenType, + returnedFlagValue.Type, + featureKey); + + return defaultValue; + } + + return returnedFlagValue; + } + + JToken Variation(string featureKey, JToken defaultValue) + { + FeatureFlagEvent featureFlagEvent = FeatureFlagEvent.Default(featureKey); + FeatureRequestEvent featureRequestEvent; + + if (!Initialized()) + { + Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); + return defaultValue; + } + + var flag = flagCacheManager.FlagForUser(featureKey, User); + if (flag != null) + { + featureFlagEvent = new FeatureFlagEvent(featureKey, flag); + var value = flag.value; + if (value == null || value.Type == JTokenType.Null) { + featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, + User, + defaultValue); + value = defaultValue; + } else { + featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, + User, + flag.variation, + flag.value, + defaultValue); + } + eventProcessor.SendEvent(featureRequestEvent); + return value; + } + + Log.InfoFormat("Unknown feature flag {0}; returning default value", + featureKey); + featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, + User, + defaultValue); + eventProcessor.SendEvent(featureRequestEvent); + return defaultValue; + } + + /// + public IDictionary AllFlags() + { + if (IsOffline()) + { + Log.Warn("AllFlags() was called when client is in offline mode. Returning null."); + return null; + } + if (!Initialized()) + { + Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); + return null; + } + + return flagCacheManager.FlagsForUser(User) + .ToDictionary(p => p.Key, p => p.Value.value); + } + + /// + public void Track(string eventName, JToken data) + { + eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); + } + + /// + public void Track(string eventName) + { + Track(eventName, null); + } + + /// + public bool Initialized() + { + //bool isInited = Instance != null; + //return isInited && Online; + // TODO: This method needs to be fixed to actually check whether the update processor has initialized. + // The previous logic (above) was meaningless because this method is not static, so by definition you + // do have a client instance if we've gotten here. But that doesn't mean it is initialized. + return Online; + } + + /// + public bool IsOffline() + { + return !online; + } + + /// + public void Flush() + { + eventProcessor.Flush(); + } + + /// + public void Identify(User user) + { + IdentifyAsync(user).Wait(); + } + + /// + public async Task IdentifyAsync(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + User userWithKey = user; + if (String.IsNullOrEmpty(user.Key)) + { + userWithKey = UserWithUniqueKey(user); + } + + await connectionLock.WaitAsync(); + try + { + User = userWithKey; + await RestartUpdateProcessorAsync(); + } + finally + { + connectionLock.Release(); + } + + eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); + } + + async Task RestartUpdateProcessorAsync() + { + ClearAndSetUpdateProcessor(); + await StartUpdateProcessorAsync(); + } + + void ClearAndSetUpdateProcessor() + { + ClearUpdateProcessor(); + updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager); + } + + void ClearUpdateProcessor() + { + if (updateProcessor != null) + { + updateProcessor.Dispose(); + updateProcessor = new NullUpdateProcessor(); + } + } + + User UserWithUniqueKey(User user) + { + string uniqueId = deviceInfo.UniqueDeviceId(); return new User(user) { Key = uniqueId, Anonymous = true - }; - } - - void IDisposable.Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - void Dispose(bool disposing) - { - if (disposing) - { - Log.InfoFormat("The mobile client is being disposed"); - updateProcessor.Dispose(); - eventProcessor.Dispose(); - } - } - - /// - public Version Version - { - get - { - return MobileClientEnvironment.Instance.Version; - } - } - - /// - public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.RegisterListener(listener, flagKey); - } - - /// - public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.UnregisterListener(listener, flagKey); - } - - internal async Task EnterBackgroundAsync() - { - // if using Streaming, processor needs to be reset - if (Config.IsStreamingEnabled) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - await RestartUpdateProcessorAsync(); - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); - } - else - { - await PingPollingProcessorAsync(); - } - } - - internal async Task EnterForegroundAsync() - { - ResetProcessorForForeground(); - await RestartUpdateProcessorAsync(); - } - - void ResetProcessorForForeground() - { - string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); - if (didBackground.Equals("true")) - { - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); - ClearUpdateProcessor(); - Config.IsStreamingEnabled = true; - } - } - - internal void BackgroundTick() - { - PingPollingProcessor(); - } - - internal async Task BackgroundTickAsync() - { - await PingPollingProcessorAsync(); - } - - void PingPollingProcessor() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - var waitTask = pollingProcessor.PingAndWait(); - waitTask.Wait(); - } - } - - async Task PingPollingProcessorAsync() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - await pollingProcessor.PingAndWait(); - } - } - } + }; + } + + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + void Dispose(bool disposing) + { + if (disposing) + { + Log.InfoFormat("The mobile client is being disposed"); + updateProcessor.Dispose(); + eventProcessor.Dispose(); + } + } + + /// + public Version Version + { + get + { + return MobileClientEnvironment.Instance.Version; + } + } + + /// + public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) + { + flagListenerManager.RegisterListener(listener, flagKey); + } + + /// + public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) + { + flagListenerManager.UnregisterListener(listener, flagKey); + } + + internal async Task EnterBackgroundAsync() + { + // if using Streaming, processor needs to be reset + if (Config.IsStreamingEnabled) + { + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + await RestartUpdateProcessorAsync(); + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + } + else + { + await PingPollingProcessorAsync(); + } + } + + internal async Task EnterForegroundAsync() + { + ResetProcessorForForeground(); + await RestartUpdateProcessorAsync(); + } + + void ResetProcessorForForeground() + { + string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); + if (didBackground.Equals("true")) + { + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); + ClearUpdateProcessor(); + Config.IsStreamingEnabled = true; + } + } + + internal void BackgroundTick() + { + PingPollingProcessor(); + } + + internal async Task BackgroundTickAsync() + { + await PingPollingProcessorAsync(); + } + + void PingPollingProcessor() + { + var pollingProcessor = updateProcessor as MobilePollingProcessor; + if (pollingProcessor != null) + { + var waitTask = pollingProcessor.PingAndWait(); + waitTask.Wait(); + } + } + + async Task PingPollingProcessorAsync() + { + var pollingProcessor = updateProcessor as MobilePollingProcessor; + if (pollingProcessor != null) + { + await pollingProcessor.PingAndWait(); + } + } + } } \ No newline at end of file diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs index 5e8c58ce..074605e4 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -23,11 +23,9 @@ public void IdentifySendsIdentifyEvent() { User user1 = User.WithKey("userkey1"); client.Identify(user1); - Assert.Collection(eventProcessor.Events, e => - { - IdentifyEvent ie = Assert.IsType(e); - Assert.Equal(user1.Key, ie.User.Key); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), // there's always an initial identify event + e => CheckIdentifyEvent(e, user1)); } } @@ -38,13 +36,14 @@ public void TrackSendsCustomEvent() { JToken data = new JValue("hi"); client.Track("eventkey", data); - Assert.Collection(eventProcessor.Events, e => - { - CustomEvent ce = Assert.IsType(e); - Assert.Equal("eventkey", ce.Key); - Assert.Equal(user.Key, ce.User.Key); - Assert.Equal(data, ce.JsonData); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + CustomEvent ce = Assert.IsType(e); + Assert.Equal("eventkey", ce.Key); + Assert.Equal(user.Key, ce.User.Key); + Assert.Equal(data, ce.JsonData); + }); } } @@ -58,17 +57,18 @@ public void VariationSendsFeatureEventForValidFlag() { string result = client.StringVariation("flag", "b"); Assert.Equal("a", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("a", fe.Value); - Assert.Equal(1, fe.Variation); - Assert.Equal(1000, fe.Version); - Assert.Equal("b", fe.Default); - Assert.True(fe.TrackEvents); - Assert.Equal(2000, fe.DebugEventsUntilDate); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Equal(2000, fe.DebugEventsUntilDate); + }); } } @@ -82,15 +82,16 @@ public void FeatureEventUsesFlagVersionIfProvided() { string result = client.StringVariation("flag", "b"); Assert.Equal("a", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("a", fe.Value); - Assert.Equal(1, fe.Variation); - Assert.Equal(1500, fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1500, fe.Version); + Assert.Equal("b", fe.Default); + }); } } @@ -103,15 +104,16 @@ public void VariationSendsFeatureEventForDefaultValue() { string result = client.StringVariation("flag", "b"); Assert.Equal("b", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("b", fe.Value); - Assert.Null(fe.Variation); - Assert.Equal(1000, fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + }); } } @@ -122,16 +124,23 @@ public void VariationSendsFeatureEventForUnknownFlag() { string result = client.StringVariation("flag", "b"); Assert.Equal("b", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("b", fe.Value); - Assert.Null(fe.Variation); - Assert.Null(fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + }); } } + + private void CheckIdentifyEvent(Event e, User u) + { + IdentifyEvent ie = Assert.IsType(e); + Assert.Equal(u.Key, ie.User.Key); + } } } From 3f2d0c090fd0c68cbd853fc66261be93474f274c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 17:06:18 -0700 Subject: [PATCH 031/254] misc project file & test cleanup --- .../LaunchDarkly.Xamarin.csproj | 24 +- .../MobileStreamingProcessorTests.cs | 414 +++++++++--------- 2 files changed, 223 insertions(+), 215 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 6b163c85..2d7142ff 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -2,25 +2,31 @@ 1.0.0-beta11 - netstandard1.6;netstandard2.0;net45 + Library + LaunchDarkly.Xamarin + LaunchDarkly.Xamarin - - - false + + + netstandard1.6;netstandard2.0;net45 - - obj\Release\netstandard2.0 - - + + netstandard1.6;netstandard2.0 + - + + + + + + diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index 83f99c41..fcb0e08e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -1,211 +1,213 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using LaunchDarkly.Client; -using LaunchDarkly.Common; -using LaunchDarkly.EventSource; -using Newtonsoft.Json; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MobileStreamingProcessorTests - { +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LaunchDarkly.Client; +using LaunchDarkly.Common; +using LaunchDarkly.EventSource; +using Newtonsoft.Json; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class MobileStreamingProcessorTests + { private const string initialFlagsJson = "{" + "\"int-flag\":{\"value\":15,\"version\":100}," + "\"float-flag\":{\"value\":13.5,\"version\":100}," + "\"string-flag\":{\"value\":\"markw@magenic.com\",\"version\":100}" + "}"; - - User user = User.WithKey("user key"); - EventSourceMock mockEventSource; - TestEventSourceFactory eventSourceFactory; - IFlagCacheManager mockFlagCacheMgr; - - private IMobileUpdateProcessor MobileStreamingProcessorStarted() - { - mockEventSource = new EventSourceMock(); - eventSourceFactory = new TestEventSourceFactory(mockEventSource); - // stub with an empty InMemoryCache, so Stream updates can be tested - mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); - var config = Configuration.Default("someKey") - .WithConnectionManager(new MockConnectionManager(true)) - .WithIsStreamingEnabled(true) - .WithFlagCacheManager(mockFlagCacheMgr); - - var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); - processor.Start(); - return processor; - } - - [Fact] - public void CanCreateMobileStreamingProcFromFactory() - { - var streamingProcessor = MobileStreamingProcessorStarted(); - Assert.IsType(streamingProcessor); - } - - [Fact] - public void PUTstoresFeatureFlags() - { - var streamingProcessor = MobileStreamingProcessorStarted(); - // should be empty before PUT message arrives - var flagsInCache = mockFlagCacheMgr.FlagsForUser(user); - Assert.Empty(flagsInCache); - - PUTMessageSentToProcessor(); - flagsInCache = mockFlagCacheMgr.FlagsForUser(user); - Assert.NotEmpty(flagsInCache); - int intFlagValue = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagValue); - } - - [Fact] - public void PATCHupdatesFeatureFlag() - { - // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - //PATCH to update 1 flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlag(), null), "patch"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - //verify flag has changed - int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(99, flagFromPatch); - } - - [Fact] - public void PATCHdoesnotUpdateFlagIfVersionIsLower() - { - // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - //PATCH to update 1 flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlagWithLowerVersion(), null), "patch"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - //verify flag has not changed - int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, flagFromPatch); - } - - [Fact] - public void DELETEremovesFeatureFlag() - { - // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - // DELETE int-flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlag(), null), "delete"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - // verify flag was deleted - Assert.Null(mockFlagCacheMgr.FlagForUser("int-flag", user)); - } - - [Fact] - public void DELTEdoesnotRemoveFeatureFlagIfVersionIsLower() - { - // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - // DELETE int-flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlagWithLowerVersion(), null), "delete"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - // verify flag was not deleted - Assert.NotNull(mockFlagCacheMgr.FlagForUser("int-flag", user)); - } - - string UpdatedFlag() - { - var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":999,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; - return updatedFlagAsJson; - } - - string DeleteFlag() - { - var flagToDelete = "{\"key\":\"int-flag\",\"version\":1214}"; - return flagToDelete; - } - - string UpdatedFlagWithLowerVersion() - { - var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":1,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; - return updatedFlagAsJson; - } - - string DeleteFlagWithLowerVersion() - { - var flagToDelete = "{\"key\":\"int-flag\",\"version\":1}"; - return flagToDelete; - } - - void PUTMessageSentToProcessor() - { - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); - mockEventSource.RaiseMessageRcvd(eventArgs); - } - } - - class TestEventSourceFactory - { - public StreamProperties ReceivedProperties { get; private set; } - public IDictionary ReceivedHeaders { get; private set; } - IEventSource _eventSource; - - public TestEventSourceFactory(IEventSource eventSource) - { - _eventSource = eventSource; - } - - public StreamManager.EventSourceCreator Create() - { - return (StreamProperties sp, IDictionary headers) => - { - ReceivedProperties = sp; - ReceivedHeaders = headers; - return _eventSource; - }; - } - } - - class EventSourceMock : IEventSource - { - public ReadyState ReadyState => throw new NotImplementedException(); - - public event EventHandler Opened; - public event EventHandler Closed; - public event EventHandler MessageReceived; - public event EventHandler CommentReceived; - public event EventHandler Error; - - public void Close() - { - - } - - public Task StartAsync() - { - return Task.CompletedTask; - } - - public void RaiseMessageRcvd(MessageReceivedEventArgs eventArgs) - { - MessageReceived(null, eventArgs); - } - } -} + + User user = User.WithKey("user key"); + EventSourceMock mockEventSource; + TestEventSourceFactory eventSourceFactory; + IFlagCacheManager mockFlagCacheMgr; + + private IMobileUpdateProcessor MobileStreamingProcessorStarted() + { + mockEventSource = new EventSourceMock(); + eventSourceFactory = new TestEventSourceFactory(mockEventSource); + // stub with an empty InMemoryCache, so Stream updates can be tested + mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); + var config = Configuration.Default("someKey") + .WithConnectionManager(new MockConnectionManager(true)) + .WithIsStreamingEnabled(true) + .WithFlagCacheManager(mockFlagCacheMgr); + + var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); + processor.Start(); + return processor; + } + + [Fact] + public void CanCreateMobileStreamingProcFromFactory() + { + var streamingProcessor = MobileStreamingProcessorStarted(); + Assert.IsType(streamingProcessor); + } + + [Fact] + public void PUTstoresFeatureFlags() + { + var streamingProcessor = MobileStreamingProcessorStarted(); + // should be empty before PUT message arrives + var flagsInCache = mockFlagCacheMgr.FlagsForUser(user); + Assert.Empty(flagsInCache); + + PUTMessageSentToProcessor(); + flagsInCache = mockFlagCacheMgr.FlagsForUser(user); + Assert.NotEmpty(flagsInCache); + int intFlagValue = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagValue); + } + + [Fact] + public void PATCHupdatesFeatureFlag() + { + // before PATCH, fill in flags + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + //PATCH to update 1 flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlag(), null), "patch"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + //verify flag has changed + int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(99, flagFromPatch); + } + + [Fact] + public void PATCHdoesnotUpdateFlagIfVersionIsLower() + { + // before PATCH, fill in flags + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + //PATCH to update 1 flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlagWithLowerVersion(), null), "patch"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + //verify flag has not changed + int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, flagFromPatch); + } + + [Fact] + public void DELETEremovesFeatureFlag() + { + // before DELETE, fill in flags, test it's there + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + // DELETE int-flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlag(), null), "delete"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + // verify flag was deleted + Assert.Null(mockFlagCacheMgr.FlagForUser("int-flag", user)); + } + + [Fact] + public void DELTEdoesnotRemoveFeatureFlagIfVersionIsLower() + { + // before DELETE, fill in flags, test it's there + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + // DELETE int-flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlagWithLowerVersion(), null), "delete"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + // verify flag was not deleted + Assert.NotNull(mockFlagCacheMgr.FlagForUser("int-flag", user)); + } + + string UpdatedFlag() + { + var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":999,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; + return updatedFlagAsJson; + } + + string DeleteFlag() + { + var flagToDelete = "{\"key\":\"int-flag\",\"version\":1214}"; + return flagToDelete; + } + + string UpdatedFlagWithLowerVersion() + { + var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":1,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; + return updatedFlagAsJson; + } + + string DeleteFlagWithLowerVersion() + { + var flagToDelete = "{\"key\":\"int-flag\",\"version\":1}"; + return flagToDelete; + } + + void PUTMessageSentToProcessor() + { + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); + mockEventSource.RaiseMessageRcvd(eventArgs); + } + } + + class TestEventSourceFactory + { + public StreamProperties ReceivedProperties { get; private set; } + public IDictionary ReceivedHeaders { get; private set; } + IEventSource _eventSource; + + public TestEventSourceFactory(IEventSource eventSource) + { + _eventSource = eventSource; + } + + public StreamManager.EventSourceCreator Create() + { + return (StreamProperties sp, IDictionary headers) => + { + ReceivedProperties = sp; + ReceivedHeaders = headers; + return _eventSource; + }; + } + } + + class EventSourceMock : IEventSource + { + public ReadyState ReadyState => throw new NotImplementedException(); + +#pragma warning disable 0067 // unused properties + public event EventHandler Opened; + public event EventHandler Closed; + public event EventHandler MessageReceived; + public event EventHandler CommentReceived; + public event EventHandler Error; +#pragma warning restore 0067 + + public void Close() + { + + } + + public Task StartAsync() + { + return Task.CompletedTask; + } + + public void RaiseMessageRcvd(MessageReceivedEventArgs eventArgs) + { + MessageReceived(null, eventArgs); + } + } +} From a25b8cc2bfd6d17470a7b29fa1d7ef3188e43da1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 26 Jul 2018 19:42:01 -0700 Subject: [PATCH 032/254] beta12 release (fixes System.Runtime reference problem in LD.Common) --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 4 ++-- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2d7142ff..1d45d321 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta11 + 1.0.0-beta12 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin @@ -18,7 +18,7 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index aa036d1b..0b13954b 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 17b758586549c062cf85999cbbd1651cdac59cb8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:36:18 -0700 Subject: [PATCH 033/254] provide callback mechanism for backgrounding --- src/LaunchDarkly.Xamarin/Configuration.cs | 13 +++++ src/LaunchDarkly.Xamarin/Factory.cs | 52 ++++++------------- .../IBackgroundingState.cs | 33 ++++++++++++ src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 37 +++++++++++++ src/LaunchDarkly.Xamarin/LdClient.cs | 36 ++++++++++++- 5 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/IBackgroundingState.cs create mode 100644 src/LaunchDarkly.Xamarin/IPlatformAdapter.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 4910fe1f..a6bdda2f 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -125,6 +125,7 @@ public class Configuration : IMobileConfiguration internal ISimplePersistance Persister { get; set; } internal IDeviceInfo DeviceInfo { get; set; } internal IFeatureFlagListenerManager FeatureFlagListenerManager { get; set; } + internal IPlatformAdapter PlatformAdapter { get; set; } /// /// Default value for . @@ -651,5 +652,17 @@ public static Configuration WithBackgroundPollingInterval(this Configuration con configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } + + /// + /// Specifies a component that provides special functionality for the current mobile platform. + /// + /// Configuration. + /// An implementation of . + /// the same Configuration instance + public static Configuration WithPlatformAdapter(this Configuration configuration, IPlatformAdapter adapter) + { + configuration.PlatformAdapter = adapter; + return configuration; + } } } diff --git a/src/LaunchDarkly.Xamarin/Factory.cs b/src/LaunchDarkly.Xamarin/Factory.cs index 5fb139c0..fa082f67 100644 --- a/src/LaunchDarkly.Xamarin/Factory.cs +++ b/src/LaunchDarkly.Xamarin/Factory.cs @@ -14,27 +14,21 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura IFlagListenerUpdater updater, User user) { - IFlagCacheManager flagCacheManager; - if (configuration.FlagCacheManager != null) { - flagCacheManager = configuration.FlagCacheManager; + return configuration.FlagCacheManager; } else { var inMemoryCache = new UserFlagInMemoryCache(); var deviceCache = new UserFlagDeviceCache(persister); - flagCacheManager = new FlagCacheManager(inMemoryCache, deviceCache, updater, user); + return new FlagCacheManager(inMemoryCache, deviceCache, updater, user); } - - return flagCacheManager; } internal static IConnectionManager CreateConnectionManager(Configuration configuration) { - IConnectionManager connectionManager; - connectionManager = configuration.ConnectionManager ?? new MobileConnectionManager(); - return connectionManager; + return configuration.ConnectionManager ?? new MobileConnectionManager(); } internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, @@ -47,29 +41,26 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return configuration.MobileUpdateProcessor; } - IMobileUpdateProcessor updateProcessor = null; if (configuration.Offline) { - Log.InfoFormat("Was configured to be offline, starting service with NullUpdateProcessor"); + Log.InfoFormat("Starting LaunchDarkly client in offline mode"); return new NullUpdateProcessor(); } if (configuration.IsStreamingEnabled) { - updateProcessor = new MobileStreamingProcessor(configuration, + return new MobileStreamingProcessor(configuration, flagCacheManager, user, source); } else { var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); - updateProcessor = new MobilePollingProcessor(featureFlagRequestor, - flagCacheManager, - user, - configuration.PollingInterval); + return new MobilePollingProcessor(featureFlagRequestor, + flagCacheManager, + user, + configuration.PollingInterval); } - - return updateProcessor; } internal static IEventProcessor CreateEventProcessor(Configuration configuration) @@ -80,7 +71,6 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration } if (configuration.Offline) { - Log.InfoFormat("Was configured to be offline, starting service with NullEventProcessor"); return new NullEventProcessor(); } @@ -90,32 +80,22 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration internal static ISimplePersistance CreatePersister(Configuration configuration) { - if (configuration.Persister != null) - { - return configuration.Persister; - } - - return new SimpleMobileDevicePersistance(); + return configuration.Persister ?? new SimpleMobileDevicePersistance(); } internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) { - if (configuration.DeviceInfo != null) - { - return configuration.DeviceInfo; - } - - return new DeviceInfo(); + return configuration.DeviceInfo ?? new DeviceInfo(); } internal static IFeatureFlagListenerManager CreateFeatureFlagListenerManager(Configuration configuration) { - if (configuration.FeatureFlagListenerManager != null) - { - return configuration.FeatureFlagListenerManager; - } + return configuration.FeatureFlagListenerManager ?? new FeatureFlagListenerManager(); + } - return new FeatureFlagListenerManager(); + internal static IPlatformAdapter CreatePlatformAdapter(Configuration configuration) + { + return configuration.PlatformAdapter ?? new NullPlatformAdapter(); } } } diff --git a/src/LaunchDarkly.Xamarin/IBackgroundingState.cs b/src/LaunchDarkly.Xamarin/IBackgroundingState.cs new file mode 100644 index 00000000..019c2fbd --- /dev/null +++ b/src/LaunchDarkly.Xamarin/IBackgroundingState.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin +{ + /// + /// An interface that is used internally by implementations of + /// to update the state of the LaunchDarkly client in background mode. Application code does not need + /// to interact with this interface. + /// + public interface IBackgroundingState + { + /// + /// Tells the LaunchDarkly client that the application is entering background mode. The client will + /// suspend the regular streaming or polling process, except when + /// is called. + /// + Task EnterBackgroundAsync(); + + /// + /// Tells the LaunchDarkly client that the application is exiting background mode. The client will + /// resume the regular streaming or polling process. + /// + Task ExitBackgroundAsync(); + + /// + /// Tells the LaunchDarkly client to initiate a request for feature flag updates while in background mode. + /// + Task BackgroundUpdateAsync(); + } +} diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs new file mode 100644 index 00000000..ad49c2c1 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LaunchDarkly.Xamarin +{ + /// + /// Interface for a component that helps LdClient interact with a specific mobile platform. + /// Currently this is necessary in order to handle features that are not part of the portable + /// Xamarin.Essentials API; in the future it may be handled automatically when you + /// create an LdClient. + /// + /// To obtain an instance of this interface, use the implementation of `PlatformComponents.CreatePlatformAdapter` + /// that is provided by the add-on library for your specific platform (e.g. LaunchDarkly.Xamarin.Android). + /// Then pass the object to + /// when you are building your client configuration. + /// + /// Application code should not call any methods of this interface directly; they are used internally + /// by LdClient. + /// + public interface IPlatformAdapter : IDisposable + { + /// + /// Tells the IPlatformAdapter to start monitoring the foreground/background state of + /// the application, and provides a callback object for it to use when the state changes. + /// + /// An implementation of IBackgroundingState provided by the client + void EnableBackgrounding(IBackgroundingState backgroundingState); + } + + internal class NullPlatformAdapter : IPlatformAdapter + { + public void EnableBackgrounding(IBackgroundingState backgroundingState) { } + + public void Dispose() { } + } +} diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 9c74e5f1..e7636eef 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -50,6 +50,7 @@ public sealed class LdClient : ILdMobileClient IDeviceInfo deviceInfo; EventFactory eventFactory = EventFactory.Default; IFeatureFlagListenerManager flagListenerManager; + IPlatformAdapter platformAdapter; SemaphoreSlim connectionLock; @@ -75,6 +76,7 @@ public sealed class LdClient : ILdMobileClient persister = Factory.CreatePersister(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); + platformAdapter = Factory.CreatePlatformAdapter(configuration); // If you pass in a user with a null or blank key, one will be assigned to them. if (String.IsNullOrEmpty(user.Key)) @@ -222,6 +224,11 @@ static void CreateInstance(Configuration configuration, User user) Instance = new LdClient(configuration, user); Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); + + if (configuration.EnableBackgroundUpdating) + { + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); + } } bool StartUpdateProcessor(TimeSpan maxWaitTime) @@ -520,7 +527,8 @@ void Dispose(bool disposing) { if (disposing) { - Log.InfoFormat("The mobile client is being disposed"); + Log.InfoFormat("Shutting down the LaunchDarkly client"); + platformAdapter.Dispose(); updateProcessor.Dispose(); eventProcessor.Dispose(); } @@ -618,4 +626,30 @@ private Exception UnwrapAggregateException(AggregateException e) return e; } } + + // Implementation of IBackgroundingState - this allows us to keep these methods out of the public LdClient API + internal class LdClientBackgroundingState : IBackgroundingState + { + private readonly LdClient _client; + + internal LdClientBackgroundingState(LdClient client) + { + _client = client; + } + + public async Task EnterBackgroundAsync() + { + await _client.EnterBackgroundAsync(); + } + + public async Task ExitBackgroundAsync() + { + await _client.EnterForegroundAsync(); + } + + public async Task BackgroundUpdateAsync() + { + await _client.BackgroundTickAsync(); + } + } } \ No newline at end of file From d7bc235f023dd0c8f97540075ddb71bda1c560c9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:48:20 -0700 Subject: [PATCH 034/254] don't try to restart the update processor in background unless backgrounding is enabled --- src/LaunchDarkly.Xamarin/LdClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index e7636eef..6213b2c0 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -562,12 +562,18 @@ internal async Task EnterBackgroundAsync() { ClearUpdateProcessor(); Config.IsStreamingEnabled = false; - await RestartUpdateProcessorAsync(); + if (Config.EnableBackgroundUpdating) + { + await RestartUpdateProcessorAsync(); + } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } else { - await PingPollingProcessorAsync(); + if (Config.EnableBackgroundUpdating) + { + await PingPollingProcessorAsync(); + } } } From 7c023e24ea5028de522dbfeec3b0efb769a20442 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:50:05 -0700 Subject: [PATCH 035/254] always initialize platform adapter --- src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 5 +++-- src/LaunchDarkly.Xamarin/LdClient.cs | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs index ad49c2c1..278d3491 100644 --- a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -25,12 +25,13 @@ public interface IPlatformAdapter : IDisposable /// the application, and provides a callback object for it to use when the state changes. /// /// An implementation of IBackgroundingState provided by the client - void EnableBackgrounding(IBackgroundingState backgroundingState); + /// True if the client should poll while in the background + void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground); } internal class NullPlatformAdapter : IPlatformAdapter { - public void EnableBackgrounding(IBackgroundingState backgroundingState) { } + public void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground) { } public void Dispose() { } } diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6213b2c0..b42ef6eb 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -225,10 +225,8 @@ static void CreateInstance(Configuration configuration, User user) Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); - if (configuration.EnableBackgroundUpdating) - { - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); - } + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), + configuration.EnableBackgroundUpdating); } bool StartUpdateProcessor(TimeSpan maxWaitTime) From a7376c05c78e3867463d5a089b9a698cd1f46b25 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 17:25:46 -0700 Subject: [PATCH 036/254] doc comment typo --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 9c74e5f1..8f02f24b 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -129,7 +129,7 @@ public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) /// from the LaunchDarkly service. /// /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient + /// to instantiate the single instance of LdClient /// for the lifetime of your application. /// /// The singleton LdClient instance. From 0de1c94dd6519e4d9fdc3c8e1b73324b5f537331 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 17:26:09 -0700 Subject: [PATCH 037/254] doc comment typo --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 8f02f24b..cccc9157 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -152,7 +152,7 @@ public static async Task InitAsync(string mobileKey, User user) /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient + /// to instantiate the single instance of LdClient /// for the lifetime of your application. /// /// The singleton LdClient instance. From a97dd0d3763a714b2968a74ab981e2c922c14862 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 10:15:58 -0700 Subject: [PATCH 038/254] fix hang in synchronous Identify --- src/LaunchDarkly.Xamarin/LdClient.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index cccc9157..5996f6b0 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -443,7 +443,10 @@ public void Identify(User user) { try { - IdentifyAsync(user).Wait(); + // Note that we must use Task.Run here, rather than just doing IdentifyAsync(user).Wait(), + // to avoid a deadlock if we are on the main thread. See: + // https://olitee.com/2015/01/c-async-await-common-deadlock-scenario/ + Task.Run(() => IdentifyAsync(user)).Wait(); } catch (AggregateException e) { @@ -454,6 +457,8 @@ public void Identify(User user) /// public async Task IdentifyAsync(User user) { + Log.Warn("IdentifyAsync"); + if (user == null) { throw new ArgumentNullException("user"); @@ -477,6 +482,8 @@ public async Task IdentifyAsync(User user) } eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); + + Log.Warn("IdentifyAsync ending"); } async Task RestartUpdateProcessorAsync() From d631156db35a6f15104fac318892db8c40fffd9b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 10:16:18 -0700 Subject: [PATCH 039/254] rm debugging --- src/LaunchDarkly.Xamarin/LdClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 5996f6b0..79bccf3f 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -457,8 +457,6 @@ public void Identify(User user) /// public async Task IdentifyAsync(User user) { - Log.Warn("IdentifyAsync"); - if (user == null) { throw new ArgumentNullException("user"); @@ -482,8 +480,6 @@ public async Task IdentifyAsync(User user) } eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); - - Log.Warn("IdentifyAsync ending"); } async Task RestartUpdateProcessorAsync() From b8da2e04ceb1ed7ee8574bcd67d88fc2555707f9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 11:20:01 -0700 Subject: [PATCH 040/254] need to pass polling interval to adapter --- src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs index 278d3491..a50c924e 100644 --- a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -25,13 +25,13 @@ public interface IPlatformAdapter : IDisposable /// the application, and provides a callback object for it to use when the state changes. /// /// An implementation of IBackgroundingState provided by the client - /// True if the client should poll while in the background - void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground); + /// if non-null, the interval at which polling should happen in the background + void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval); } internal class NullPlatformAdapter : IPlatformAdapter { - public void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground) { } + public void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval) { } public void Dispose() { } } From bb7638049f462a567dbb4664e2cc2ba5c7f1650e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 11:56:43 -0700 Subject: [PATCH 041/254] fix method parameter --- src/LaunchDarkly.Xamarin/LdClient.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index e943dc73..188f598a 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -225,8 +225,12 @@ static void CreateInstance(Configuration configuration, User user) Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), - configuration.EnableBackgroundUpdating); + TimeSpan? bgPollInterval = null; + if (configuration.EnableBackgroundUpdating) + { + bgPollInterval = configuration.BackgroundPollingInterval; + } + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), bgPollInterval); } bool StartUpdateProcessor(TimeSpan maxWaitTime) From 3342490965d6d4708d0d4ce5e54bb4567d733b6a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 13:43:49 -0700 Subject: [PATCH 042/254] version 1.0.0-beta13 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 1d45d321..51d35c2d 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta12 + 1.0.0-beta13 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin From 7a0340961d8a4008234b084a988fc05785437ead Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 31 Jul 2018 11:08:33 -0700 Subject: [PATCH 043/254] don't allow null/empty mobile key --- src/LaunchDarkly.Xamarin/Configuration.cs | 4 ++++ .../LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index a6bdda2f..dc6e578c 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -188,6 +188,10 @@ public class Configuration : IMobileConfiguration /// a Configuration instance public static Configuration Default(string mobileKey) { + if (String.IsNullOrEmpty(mobileKey)) + { + throw new ArgumentOutOfRangeException("mobileKey", "key is required"); + } var defaultConfiguration = new Configuration { BaseUri = DefaultUri, diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index 250deb68..af1c0fc7 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -36,6 +36,18 @@ public void CanOverrideStreamConfiguration() Assert.Equal(TimeSpan.FromDays(1), config.ReconnectTime); } + [Fact] + public void MobileKeyCannotBeNull() + { + Assert.Throws(() => Configuration.Default(null)); + } + + [Fact] + public void MobileKeyCannotBeEmpty() + { + Assert.Throws(() => Configuration.Default("")); + } + [Fact] public void CannotOverrideTooSmallPollingInterval() { From f73aaa65ef4fc5a29bf909425c17c84c4f7a1708 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 31 Jul 2018 12:48:39 -0700 Subject: [PATCH 044/254] make polling intervals consistent with other mobile SDKs --- src/LaunchDarkly.Xamarin/Configuration.cs | 23 +++++++++++++++---- .../ConfigurationTest.cs | 18 +++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index dc6e578c..0435e21e 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -130,7 +130,11 @@ public class Configuration : IMobileConfiguration /// /// Default value for . /// - public static TimeSpan DefaultPollingInterval = TimeSpan.FromSeconds(30); + public static TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + /// + /// Minimum value for . + /// + public static TimeSpan MinimumPollingInterval = TimeSpan.FromMinutes(5); /// /// Default value for . /// @@ -174,7 +178,11 @@ public class Configuration : IMobileConfiguration /// /// The default value for . /// - private static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(3600); + private static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(60); + /// + /// The minimum value for . + /// + public static readonly TimeSpan MinimumBackgroundPollingInterval = TimeSpan.FromMinutes(15); /// /// The default value for . /// @@ -367,10 +375,10 @@ public static Configuration WithEventSamplingInterval(this Configuration configu /// the same Configuration instance public static Configuration WithPollingInterval(this Configuration configuration, TimeSpan pollingInterval) { - if (pollingInterval.CompareTo(Configuration.DefaultPollingInterval) < 0) + if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) { - Log.Warn("PollingInterval cannot be less than the default of 30 seconds."); - pollingInterval = Configuration.DefaultPollingInterval; + Log.WarnFormat("PollingInterval cannot be less than the default of {0}."); + pollingInterval = Configuration.MinimumPollingInterval; } configuration.PollingInterval = pollingInterval; return configuration; @@ -653,6 +661,11 @@ public static Configuration WithEnableBackgroundUpdating(this Configuration conf /// the same Configuration instance public static Configuration WithBackgroundPollingInterval(this Configuration configuration, TimeSpan backgroundPollingInternal) { + if (backgroundPollingInternal.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) + { + Log.WarnFormat("BackgroundPollingInterval cannot be less than the default of {0}.", Configuration.MinimumBackgroundPollingInterval); + backgroundPollingInternal = Configuration.MinimumBackgroundPollingInterval; + } configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index af1c0fc7..8f5b4f8d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -13,12 +13,12 @@ public void CanOverrideConfiguration() var config = Configuration.Default("AnyOtherSdkKey") .WithBaseUri("https://app.AnyOtherEndpoint.com") .WithEventQueueCapacity(99) - .WithPollingInterval(TimeSpan.FromMinutes(1)); + .WithPollingInterval(TimeSpan.FromMinutes(45)); Assert.Equal(new Uri("https://app.AnyOtherEndpoint.com"), config.BaseUri); Assert.Equal("AnyOtherSdkKey", config.MobileKey); Assert.Equal(99, config.EventQueueCapacity); - Assert.Equal(TimeSpan.FromMinutes(1), config.PollingInterval); + Assert.Equal(TimeSpan.FromMinutes(45), config.PollingInterval); } [Fact] @@ -49,11 +49,19 @@ public void MobileKeyCannotBeEmpty() } [Fact] - public void CannotOverrideTooSmallPollingInterval() + public void CannotSetTooSmallPollingInterval() { - var config = Configuration.Default("AnyOtherSdkKey").WithPollingInterval(TimeSpan.FromSeconds(29)); + var config = Configuration.Default("AnyOtherSdkKey").WithPollingInterval(TimeSpan.FromSeconds(299)); - Assert.Equal(TimeSpan.FromSeconds(30), config.PollingInterval); + Assert.Equal(TimeSpan.FromSeconds(300), config.PollingInterval); + } + + [Fact] + public void CannotSetTooSmallBackgroundPollingInterval() + { + var config = Configuration.Default("SdkKey").WithBackgroundPollingInterval(TimeSpan.FromSeconds(899)); + + Assert.Equal(TimeSpan.FromSeconds(900), config.BackgroundPollingInterval); } [Fact] From 08e08f77009d5aef7e53904e8d2b5e7bcd83d4b2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 14 Aug 2018 11:40:16 -0700 Subject: [PATCH 045/254] bump LD.Common to 1.0.5 to get fix for reconnection delay --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 51d35c2d..68087523 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 0b13954b..400a99fc 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 0b66a68f02d42205366cd1193b18179211e439a2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 14 Aug 2018 11:50:08 -0700 Subject: [PATCH 046/254] 1.0.0-beta14 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 68087523..d4e9e124 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta13 + 1.0.0-beta14 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin From 63ed5ba2c09f0d0b6b7b35c1fc6a8bc08ac4f1b4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Sep 2018 11:59:30 -0700 Subject: [PATCH 047/254] fix sample code --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2065665..609f27aa 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,15 @@ Quick setup Install-Package LaunchDarkly.Xamarin -1. Import the LaunchDarkly package: +1. Import the LaunchDarkly packages: + using LaunchDarkly.Client; using LaunchDarkly.Xamarin; 2. Initialize the LDClient with your Mobile key and user: User user = User.WithKey(username); - LdClient ldClient = LdClient.Init("YOUR_MOBILE_KEY", username); + LdClient ldClient = LdClient.Init("YOUR_MOBILE_KEY", user); Your first feature flag ----------------------- From 3c9712d541cc33e8e09624d7f828ffab1e904aaa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Sep 2018 13:15:04 -0700 Subject: [PATCH 048/254] use LaunchDarkly.Common 1.1.1 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- src/LaunchDarkly.Xamarin/LdClient.cs | 9 +++++---- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index d4e9e124..3ce8565a 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 188f598a..6e6ffa82 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -372,13 +372,13 @@ JToken Variation(string featureKey, JToken defaultValue) if (value == null || value.Type == JTokenType.Null) { featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, User, - defaultValue); + defaultValue, + EvaluationErrorKind.FLAG_NOT_FOUND); value = defaultValue; } else { featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, User, - flag.variation, - flag.value, + new EvaluationDetail(flag.value, flag.variation, null), defaultValue); } eventProcessor.SendEvent(featureRequestEvent); @@ -389,7 +389,8 @@ JToken Variation(string featureKey, JToken defaultValue) featureKey); featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, - defaultValue); + defaultValue, + EvaluationErrorKind.FLAG_NOT_FOUND); eventProcessor.SendEvent(featureRequestEvent); return defaultValue; } diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 400a99fc..83335352 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From c2661058f20815e27521f9f516fbb7d5137882c0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Sep 2018 13:19:34 -0700 Subject: [PATCH 049/254] use Xamarin.Essentials 0.10.0-preview --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- src/LaunchDarkly.Xamarin/MobileConnectionManager.cs | 2 +- .../LaunchDarkly.Xamarin.Tests.csproj | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 3ce8565a..b6a4ac24 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs index 3d9cccfa..13daea8f 100644 --- a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs +++ b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs @@ -28,7 +28,7 @@ bool IConnectionManager.IsConnected } } - void Connectivity_ConnectivityChanged(ConnectivityChangedEventArgs e) + void Connectivity_ConnectivityChanged(object sender, ConnectivityChangedEventArgs e) { UpdateConnectedStatus(); ConnectionChanged?.Invoke(isConnected); diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 83335352..f93d0ab2 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -7,6 +7,7 @@ + From 65e2bdcee734e98d6d6a9a695a5326709462e52a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Sep 2018 13:36:49 -0700 Subject: [PATCH 050/254] version 1.0.0-beta15 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index b6a4ac24..89f657a8 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta14 + 1.0.0-beta15 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin From 7faafbe80f1cf65b06b65f665ccff8bd879b51ae Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 5 Mar 2019 11:13:51 -0800 Subject: [PATCH 051/254] feat(src/): Removed Xamarin.Essentials and added necessary classes to project with proper licensing --- .../Connectivity/Connectivity.android.cs | 252 ++++++++++++++++++ .../Connectivity/Connectivity.ios.cs | 87 ++++++ .../Connectivity.ios.reachability.cs | 187 +++++++++++++ .../Connectivity/Connectivity.netstandard.cs | 41 +++ .../Connectivity/Connectivity.shared.cs | 91 +++++++ .../Connectivity/Connectivity.shared.enums.cs | 42 +++ .../LaunchDarkly.Xamarin.csproj | 54 +++- .../MobileConnectionManager.cs | 1 - .../Preferences/Preferences.android.cs | 176 ++++++++++++ .../Preferences/Preferences.ios.cs | 162 +++++++++++ .../Preferences/Preferences.netstandard.cs | 42 +++ .../Preferences/Preferences.shared.cs | 140 ++++++++++ .../SimpleMobileDevicePersistance.cs | 5 +- .../UserFlagDeviceCache.cs | 1 - .../LaunchDarkly.Xamarin.Tests.csproj | 1 - 15 files changed, 1272 insertions(+), 10 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs create mode 100644 src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs create mode 100644 src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs create mode 100644 src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs create mode 100644 src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs new file mode 100644 index 00000000..29dd8742 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs @@ -0,0 +1,252 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Android.Content; +using Android.Net; +using Android.OS; +using Debug = System.Diagnostics.Debug; + +namespace LaunchDarkly.Xamarin.Connectivity +{ + public partial class Connectivity + { + static ConnectivityBroadcastReceiver conectivityReceiver; + + static void StartListeners() + { + Permissions.EnsureDeclared(PermissionType.NetworkState); + + conectivityReceiver = new ConnectivityBroadcastReceiver(OnConnectivityChanged); + + Platform.AppContext.RegisterReceiver(conectivityReceiver, new IntentFilter(ConnectivityManager.ConnectivityAction)); + } + + static void StopListeners() + { + if (conectivityReceiver == null) + return; + try + { + Platform.AppContext.UnregisterReceiver(conectivityReceiver); + } + catch (Java.Lang.IllegalArgumentException) + { + Debug.WriteLine("Connectivity receiver already unregistered. Disposing of it."); + } + conectivityReceiver.Dispose(); + conectivityReceiver = null; + } + + static NetworkAccess IsBetterAccess(NetworkAccess currentAccess, NetworkAccess newAccess) => + newAccess > currentAccess ? newAccess : currentAccess; + + static NetworkAccess PlatformNetworkAccess + { + get + { + Permissions.EnsureDeclared(PermissionType.NetworkState); + + try + { + var currentAccess = NetworkAccess.None; + var manager = Platform.ConnectivityManager; + + if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + { + foreach (var network in manager.GetAllNetworks()) + { + try + { + var capabilities = manager.GetNetworkCapabilities(network); + + if (capabilities == null) + continue; + + var info = manager.GetNetworkInfo(network); + + if (info == null || !info.IsAvailable) + continue; + + // Check to see if it has the internet capability + if (!capabilities.HasCapability(NetCapability.Internet)) + { + // Doesn't have internet, but local is possible + currentAccess = IsBetterAccess(currentAccess, NetworkAccess.Local); + continue; + } + + ProcessNetworkInfo(info); + } + catch + { + // there is a possibility, but don't worry + } + } + } + else + { +#pragma warning disable CS0618 // Type or member is obsolete + foreach (var info in manager.GetAllNetworkInfo()) +#pragma warning restore CS0618 // Type or member is obsolete + { + ProcessNetworkInfo(info); + } + } + + void ProcessNetworkInfo(NetworkInfo info) + { + if (info == null || !info.IsAvailable) + return; + + if (info.IsConnected) + currentAccess = IsBetterAccess(currentAccess, NetworkAccess.Internet); + else if (info.IsConnectedOrConnecting) + currentAccess = IsBetterAccess(currentAccess, NetworkAccess.ConstrainedInternet); + } + + return currentAccess; + } + catch (Exception e) + { + Debug.WriteLine("Unable to get connected state - do you have ACCESS_NETWORK_STATE permission? - error: {0}", e); + return NetworkAccess.Unknown; + } + } + } + + static IEnumerable PlatformConnectionProfiles + { + get + { + Permissions.EnsureDeclared(PermissionType.NetworkState); + + var manager = Platform.ConnectivityManager; + if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + { + foreach (var network in manager.GetAllNetworks()) + { + NetworkInfo info = null; + try + { + info = manager.GetNetworkInfo(network); + } + catch + { + // there is a possibility, but don't worry about it + } + + var p = ProcessNetworkInfo(info); + if (p.HasValue) + yield return p.Value; + } + } + else + { +#pragma warning disable CS0618 // Type or member is obsolete + foreach (var info in manager.GetAllNetworkInfo()) +#pragma warning restore CS0618 // Type or member is obsolete + { + var p = ProcessNetworkInfo(info); + if (p.HasValue) + yield return p.Value; + } + } + + ConnectionProfile? ProcessNetworkInfo(NetworkInfo info) + { + if (info == null || !info.IsAvailable || !info.IsConnectedOrConnecting) + return null; + + return GetConnectionType(info.Type, info.TypeName); + } + } + } + + internal static ConnectionProfile GetConnectionType(ConnectivityType connectivityType, string typeName) + { + switch (connectivityType) + { + case ConnectivityType.Ethernet: + return ConnectionProfile.Ethernet; + case ConnectivityType.Wifi: + return ConnectionProfile.WiFi; + case ConnectivityType.Bluetooth: + return ConnectionProfile.Bluetooth; + case ConnectivityType.Wimax: + case ConnectivityType.Mobile: + case ConnectivityType.MobileDun: + case ConnectivityType.MobileHipri: + case ConnectivityType.MobileMms: + return ConnectionProfile.Cellular; + case ConnectivityType.Dummy: + return ConnectionProfile.Unknown; + default: + if (string.IsNullOrWhiteSpace(typeName)) + return ConnectionProfile.Unknown; + + var typeNameLower = typeName.ToLowerInvariant(); + if (typeNameLower.Contains("mobile")) + return ConnectionProfile.Cellular; + + if (typeNameLower.Contains("wimax")) + return ConnectionProfile.Cellular; + + if (typeNameLower.Contains("wifi")) + return ConnectionProfile.WiFi; + + if (typeNameLower.Contains("ethernet")) + return ConnectionProfile.Ethernet; + + if (typeNameLower.Contains("bluetooth")) + return ConnectionProfile.Bluetooth; + + return ConnectionProfile.Unknown; + } + } + } + + [BroadcastReceiver(Enabled = true, Exported = false, Label = "Essentials Connectivity Broadcast Receiver")] + class ConnectivityBroadcastReceiver : BroadcastReceiver + { + Action onChanged; + + public ConnectivityBroadcastReceiver() + { + } + + public ConnectivityBroadcastReceiver(Action onChanged) => + this.onChanged = onChanged; + + public override async void OnReceive(Context context, Intent intent) + { + if (intent.Action != ConnectivityManager.ConnectivityAction) + return; + + // await 500ms to ensure that the the connection manager updates + await Task.Delay(500); + onChanged?.Invoke(); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs new file mode 100644 index 00000000..6a53fad9 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs @@ -0,0 +1,87 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; + +namespace LaunchDarkly.Xamarin.Connectivity +{ + public static partial class Connectivity + { + static ReachabilityListener listener; + + static void StartListeners() + { + listener = new ReachabilityListener(); + listener.ReachabilityChanged += OnConnectivityChanged; + } + + static void StopListeners() + { + if (listener == null) + return; + + listener.ReachabilityChanged -= OnConnectivityChanged; + listener.Dispose(); + listener = null; + } + + static NetworkAccess PlatformNetworkAccess + { + get + { + var internetStatus = Reachability.InternetConnectionStatus(); + if (internetStatus == NetworkStatus.ReachableViaCarrierDataNetwork || internetStatus == NetworkStatus.ReachableViaWiFiNetwork) + return NetworkAccess.Internet; + + var remoteHostStatus = Reachability.RemoteHostStatus(); + if (remoteHostStatus == NetworkStatus.ReachableViaCarrierDataNetwork || remoteHostStatus == NetworkStatus.ReachableViaWiFiNetwork) + return NetworkAccess.Internet; + + return NetworkAccess.None; + } + } + + static IEnumerable PlatformConnectionProfiles + { + get + { + var statuses = Reachability.GetActiveConnectionType(); + foreach (var status in statuses) + { + switch (status) + { + case NetworkStatus.ReachableViaCarrierDataNetwork: + yield return ConnectionProfile.Cellular; + break; + case NetworkStatus.ReachableViaWiFiNetwork: + yield return ConnectionProfile.WiFi; + break; + default: + yield return ConnectionProfile.Unknown; + break; + } + } + } + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs new file mode 100644 index 00000000..4963da02 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs @@ -0,0 +1,187 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using CoreFoundation; +using SystemConfiguration; + +namespace LaunchDarkly.Xamarin.Connectivity +{ + enum NetworkStatus + { + NotReachable, + ReachableViaCarrierDataNetwork, + ReachableViaWiFiNetwork + } + + static class Reachability + { + internal const string HostName = "www.microsoft.com"; + + internal static NetworkStatus RemoteHostStatus() + { + using (var remoteHostReachability = new NetworkReachability(HostName)) + { + var reachable = remoteHostReachability.TryGetFlags(out var flags); + + if (!reachable) + return NetworkStatus.NotReachable; + + if (!IsReachableWithoutRequiringConnection(flags)) + return NetworkStatus.NotReachable; + + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + return NetworkStatus.ReachableViaCarrierDataNetwork; + + return NetworkStatus.ReachableViaWiFiNetwork; + } + } + + internal static NetworkStatus InternetConnectionStatus() + { + var status = NetworkStatus.NotReachable; + + var defaultNetworkAvailable = IsNetworkAvailable(out var flags); + + // If it's a WWAN connection.. + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + status = NetworkStatus.ReachableViaCarrierDataNetwork; + + // If the connection is reachable and no connection is required, then assume it's WiFi + if (defaultNetworkAvailable) + { + status = NetworkStatus.ReachableViaWiFiNetwork; + } + + // If the connection is on-demand or on-traffic and no user intervention + // is required, then assume WiFi. + if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && + (flags & NetworkReachabilityFlags.InterventionRequired) == 0) + { + status = NetworkStatus.ReachableViaWiFiNetwork; + } + + return status; + } + + internal static IEnumerable GetActiveConnectionType() + { + var status = new List(); + + var defaultNetworkAvailable = IsNetworkAvailable(out var flags); + + // If it's a WWAN connection.. + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + { + status.Add(NetworkStatus.ReachableViaCarrierDataNetwork); + } + else if (defaultNetworkAvailable) + { + status.Add(NetworkStatus.ReachableViaWiFiNetwork); + } + else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && + (flags & NetworkReachabilityFlags.InterventionRequired) == 0) + { + // If the connection is on-demand or on-traffic and no user intervention + // is required, then assume WiFi. + status.Add(NetworkStatus.ReachableViaWiFiNetwork); + } + + return status; + } + + internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags) + { + var ip = new IPAddress(0); + using (var defaultRouteReachability = new NetworkReachability(ip)) + { + if (!defaultRouteReachability.TryGetFlags(out flags)) + return false; + + return IsReachableWithoutRequiringConnection(flags); + } + } + + internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags) + { + // Is it reachable with the current network configuration? + var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0; + + // Do we need a connection to reach it? + var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0; + + // Since the network stack will automatically try to get the WAN up, + // probe that + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + noConnectionRequired = true; + + return isReachable && noConnectionRequired; + } + } + + class ReachabilityListener : IDisposable + { + NetworkReachability defaultRouteReachability; + NetworkReachability remoteHostReachability; + + internal ReachabilityListener() + { + var ip = new IPAddress(0); + defaultRouteReachability = new NetworkReachability(ip); + defaultRouteReachability.SetNotification(OnChange); + defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); + + remoteHostReachability = new NetworkReachability(Reachability.HostName); + + // Need to probe before we queue, or we wont get any meaningful values + // this only happens when you create NetworkReachability from a hostname + remoteHostReachability.TryGetFlags(out var flags); + + remoteHostReachability.SetNotification(OnChange); + remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); + } + + internal event Action ReachabilityChanged; + + void IDisposable.Dispose() => Dispose(); + + internal void Dispose() + { + defaultRouteReachability?.Dispose(); + defaultRouteReachability = null; + remoteHostReachability?.Dispose(); + remoteHostReachability = null; + } + + async void OnChange(NetworkReachabilityFlags flags) + { + // Add in artifical delay so the connection status has time to change + // else it will return true no matter what. + await Task.Delay(100); + + ReachabilityChanged?.Invoke(); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs new file mode 100644 index 00000000..944ac838 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs @@ -0,0 +1,41 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System.Collections.Generic; + +namespace LaunchDarkly.Xamarin.Connectivity +{ + public static partial class Connectivity + { + static NetworkAccess PlatformNetworkAccess => + throw new System.NullReferenceException(); + + static IEnumerable PlatformConnectionProfiles => + throw new System.NullReferenceException(); + + static void StartListeners() => + throw new System.NullReferenceException(); + + static void StopListeners() => + throw new System.NullReferenceException(); + } +} diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs new file mode 100644 index 00000000..e8a23616 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs @@ -0,0 +1,91 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LaunchDarkly.Xamarin.Connectivity +{ + public static partial class Connectivity + { + static event EventHandler ConnectivityChangedInternal; + + // a cache so that events aren't fired unnecessarily + // this is mainly an issue on Android, but we can stiil do this everywhere + static NetworkAccess currentAccess; + static List currentProfiles; + + public static NetworkAccess NetworkAccess => PlatformNetworkAccess; + + public static IEnumerable ConnectionProfiles => PlatformConnectionProfiles.Distinct(); + + public static event EventHandler ConnectivityChanged + { + add + { + var wasRunning = ConnectivityChangedInternal != null; + + ConnectivityChangedInternal += value; + + if (!wasRunning && ConnectivityChangedInternal != null) + { + SetCurrent(); + StartListeners(); + } + } + + remove + { + var wasRunning = ConnectivityChangedInternal != null; + + ConnectivityChangedInternal -= value; + + if (wasRunning && ConnectivityChangedInternal == null) + StopListeners(); + } + } + + static void SetCurrent() + { + currentAccess = NetworkAccess; + currentProfiles = new List(ConnectionProfiles); + } + } + + public class ConnectivityChangedEventArgs : EventArgs + { + public ConnectivityChangedEventArgs(NetworkAccess access, IEnumerable connectionProfiles) + { + NetworkAccess = access; + ConnectionProfiles = connectionProfiles; + } + + public NetworkAccess NetworkAccess { get; } + + public IEnumerable ConnectionProfiles { get; } + + public override string ToString() => + $"{nameof(NetworkAccess)}: {NetworkAccess}, " + + $"{nameof(ConnectionProfiles)}: [{string.Join(", ", ConnectionProfiles)}]"; + } +} diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs new file mode 100644 index 00000000..e6a7c423 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs @@ -0,0 +1,42 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +namespace LaunchDarkly.Xamarin.Connectivity +{ + public enum ConnectionProfile + { + Unknown = 0, + Bluetooth = 1, + Cellular = 2, + Ethernet = 3, + WiFi = 4 + } + + public enum NetworkAccess + { + Unknown = 0, + None = 1, + Local = 2, + ConstrainedInternet = 3, + Internet = 4 + } +} diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 89f657a8..98055e71 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -5,18 +5,18 @@ Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin + false - netstandard1.6;netstandard2.0;net45 + netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81 - netstandard1.6;netstandard2.0 + netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81 - @@ -29,4 +29,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs index 13daea8f..f9453560 100644 --- a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs +++ b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs @@ -1,5 +1,4 @@ using System; -using Xamarin.Essentials; namespace LaunchDarkly.Xamarin { diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs new file mode 100644 index 00000000..b3616733 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs @@ -0,0 +1,176 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Globalization; +using Android.App; +using Android.Content; +using Android.Preferences; + +namespace LaunchDarkly.Xamarin.Preferences +{ + public static partial class Preferences + { + static readonly object locker = new object(); + + static bool PlatformContainsKey(string key, string sharedName) + { + lock (locker) + { + using (var sharedPreferences = GetSharedPreferences(sharedName)) + { + return sharedPreferences.Contains(key); + } + } + } + + static void PlatformRemove(string key, string sharedName) + { + lock (locker) + { + using (var sharedPreferences = GetSharedPreferences(sharedName)) + using (var editor = sharedPreferences.Edit()) + { + editor.Remove(key).Commit(); + } + } + } + + static void PlatformClear(string sharedName) + { + lock (locker) + { + using (var sharedPreferences = GetSharedPreferences(sharedName)) + using (var editor = sharedPreferences.Edit()) + { + editor.Clear().Commit(); + } + } + } + + static void PlatformSet(string key, T value, string sharedName) + { + lock (locker) + { + using (var sharedPreferences = GetSharedPreferences(sharedName)) + using (var editor = sharedPreferences.Edit()) + { + if (value == null) + { + editor.Remove(key); + } + else + { + switch (value) + { + case string s: + editor.PutString(key, s); + break; + case int i: + editor.PutInt(key, i); + break; + case bool b: + editor.PutBoolean(key, b); + break; + case long l: + editor.PutLong(key, l); + break; + case double d: + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + editor.PutString(key, valueString); + break; + case float f: + editor.PutFloat(key, f); + break; + } + } + editor.Apply(); + } + } + } + + static T PlatformGet(string key, T defaultValue, string sharedName) + { + lock (locker) + { + object value = null; + using (var sharedPreferences = GetSharedPreferences(sharedName)) + { + if (defaultValue == null) + { + value = sharedPreferences.GetString(key, null); + } + else + { + switch (defaultValue) + { + case int i: + value = sharedPreferences.GetInt(key, i); + break; + case bool b: + value = sharedPreferences.GetBoolean(key, b); + break; + case long l: + value = sharedPreferences.GetLong(key, l); + break; + case double d: + var savedDouble = sharedPreferences.GetString(key, null); + if (string.IsNullOrWhiteSpace(savedDouble)) + { + value = defaultValue; + } + else + { + if (!double.TryParse(savedDouble, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var outDouble)) + { + var maxString = Convert.ToString(double.MaxValue, CultureInfo.InvariantCulture); + outDouble = savedDouble.Equals(maxString) ? double.MaxValue : double.MinValue; + } + + value = outDouble; + } + break; + case float f: + value = sharedPreferences.GetFloat(key, f); + break; + case string s: + // the case when the string is not null + value = sharedPreferences.GetString(key, s); + break; + } + } + } + + return (T)value; + } + } + + static ISharedPreferences GetSharedPreferences(string sharedName) + { + var context = Application.Context; + + return string.IsNullOrWhiteSpace(sharedName) ? + PreferenceManager.GetDefaultSharedPreferences(context) : + context.GetSharedPreferences(sharedName, FileCreationMode.Private); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs new file mode 100644 index 00000000..ad508f29 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs @@ -0,0 +1,162 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Globalization; +using Foundation; + +namespace LaunchDarkly.Xamarin.Preferences +{ + public static partial class Preferences + { + static readonly object locker = new object(); + + static bool PlatformContainsKey(string key, string sharedName) + { + lock (locker) + { + return GetUserDefaults(sharedName)[key] != null; + } + } + + static void PlatformRemove(string key, string sharedName) + { + lock (locker) + { + using (var userDefaults = GetUserDefaults(sharedName)) + { + if (userDefaults[key] != null) + userDefaults.RemoveObject(key); + } + } + } + + static void PlatformClear(string sharedName) + { + lock (locker) + { + using (var userDefaults = GetUserDefaults(sharedName)) + { + var items = userDefaults.ToDictionary(); + + foreach (var item in items.Keys) + { + if (item is NSString nsString) + userDefaults.RemoveObject(nsString); + } + } + } + } + + static void PlatformSet(string key, T value, string sharedName) + { + lock (locker) + { + using (var userDefaults = GetUserDefaults(sharedName)) + { + if (value == null) + { + if (userDefaults[key] != null) + userDefaults.RemoveObject(key); + return; + } + + switch (value) + { + case string s: + userDefaults.SetString(s, key); + break; + case int i: + userDefaults.SetInt(i, key); + break; + case bool b: + userDefaults.SetBool(b, key); + break; + case long l: + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + userDefaults.SetString(valueString, key); + break; + case double d: + userDefaults.SetDouble(d, key); + break; + case float f: + userDefaults.SetFloat(f, key); + break; + } + } + } + } + + static T PlatformGet(string key, T defaultValue, string sharedName) + { + object value = null; + + lock (locker) + { + using (var userDefaults = GetUserDefaults(sharedName)) + { + if (userDefaults[key] == null) + return defaultValue; + + switch (defaultValue) + { + case int i: + value = (int)(nint)userDefaults.IntForKey(key); + break; + case bool b: + value = userDefaults.BoolForKey(key); + break; + case long l: + var savedLong = userDefaults.StringForKey(key); + value = Convert.ToInt64(savedLong, CultureInfo.InvariantCulture); + break; + case double d: + value = userDefaults.DoubleForKey(key); + break; + case float f: + value = userDefaults.FloatForKey(key); + break; + case string s: + // the case when the string is not null + value = userDefaults.StringForKey(key); + break; + default: + // the case when the string is null + if (typeof(T) == typeof(string)) + value = userDefaults.StringForKey(key); + break; + } + } + } + + return (T)value; + } + + static NSUserDefaults GetUserDefaults(string sharedName) + { + if (!string.IsNullOrWhiteSpace(sharedName)) + return new NSUserDefaults(sharedName, NSUserDefaultsType.SuiteName); + else + return NSUserDefaults.StandardUserDefaults; + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs new file mode 100644 index 00000000..b53f9bca --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs @@ -0,0 +1,42 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +namespace LaunchDarkly.Xamarin.Preferences +{ + public static partial class Preferences + { + static bool PlatformContainsKey(string key, string sharedName) => + throw new System.NullReferenceException(); + + static void PlatformRemove(string key, string sharedName) => + throw new System.NullReferenceException(); + + static void PlatformClear(string sharedName) => + throw new System.NullReferenceException(); + + static void PlatformSet(string key, T value, string sharedName) => + throw new System.NullReferenceException(); + + static T PlatformGet(string key, T defaultValue, string sharedName) => + throw new System.NullReferenceException(); + } +} diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs new file mode 100644 index 00000000..6786e940 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs @@ -0,0 +1,140 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; + +namespace LaunchDarkly.Xamarin.Preferences +{ + public static partial class Preferences + { + internal static string GetPrivatePreferencesSharedName(string feature) => + $"LaunchDarkly.Xamarin.{feature}"; + + // overloads + + public static bool ContainsKey(string key) => + ContainsKey(key, null); + + public static void Remove(string key) => + Remove(key, null); + + public static void Clear() => + Clear(null); + + public static string Get(string key, string defaultValue) => + Get(key, defaultValue, null); + + public static bool Get(string key, bool defaultValue) => + Get(key, defaultValue, null); + + public static int Get(string key, int defaultValue) => + Get(key, defaultValue, null); + + public static double Get(string key, double defaultValue) => + Get(key, defaultValue, null); + + public static float Get(string key, float defaultValue) => + Get(key, defaultValue, null); + + public static long Get(string key, long defaultValue) => + Get(key, defaultValue, null); + + public static void Set(string key, string value) => + Set(key, value, null); + + public static void Set(string key, bool value) => + Set(key, value, null); + + public static void Set(string key, int value) => + Set(key, value, null); + + public static void Set(string key, double value) => + Set(key, value, null); + + public static void Set(string key, float value) => + Set(key, value, null); + + public static void Set(string key, long value) => + Set(key, value, null); + + // shared -> platform + + public static bool ContainsKey(string key, string sharedName) => + PlatformContainsKey(key, sharedName); + + public static void Remove(string key, string sharedName) => + PlatformRemove(key, sharedName); + + public static void Clear(string sharedName) => + PlatformClear(sharedName); + + public static string Get(string key, string defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static bool Get(string key, bool defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static int Get(string key, int defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static double Get(string key, double defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static float Get(string key, float defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static long Get(string key, long defaultValue, string sharedName) => + PlatformGet(key, defaultValue, sharedName); + + public static void Set(string key, string value, string sharedName) => + PlatformSet(key, value, sharedName); + + public static void Set(string key, bool value, string sharedName) => + PlatformSet(key, value, sharedName); + + public static void Set(string key, int value, string sharedName) => + PlatformSet(key, value, sharedName); + + public static void Set(string key, double value, string sharedName) => + PlatformSet(key, value, sharedName); + + public static void Set(string key, float value, string sharedName) => + PlatformSet(key, value, sharedName); + + public static void Set(string key, long value, string sharedName) => + PlatformSet(key, value, sharedName); + + // DateTime + + public static DateTime Get(string key, DateTime defaultValue) => + Get(key, defaultValue, null); + + public static void Set(string key, DateTime value) => + Set(key, value, null); + + public static DateTime Get(string key, DateTime defaultValue, string sharedName) => + DateTime.FromBinary(PlatformGet(key, defaultValue.ToBinary(), sharedName)); + + public static void Set(string key, DateTime value, string sharedName) => + PlatformSet(key, value.ToBinary(), sharedName); + } +} diff --git a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs index 5a0f86e1..821d7ce5 100644 --- a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs +++ b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs @@ -1,7 +1,4 @@ -using System; -using Xamarin.Essentials; - -namespace LaunchDarkly.Xamarin +namespace LaunchDarkly.Xamarin { internal class SimpleMobileDevicePersistance : ISimplePersistance { diff --git a/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs b/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs index aa20773c..b3fc8ff3 100644 --- a/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs +++ b/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs @@ -4,7 +4,6 @@ using LaunchDarkly.Client; using LaunchDarkly.Common; using Newtonsoft.Json; -using Xamarin.Essentials; namespace LaunchDarkly.Xamarin { diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index f93d0ab2..83335352 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -7,7 +7,6 @@ - From 3d015eb1f0968a242e424592f2d4aaf8578fce4d Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 5 Mar 2019 11:24:07 -0800 Subject: [PATCH 052/254] feat(README, LdClient.cs): Implemented Initialized(), removed python twisted from README --- README.md | 1 - src/LaunchDarkly.Xamarin/LdClient.cs | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 609f27aa..23535490 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ About LaunchDarkly * [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK") * [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK") * [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK") - * [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK") * [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK") * [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK") * [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK") diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6e6ffa82..03bb1588 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -428,12 +428,7 @@ public void Track(string eventName) /// public bool Initialized() { - //bool isInited = Instance != null; - //return isInited && Online; - // TODO: This method needs to be fixed to actually check whether the update processor has initialized. - // The previous logic (above) was meaningless because this method is not static, so by definition you - // do have a client instance if we've gotten here. But that doesn't mean it is initialized. - return Online; + return Online && updateProcessor.Initialized(); } /// From 95d4e1567ef13564ba5f14f970ebf58067fbc8a7 Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 5 Mar 2019 14:30:06 -0800 Subject: [PATCH 053/254] feat(src/): Added BackgroundAdapter/, changed conditional compilation a bunch, fixed a lot of build errors that weren't appearing before --- .../BackgroundAdapter.android.cs | 89 +++++++++++++++++++ .../BackgroundAdapter.ios.cs | 51 +++++++++++ .../BackgroundAdapter.netstandard.cs | 18 ++++ ...ientRunMode.cs => ClientRunMode.shared.cs} | 0 ...nfiguration.cs => Configuration.shared.cs} | 0 .../Connectivity/Connectivity.netstandard.cs | 10 ++- .../{Constants.cs => Constants.shared.cs} | 0 .../{DeviceInfo.cs => DeviceInfo.shared.cs} | 0 .../{Extensions.cs => Extensions.shared.cs} | 0 .../{Factory.cs => Factory.shared.cs} | 0 .../{FeatureFlag.cs => FeatureFlag.shared.cs} | 0 ...s => FeatureFlagListenerManager.shared.cs} | 0 ...stor.cs => FeatureFlagRequestor.shared.cs} | 0 ...eManager.cs => FlagCacheManager.shared.cs} | 0 ...State.cs => IBackgroundingState.shared.cs} | 0 ...anager.cs => IConnectionManager.shared.cs} | 0 .../{IDeviceInfo.cs => IDeviceInfo.shared.cs} | 0 ...ener.cs => IFeatureFlagListener.shared.cs} | 0 ... => IFeatureFlagListenerManager.shared.cs} | 0 ...Manager.cs => IFlagCacheManager.shared.cs} | 0 ...ileClient.cs => ILdMobileClient.shared.cs} | 0 ...tion.cs => IMobileConfiguration.shared.cs} | 0 ...or.cs => IMobileUpdateProcessor.shared.cs} | 0 ...mAdapter.cs => IPlatformAdapter.shared.cs} | 0 ...stance.cs => ISimplePersistance.shared.cs} | 0 ...rFlagCache.cs => IUserFlagCache.shared.cs} | 0 .../{LdClient.cs => LdClient.shared.cs} | 2 + ...r.cs => MobileConnectionManager.shared.cs} | 10 +-- ...or.cs => MobilePollingProcessor.shared.cs} | 0 ... => MobileSideClientEnvironment.shared.cs} | 0 ....cs => MobileStreamingProcessor.shared.cs} | 0 .../Preferences/Preferences.netstandard.cs | 12 +-- ...cs => SimpleInMemoryPersistance.shared.cs} | 0 ...> SimpleMobileDevicePersistance.shared.cs} | 12 +-- ...Cache.cs => UserFlagDeviceCache.shared.cs} | 0 ...che.cs => UserFlagInMemoryCache.shared.cs} | 0 36 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs create mode 100644 src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs rename src/LaunchDarkly.Xamarin/{ClientRunMode.cs => ClientRunMode.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{Configuration.cs => Configuration.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{Constants.cs => Constants.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{DeviceInfo.cs => DeviceInfo.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{Extensions.cs => Extensions.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{Factory.cs => Factory.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{FeatureFlag.cs => FeatureFlag.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{FeatureFlagListenerManager.cs => FeatureFlagListenerManager.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{FeatureFlagRequestor.cs => FeatureFlagRequestor.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{FlagCacheManager.cs => FlagCacheManager.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IBackgroundingState.cs => IBackgroundingState.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IConnectionManager.cs => IConnectionManager.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IDeviceInfo.cs => IDeviceInfo.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IFeatureFlagListener.cs => IFeatureFlagListener.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IFeatureFlagListenerManager.cs => IFeatureFlagListenerManager.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IFlagCacheManager.cs => IFlagCacheManager.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{ILdMobileClient.cs => ILdMobileClient.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IMobileConfiguration.cs => IMobileConfiguration.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IMobileUpdateProcessor.cs => IMobileUpdateProcessor.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IPlatformAdapter.cs => IPlatformAdapter.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{ISimplePersistance.cs => ISimplePersistance.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{IUserFlagCache.cs => IUserFlagCache.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{LdClient.cs => LdClient.shared.cs} (96%) rename src/LaunchDarkly.Xamarin/{MobileConnectionManager.cs => MobileConnectionManager.shared.cs} (70%) rename src/LaunchDarkly.Xamarin/{MobilePollingProcessor.cs => MobilePollingProcessor.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{MobileSideClientEnvironment.cs => MobileSideClientEnvironment.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{MobileStreamingProcessor.cs => MobileStreamingProcessor.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{SimpleInMemoryPersistance.cs => SimpleInMemoryPersistance.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{SimpleMobileDevicePersistance.cs => SimpleMobileDevicePersistance.shared.cs} (54%) rename src/LaunchDarkly.Xamarin/{UserFlagDeviceCache.cs => UserFlagDeviceCache.shared.cs} (100%) rename src/LaunchDarkly.Xamarin/{UserFlagInMemoryCache.cs => UserFlagInMemoryCache.shared.cs} (100%) diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs new file mode 100644 index 00000000..586168c0 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs @@ -0,0 +1,89 @@ +using System; +using LaunchDarkly.Xamarin; +using Android.App; +using Android.OS; + +namespace LaunchDarkly.Xamarin.BackgroundAdapter +{ + public class BackgroundAdapter : IPlatformAdapter + { + private static ActivityLifecycleCallbacks _callbacks; + private Application application; + + public void EnableBackgrounding(IBackgroundingState backgroundingState) + { + if (_callbacks == null) + { + _callbacks = new ActivityLifecycleCallbacks(backgroundingState); + application = (Application)Application.Context; + application.RegisterActivityLifecycleCallbacks(_callbacks); + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void _Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + application = null; + _callbacks = null; + + disposedValue = true; + } + } + + public void Dispose() + { + _Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + + private class ActivityLifecycleCallbacks : Java.Lang.Object, Application.IActivityLifecycleCallbacks + { + private IBackgroundingState _backgroundingState; + + public ActivityLifecycleCallbacks(IBackgroundingState backgroundingState) + { + _backgroundingState = backgroundingState; + } + + public void OnActivityCreated(Activity activity, Bundle savedInstanceState) + { + } + + public void OnActivityDestroyed(Activity activity) + { + } + + public void OnActivityPaused(Activity activity) + { + _backgroundingState.EnterBackgroundAsync(); + } + + public void OnActivityResumed(Activity activity) + { + _backgroundingState.ExitBackgroundAsync(); + } + + public void OnActivitySaveInstanceState(Activity activity, Bundle outState) + { + } + + public void OnActivityStarted(Activity activity) + { + } + + public void OnActivityStopped(Activity activity) + { + } + } + } +} diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs new file mode 100644 index 00000000..47190d6c --- /dev/null +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -0,0 +1,51 @@ +using System; +using LaunchDarkly.Xamarin; +using UIKit; + +namespace LaunchDarkly.Xamarin.BackgroundAdapter +{ + public class BackgroundAdapter : UIApplicationDelegate, IPlatformAdapter + { + private IBackgroundingState _backgroundingState; + + public void EnableBackgrounding(IBackgroundingState backgroundingState) + { + _backgroundingState = backgroundingState; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected void _Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + _backgroundingState = null; + + disposedValue = true; + } + } + + public new void Dispose() + { + _Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + + public override void WillEnterForeground(UIApplication application) + { + _backgroundingState.ExitBackgroundAsync(); + } + + public override void DidEnterBackground(UIApplication application) + { + _backgroundingState.EnterBackgroundAsync(); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs new file mode 100644 index 00000000..403d6eae --- /dev/null +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs @@ -0,0 +1,18 @@ +using System; +using LaunchDarkly.Xamarin; + +namespace LaunchDarkly.Xamarin.BackgroundAdapter +{ + public class BackgroundAdapter : IPlatformAdapter + { + public void Dispose() + { + throw new NotImplementedException(); + } + + public void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/ClientRunMode.cs b/src/LaunchDarkly.Xamarin/ClientRunMode.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ClientRunMode.cs rename to src/LaunchDarkly.Xamarin/ClientRunMode.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Configuration.cs rename to src/LaunchDarkly.Xamarin/Configuration.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs index 944ac838..38af6cec 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs @@ -20,6 +20,8 @@ portions of the Software. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +using System; + using System.Collections.Generic; namespace LaunchDarkly.Xamarin.Connectivity @@ -27,15 +29,15 @@ namespace LaunchDarkly.Xamarin.Connectivity public static partial class Connectivity { static NetworkAccess PlatformNetworkAccess => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static IEnumerable PlatformConnectionProfiles => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static void StartListeners() => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static void StopListeners() => - throw new System.NullReferenceException(); + throw new NotImplementedException(); } } diff --git a/src/LaunchDarkly.Xamarin/Constants.cs b/src/LaunchDarkly.Xamarin/Constants.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Constants.cs rename to src/LaunchDarkly.Xamarin/Constants.shared.cs diff --git a/src/LaunchDarkly.Xamarin/DeviceInfo.cs b/src/LaunchDarkly.Xamarin/DeviceInfo.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/DeviceInfo.cs rename to src/LaunchDarkly.Xamarin/DeviceInfo.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Extensions.cs b/src/LaunchDarkly.Xamarin/Extensions.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Extensions.cs rename to src/LaunchDarkly.Xamarin/Extensions.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Factory.cs b/src/LaunchDarkly.Xamarin/Factory.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Factory.cs rename to src/LaunchDarkly.Xamarin/Factory.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlag.cs b/src/LaunchDarkly.Xamarin/FeatureFlag.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlag.cs rename to src/LaunchDarkly.Xamarin/FeatureFlag.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.cs b/src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.cs rename to src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs b/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs rename to src/LaunchDarkly.Xamarin/FeatureFlagRequestor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FlagCacheManager.cs b/src/LaunchDarkly.Xamarin/FlagCacheManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FlagCacheManager.cs rename to src/LaunchDarkly.Xamarin/FlagCacheManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IBackgroundingState.cs b/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IBackgroundingState.cs rename to src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IConnectionManager.cs b/src/LaunchDarkly.Xamarin/IConnectionManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IConnectionManager.cs rename to src/LaunchDarkly.Xamarin/IConnectionManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IDeviceInfo.cs b/src/LaunchDarkly.Xamarin/IDeviceInfo.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IDeviceInfo.cs rename to src/LaunchDarkly.Xamarin/IDeviceInfo.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFeatureFlagListener.cs b/src/LaunchDarkly.Xamarin/IFeatureFlagListener.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFeatureFlagListener.cs rename to src/LaunchDarkly.Xamarin/IFeatureFlagListener.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.cs b/src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.cs rename to src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFlagCacheManager.cs b/src/LaunchDarkly.Xamarin/IFlagCacheManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFlagCacheManager.cs rename to src/LaunchDarkly.Xamarin/IFlagCacheManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs b/src/LaunchDarkly.Xamarin/ILdMobileClient.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ILdMobileClient.cs rename to src/LaunchDarkly.Xamarin/ILdMobileClient.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IMobileConfiguration.cs b/src/LaunchDarkly.Xamarin/IMobileConfiguration.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IMobileConfiguration.cs rename to src/LaunchDarkly.Xamarin/IMobileConfiguration.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.cs b/src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.cs rename to src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IPlatformAdapter.cs rename to src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs diff --git a/src/LaunchDarkly.Xamarin/ISimplePersistance.cs b/src/LaunchDarkly.Xamarin/ISimplePersistance.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ISimplePersistance.cs rename to src/LaunchDarkly.Xamarin/ISimplePersistance.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IUserFlagCache.cs b/src/LaunchDarkly.Xamarin/IUserFlagCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IUserFlagCache.cs rename to src/LaunchDarkly.Xamarin/IUserFlagCache.shared.cs diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs similarity index 96% rename from src/LaunchDarkly.Xamarin/LdClient.cs rename to src/LaunchDarkly.Xamarin/LdClient.shared.cs index 03bb1588..aba76010 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -69,6 +69,8 @@ public sealed class LdClient : ILdMobileClient throw new ArgumentNullException("user"); } + configuration.PlatformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); + Config = configuration; connectionLock = new SemaphoreSlim(1, 1); diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs b/src/LaunchDarkly.Xamarin/MobileConnectionManager.shared.cs similarity index 70% rename from src/LaunchDarkly.Xamarin/MobileConnectionManager.cs rename to src/LaunchDarkly.Xamarin/MobileConnectionManager.shared.cs index f9453560..c3c835c2 100644 --- a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs +++ b/src/LaunchDarkly.Xamarin/MobileConnectionManager.shared.cs @@ -11,9 +11,9 @@ internal MobileConnectionManager() UpdateConnectedStatus(); try { - Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + LaunchDarkly.Xamarin.Connectivity.Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; } - catch (NotImplementedInReferenceAssemblyException) + catch (NotImplementedException) { } } @@ -27,7 +27,7 @@ bool IConnectionManager.IsConnected } } - void Connectivity_ConnectivityChanged(object sender, ConnectivityChangedEventArgs e) + void Connectivity_ConnectivityChanged(object sender, Connectivity.ConnectivityChangedEventArgs e) { UpdateConnectedStatus(); ConnectionChanged?.Invoke(isConnected); @@ -37,9 +37,9 @@ private void UpdateConnectedStatus() { try { - isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; + isConnected = LaunchDarkly.Xamarin.Connectivity.Connectivity.NetworkAccess == LaunchDarkly.Xamarin.Connectivity.NetworkAccess.Internet; } - catch (NotImplementedInReferenceAssemblyException) + catch (NotImplementedException) { // .NET Standard has no way to detect network connectivity isConnected = true; diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs rename to src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.cs b/src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.cs rename to src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs rename to src/LaunchDarkly.Xamarin/MobileStreamingProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs index b53f9bca..02694894 100644 --- a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs @@ -20,23 +20,25 @@ portions of the Software. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +using System; + namespace LaunchDarkly.Xamarin.Preferences { public static partial class Preferences { static bool PlatformContainsKey(string key, string sharedName) => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static void PlatformRemove(string key, string sharedName) => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static void PlatformClear(string sharedName) => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static void PlatformSet(string key, T value, string sharedName) => - throw new System.NullReferenceException(); + throw new NotImplementedException(); static T PlatformGet(string key, T defaultValue, string sharedName) => - throw new System.NullReferenceException(); + throw new NotImplementedException(); } } diff --git a/src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.cs b/src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.cs rename to src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.shared.cs diff --git a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.shared.cs similarity index 54% rename from src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs rename to src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.shared.cs index 821d7ce5..5809d1b3 100644 --- a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs +++ b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.shared.cs @@ -1,4 +1,6 @@ -namespace LaunchDarkly.Xamarin +using System; + +namespace LaunchDarkly.Xamarin { internal class SimpleMobileDevicePersistance : ISimplePersistance { @@ -6,18 +8,18 @@ public void Save(string key, string value) { try { - Preferences.Set(key, value); + LaunchDarkly.Xamarin.Preferences.Preferences.Set(key, value); } - catch (NotImplementedInReferenceAssemblyException) { } + catch (NotImplementedException) { } } public string GetValue(string key) { try { - return Preferences.Get(key, null); + return LaunchDarkly.Xamarin.Preferences.Preferences.Get(key, null); } - catch (NotImplementedInReferenceAssemblyException) + catch (NotImplementedException) { return null; } diff --git a/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs b/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserFlagDeviceCache.cs rename to src/LaunchDarkly.Xamarin/UserFlagDeviceCache.shared.cs diff --git a/src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.cs b/src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.cs rename to src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.shared.cs From 791531cf267e6a8d90a6d34caa500e80867e0f77 Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 5 Mar 2019 16:52:44 -0800 Subject: [PATCH 054/254] fix(src/): AssemblyInfo wasn't compiling, removed unnecessary public key --- LaunchDarkly.Xamarin.pk | Bin 160 -> 0 bytes .../LaunchDarkly.Xamarin.csproj | 5 +---- .../{AssemblyInfo.cs => AssemblyInfo.shared.cs} | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 LaunchDarkly.Xamarin.pk rename src/LaunchDarkly.Xamarin/Properties/{AssemblyInfo.cs => AssemblyInfo.shared.cs} (99%) diff --git a/LaunchDarkly.Xamarin.pk b/LaunchDarkly.Xamarin.pk deleted file mode 100644 index d8290e41aed16ba3e3752701be6d9a3c063507f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa500966iXFVm!$NhP$%`JV zq(*!F>INn$@)FN{W}otFPA~Js2q`3oyfmANXnk`7Kk-e!h8`a3BQQE!T@$g}14o$? zy%Gzfa?h3>C)=N8CNWlDPKy6ZdEp%XaX2#-e#>3Uz%6$vXW1F}@Nd52C#GcFSs60b OuwPA~$U~O`?ESGf_CA>a diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 98055e71..b1463d06 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -25,11 +25,8 @@ - - - - + diff --git a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs similarity index 99% rename from src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs rename to src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs index 7d76c4d2..fcb57d2a 100644 --- a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs +++ b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs @@ -2,4 +2,3 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests")] - From 18ea6f8efdcf9a41941584a9e4d5789a83a52e11 Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 5 Mar 2019 17:09:55 -0800 Subject: [PATCH 055/254] fix(LdClient.shared.cs): Fixed unit tests by catching NotImplementedException thrown by BackgroundAdapter.netstandard.cs --- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index aba76010..272e1f4a 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -232,7 +232,14 @@ static void CreateInstance(Configuration configuration, User user) { bgPollInterval = configuration.BackgroundPollingInterval; } - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), bgPollInterval); + try + { + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), bgPollInterval); + } + catch + { + Log.Info("Foreground/Background is only available on iOS and Android"); + } } bool StartUpdateProcessor(TimeSpan maxWaitTime) @@ -531,7 +538,14 @@ void Dispose(bool disposing) if (disposing) { Log.InfoFormat("Shutting down the LaunchDarkly client"); - platformAdapter.Dispose(); + try + { + platformAdapter.Dispose(); + } + catch + { + Log.Info("Foreground/Background is only available on iOS and Android"); + } updateProcessor.Dispose(); eventProcessor.Dispose(); } From 40fa90921a5ac9da0b3aca4be202045d7929950f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 12 Mar 2019 19:29:37 -0700 Subject: [PATCH 056/254] implement evaluation reasons --- src/LaunchDarkly.Xamarin/Configuration.cs | 26 +++- src/LaunchDarkly.Xamarin/FeatureFlag.cs | 31 +++- .../FeatureFlagRequestor.cs | 36 ++--- src/LaunchDarkly.Xamarin/ILdMobileClient.cs | 50 +++++++ .../IMobileConfiguration.cs | 10 ++ .../LaunchDarkly.Xamarin.csproj | 2 +- src/LaunchDarkly.Xamarin/LdClient.cs | 140 ++++++++++-------- .../MobileStreamingProcessor.cs | 35 ++--- src/LaunchDarkly.Xamarin/ValueType.cs | 81 ++++++++++ .../FeatureFlagTests.cs | 4 +- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- .../LdClientEvaluationTests.cs | 89 +++++++++++ .../LdClientEventTests.cs | 84 +++++++++++ .../MobileStreamingProcessorTests.cs | 78 ++++++++-- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 12 +- 15 files changed, 556 insertions(+), 124 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/ValueType.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 0435e21e..126a6123 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -108,7 +108,15 @@ public class Configuration : IMobileConfiguration /// True if full user details should be included in every analytics event. The default is false (events will /// only include the user key, except for one "index" event that provides the full details for the user). /// - public bool InlineUsersInEvents { get; internal set; } + public bool InlineUsersInEvents { get; internal set; } + /// + /// True if LaunchDarkly should provide additional information about how flag values were + /// calculated. The additional information will then be available through the client's "detail" + /// methods such as . Since this + /// increases the size of network requests, such information is not sent unless you set this option + /// to true. + /// + public bool EvaluationReasons { get; internal set; } /// public TimeSpan BackgroundPollingInterval { get; internal set; } /// @@ -581,6 +589,22 @@ public static Configuration WithUseReport(this Configuration configuration, bool return configuration; } + /// + /// Set to true if LaunchDarkly should provide additional information about how flag values were + /// calculated. The additional information will then be available through the client's "detail" + /// methods such as . Since this + /// increases the size of network requests, such information is not sent unless you set this option + /// to true. + /// + /// Configuration. + /// True if evaluation reasons are desired. + /// the same Configuration instance + public static Configuration WithEvaluationReasons(this Configuration configuration, bool evaluationReasons) + { + configuration.EvaluationReasons = evaluationReasons; + return configuration; + } + /// /// Sets the IMobileUpdateProcessor instance, used internally for stubbing mock instances. /// diff --git a/src/LaunchDarkly.Xamarin/FeatureFlag.cs b/src/LaunchDarkly.Xamarin/FeatureFlag.cs index bd37ecbf..b6b67d5c 100644 --- a/src/LaunchDarkly.Xamarin/FeatureFlag.cs +++ b/src/LaunchDarkly.Xamarin/FeatureFlag.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json.Linq; +using LaunchDarkly.Client; using LaunchDarkly.Common; namespace LaunchDarkly.Xamarin @@ -10,8 +11,10 @@ public class FeatureFlag : IEquatable public int version; public int? flagVersion; public bool trackEvents; + public bool trackReason; public int? variation; public long? debugEventsUntilDate; + public EvaluationReason reason; public bool Equals(FeatureFlag otherFlag) { @@ -20,22 +23,26 @@ public bool Equals(FeatureFlag otherFlag) && flagVersion == otherFlag.flagVersion && trackEvents == otherFlag.trackEvents && variation == otherFlag.variation - && debugEventsUntilDate == otherFlag.debugEventsUntilDate; + && debugEventsUntilDate == otherFlag.debugEventsUntilDate + && reason == otherFlag.reason; } } - internal class FeatureFlagEvent : IFlagEventProperties + /// + /// The IFlagEventProperties abstraction is used by LaunchDarkly.Common to communicate properties + /// that affect event generation. We can't just have FeatureFlag itself implement that interface, + /// because it doesn't actually contain its own flag key. + /// + internal struct FeatureFlagEvent : IFlagEventProperties { - private FeatureFlag _featureFlag; - private string _key; + private readonly FeatureFlag _featureFlag; + private readonly string _key; public static FeatureFlagEvent Default(string key) { return new FeatureFlagEvent(key, new FeatureFlag()); } - - private FeatureFlagEvent() { } - + public FeatureFlagEvent(string key, FeatureFlag featureFlag) { _featureFlag = featureFlag; @@ -43,8 +50,16 @@ public FeatureFlagEvent(string key, FeatureFlag featureFlag) } public string Key => _key; - public int Version => _featureFlag.flagVersion ?? _featureFlag.version; + public int EventVersion => _featureFlag.flagVersion ?? _featureFlag.version; public bool TrackEvents => _featureFlag.trackEvents; public long? DebugEventsUntilDate => _featureFlag.debugEventsUntilDate; + + public bool IsExperiment(EvaluationReason reason) + { + // EventFactory passes the reason parameter to this method because the server-side SDK needs to + // look at the reason; but in this client-side SDK, we don't look at that parameter, because + // LD has already done the relevant calculation for us and sent us the result in trackReason. + return _featureFlag.trackReason; + } } } diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs b/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs index 1e8b5982..14c609a4 100644 --- a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs +++ b/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs @@ -35,9 +35,11 @@ internal interface IFeatureFlagRequestor : IDisposable internal class FeatureFlagRequestor : IFeatureFlagRequestor { private static readonly ILog Log = LogManager.GetLogger(typeof(FeatureFlagRequestor)); + private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); + private readonly IMobileConfiguration _configuration; private readonly User _currentUser; - private volatile HttpClient _httpClient; + private readonly HttpClient _httpClient; private volatile EntityTagHeaderValue _etag; internal FeatureFlagRequestor(IMobileConfiguration configuration, User user) @@ -49,36 +51,34 @@ internal FeatureFlagRequestor(IMobileConfiguration configuration, User user) public async Task FeatureFlagsAsync() { - HttpRequestMessage requestMessage; - if (_configuration.UseReport) - { - requestMessage = ReportRequestMessage(); - } - else - { - requestMessage = GetRequestMessage(); - } - + var requestMessage = _configuration.UseReport ? ReportRequestMessage() : GetRequestMessage(); return await MakeRequest(requestMessage); } private HttpRequestMessage GetRequestMessage() { - var encodedUser = _currentUser.AsJson().Base64Encode(); - var requestUrlPath = _configuration.BaseUri + Constants.FLAG_REQUEST_PATH_GET + encodedUser; - var request = new HttpRequestMessage(HttpMethod.Get, requestUrlPath); - return request; + var path = Constants.FLAG_REQUEST_PATH_GET + _currentUser.AsJson().Base64Encode(); + return new HttpRequestMessage(HttpMethod.Get, MakeRequestUriWithPath(path)); } private HttpRequestMessage ReportRequestMessage() { - var requestUrlPath = _configuration.BaseUri + Constants.FLAG_REQUEST_PATH_REPORT; - var request = new HttpRequestMessage(new HttpMethod("REPORT"), requestUrlPath); + var request = new HttpRequestMessage(ReportMethod, MakeRequestUriWithPath(Constants.FLAG_REQUEST_PATH_REPORT)); request.Content = new StringContent(_currentUser.AsJson(), Encoding.UTF8, Constants.APPLICATION_JSON); - return request; } + private Uri MakeRequestUriWithPath(string path) + { + var uri = new UriBuilder(_configuration.BaseUri); + uri.Path = path; + if (_configuration.EvaluationReasons) + { + uri.Query = "withReasons=true"; + } + return uri.Uri; + } + private async Task MakeRequest(HttpRequestMessage request) { var cts = new CancellationTokenSource(_configuration.HttpClientTimeout); diff --git a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs index 39e7b26d..9f13e1f1 100644 --- a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs +++ b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs @@ -23,6 +23,16 @@ public interface ILdMobileClient : ILdCommonClient /// disabled in the LaunchDarkly control panel bool BoolVariation(string key, bool defaultValue = false); + /// + /// Returns the boolean value of a feature flag for a given flag key, in an object that also + /// describes the way the value was determined. The Reason property in the result will + /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// + /// the unique feature key for the feature flag + /// the default value of the flag + /// an EvaluationDetail object + EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false); + /// /// Returns the string value of a feature flag for a given flag key. /// @@ -32,6 +42,16 @@ public interface ILdMobileClient : ILdCommonClient /// disabled in the LaunchDarkly control panel string StringVariation(string key, string defaultValue); + /// + /// Returns the string value of a feature flag for a given flag key, in an object that also + /// describes the way the value was determined. The Reason property in the result will + /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// + /// the unique feature key for the feature flag + /// the default value of the flag + /// an EvaluationDetail object + EvaluationDetail StringVariationDetail(string key, string defaultValue); + /// /// Returns the float value of a feature flag for a given flag key. /// @@ -41,6 +61,16 @@ public interface ILdMobileClient : ILdCommonClient /// disabled in the LaunchDarkly control panel float FloatVariation(string key, float defaultValue = 0); + /// + /// Returns the float value of a feature flag for a given flag key, in an object that also + /// describes the way the value was determined. The Reason property in the result will + /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// + /// the unique feature key for the feature flag + /// the default value of the flag + /// an EvaluationDetail object + EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0); + /// /// Returns the integer value of a feature flag for a given flag key. /// @@ -50,6 +80,16 @@ public interface ILdMobileClient : ILdCommonClient /// disabled in the LaunchDarkly control panel int IntVariation(string key, int defaultValue = 0); + /// + /// Returns the integer value of a feature flag for a given flag key, in an object that also + /// describes the way the value was determined. The Reason property in the result will + /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// + /// the unique feature key for the feature flag + /// the default value of the flag + /// an EvaluationDetail object + EvaluationDetail IntVariationDetail(string key, int defaultValue = 0); + /// /// Returns the JToken value of a feature flag for a given flag key. /// @@ -59,6 +99,16 @@ public interface ILdMobileClient : ILdCommonClient /// disabled in the LaunchDarkly control panel JToken JsonVariation(string key, JToken defaultValue); + /// + /// Returns the JToken value of a feature flag for a given flag key, in an object that also + /// describes the way the value was determined. The Reason property in the result will + /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// + /// the unique feature key for the feature flag + /// the default value of the flag + /// an EvaluationDetail object + EvaluationDetail JsonVariationDetail(string key, JToken defaultValue); + /// /// Tracks that current user performed an event for the given JToken value and given event name. /// diff --git a/src/LaunchDarkly.Xamarin/IMobileConfiguration.cs b/src/LaunchDarkly.Xamarin/IMobileConfiguration.cs index 50341113..8a98fde0 100644 --- a/src/LaunchDarkly.Xamarin/IMobileConfiguration.cs +++ b/src/LaunchDarkly.Xamarin/IMobileConfiguration.cs @@ -41,5 +41,15 @@ public interface IMobileConfiguration : IBaseConfiguration /// /// true if use report; otherwise, false. bool UseReport { get; } + + /// + /// True if LaunchDarkly should provide additional information about how flag values were + /// calculated. The additional information will then be available through the client's "detail" + /// methods such as . Since this + /// increases the size of network requests, such information is not sent unless you set this option + /// to true. + /// + /// true if evaluation reasons are desired. + bool EvaluationReasons { get; } } } diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 89f657a8..6881df12 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6e6ffa82..cbbb1fa3 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -48,7 +48,8 @@ public sealed class LdClient : ILdMobileClient IEventProcessor eventProcessor; ISimplePersistance persister; IDeviceInfo deviceInfo; - EventFactory eventFactory = EventFactory.Default; + readonly EventFactory eventFactoryDefault = EventFactory.Default; + readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; IFeatureFlagListenerManager flagListenerManager; IPlatformAdapter platformAdapter; @@ -93,7 +94,7 @@ public sealed class LdClient : ILdMobileClient updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); eventProcessor = Factory.CreateEventProcessor(configuration); - eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(User)); + eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); SetupConnectionManager(); } @@ -304,95 +305,112 @@ void MobileConnectionManager_ConnectionChanged(bool isOnline) /// public bool BoolVariation(string key, bool defaultValue = false) { - return VariationWithType(key, defaultValue, JTokenType.Boolean).Value(); + return VariationInternal(key, defaultValue, ValueType.Bool, eventFactoryDefault).Value; + } + + /// + public EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false) + { + return VariationInternal(key, defaultValue, ValueType.Bool, eventFactoryWithReasons); } /// public string StringVariation(string key, string defaultValue) { - var value = VariationWithType(key, defaultValue, JTokenType.String); - if (value != null) - { - return value.Value(); - } - - return null; + return VariationInternal(key, defaultValue, ValueType.String, eventFactoryDefault).Value; + } + + /// + public EvaluationDetail StringVariationDetail(string key, string defaultValue) + { + return VariationInternal(key, defaultValue, ValueType.String, eventFactoryWithReasons); } /// public float FloatVariation(string key, float defaultValue = 0) { - return VariationWithType(key, defaultValue, JTokenType.Float).Value(); + return VariationInternal(key, defaultValue, ValueType.Float, eventFactoryDefault).Value; + } + + /// + public EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0) + { + return VariationInternal(key, defaultValue, ValueType.Float, eventFactoryWithReasons); } /// public int IntVariation(string key, int defaultValue = 0) { - return VariationWithType(key, defaultValue, JTokenType.Integer).Value(); + return VariationInternal(key, defaultValue, ValueType.Int, eventFactoryDefault).Value; + } + + /// + public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0) + { + return VariationInternal(key, defaultValue, ValueType.Int, eventFactoryWithReasons); } /// public JToken JsonVariation(string key, JToken defaultValue) { - return VariationWithType(key, defaultValue, null); - } - - JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jtokenType) + return VariationInternal(key, defaultValue, ValueType.Json, eventFactoryDefault).Value; + } + + /// + public EvaluationDetail JsonVariationDetail(string key, JToken defaultValue) { - var returnedFlagValue = Variation(featureKey, defaultValue); - if (returnedFlagValue != null && jtokenType != null && !returnedFlagValue.Type.Equals(jtokenType)) - { - Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", - jtokenType, - returnedFlagValue.Type, - featureKey); - - return defaultValue; - } - - return returnedFlagValue; + return VariationInternal(key, defaultValue, ValueType.Json, eventFactoryWithReasons); } - JToken Variation(string featureKey, JToken defaultValue) + EvaluationDetail VariationInternal(string featureKey, T defaultValue, ValueType desiredType, EventFactory eventFactory) { FeatureFlagEvent featureFlagEvent = FeatureFlagEvent.Default(featureKey); - FeatureRequestEvent featureRequestEvent; + JToken defaultJson = desiredType.ValueToJson(defaultValue); + + EvaluationDetail errorResult(EvaluationErrorKind kind) => + new EvaluationDetail(defaultValue, null, new EvaluationReason.Error(kind)); if (!Initialized()) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); - return defaultValue; - } - + return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); + } + var flag = flagCacheManager.FlagForUser(featureKey, User); - if (flag != null) - { - featureFlagEvent = new FeatureFlagEvent(featureKey, flag); - var value = flag.value; - if (value == null || value.Type == JTokenType.Null) { - featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, - User, - defaultValue, - EvaluationErrorKind.FLAG_NOT_FOUND); - value = defaultValue; - } else { - featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, - User, - new EvaluationDetail(flag.value, flag.variation, null), - defaultValue); - } - eventProcessor.SendEvent(featureRequestEvent); - return value; + if (flag == null) + { + Log.InfoFormat("Unknown feature flag {0}; returning default value", featureKey); + eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, + EvaluationErrorKind.FLAG_NOT_FOUND)); + return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); } - Log.InfoFormat("Unknown feature flag {0}; returning default value", - featureKey); - featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, - User, - defaultValue, - EvaluationErrorKind.FLAG_NOT_FOUND); - eventProcessor.SendEvent(featureRequestEvent); - return defaultValue; + featureFlagEvent = new FeatureFlagEvent(featureKey, flag); + EvaluationDetail result; + JToken valueJson; + if (flag.value == null || flag.value.Type == JTokenType.Null) + { + valueJson = defaultJson; + result = new EvaluationDetail(defaultValue, flag.variation, flag.reason); + } + else + { + try + { + valueJson = flag.value; + var value = desiredType.ValueFromJson(flag.value); + result = new EvaluationDetail(value, flag.variation, flag.reason); + } + catch (Exception) + { + valueJson = defaultJson; + result = new EvaluationDetail(defaultValue, null, new EvaluationReason.Error(EvaluationErrorKind.WRONG_TYPE)); + } + } + var featureEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, User, + new EvaluationDetail(valueJson, flag.variation, flag.reason), defaultJson); + eventProcessor.SendEvent(featureEvent); + return result; } /// @@ -416,7 +434,7 @@ public IDictionary AllFlags() /// public void Track(string eventName, JToken data) { - eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); + eventProcessor.SendEvent(eventFactoryDefault.NewCustomEvent(eventName, User, data)); } /// @@ -489,7 +507,7 @@ public async Task IdentifyAsync(User user) connectionLock.Release(); } - eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); + eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(userWithKey)); } async Task RestartUpdateProcessorAsync() diff --git a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs index 1cd8c9b9..47a66423 100644 --- a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs @@ -14,6 +14,7 @@ namespace LaunchDarkly.Xamarin internal class MobileStreamingProcessor : IMobileUpdateProcessor, IStreamProcessor { private static readonly ILog Log = LogManager.GetLogger(typeof(MobileStreamingProcessor)); + private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); private readonly IMobileConfiguration _configuration; private readonly IFlagCacheManager _cacheManager; @@ -29,15 +30,7 @@ internal MobileStreamingProcessor(IMobileConfiguration configuration, this._cacheManager = cacheManager; this._user = user; - StreamProperties streamProperties; - if (_configuration.UseReport) - { - streamProperties = MakeStreamPropertiesForREPORT(); - } - else - { - streamProperties = MakeStreamPropertiesForGET(); - } + var streamProperties = _configuration.UseReport ? MakeStreamPropertiesForReport() : MakeStreamPropertiesForGet(); _streamManager = new StreamManager(this, streamProperties, @@ -60,20 +53,28 @@ Task IMobileUpdateProcessor.Start() #endregion - private StreamProperties MakeStreamPropertiesForGET() + private StreamProperties MakeStreamPropertiesForGet() { var userEncoded = _user.AsJson().Base64Encode(); - Uri uri = new Uri(_configuration.StreamUri, Constants.STREAM_REQUEST_PATH + userEncoded); - return new StreamProperties(uri, HttpMethod.Get, null); + var path = Constants.STREAM_REQUEST_PATH + userEncoded; + return new StreamProperties(MakeRequestUriWithPath(path), HttpMethod.Get, null); } - private StreamProperties MakeStreamPropertiesForREPORT() + private StreamProperties MakeStreamPropertiesForReport() { - var userEncoded = _user.AsJson(); - Uri uri = new Uri(_configuration.StreamUri, Constants.STREAM_REQUEST_PATH); var content = new StringContent(_user.AsJson(), Encoding.UTF8, Constants.APPLICATION_JSON); - var method = new HttpMethod("REPORT"); - return new StreamProperties(uri, method, content); + return new StreamProperties(MakeRequestUriWithPath(Constants.STREAM_REQUEST_PATH), ReportMethod, content); + } + + private Uri MakeRequestUriWithPath(string path) + { + var uri = new UriBuilder(_configuration.StreamUri); + uri.Path = path; + if (_configuration.EvaluationReasons) + { + uri.Query = "withReasons=true"; + } + return uri.Uri; } #region IStreamProcessor diff --git a/src/LaunchDarkly.Xamarin/ValueType.cs b/src/LaunchDarkly.Xamarin/ValueType.cs new file mode 100644 index 00000000..3874f81e --- /dev/null +++ b/src/LaunchDarkly.Xamarin/ValueType.cs @@ -0,0 +1,81 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace LaunchDarkly.Xamarin +{ + internal class ValueType + { + public Func ValueFromJson { get; internal set; } + public Func ValueToJson { get; internal set; } + } + + internal class ValueType + { + private static ArgumentException BadTypeException() + { + return new ArgumentException("unexpected data type"); + } + + public static ValueType Bool = new ValueType + { + ValueFromJson = json => + { + if (json.Type != JTokenType.Boolean) + { + throw BadTypeException(); + } + return json.Value(); + }, + ValueToJson = value => new JValue(value) + }; + + public static ValueType Int = new ValueType + { + ValueFromJson = json => + { + if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) + { + throw BadTypeException(); + } + return json.Value(); + }, + ValueToJson = value => new JValue(value) + }; + + public static ValueType Float = new ValueType + { + ValueFromJson = json => + { + if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) + { + throw BadTypeException(); + } + return json.Value(); + }, + ValueToJson = value => new JValue(value) + }; + + public static ValueType String = new ValueType + { + ValueFromJson = json => + { + if (json == null || json.Type == JTokenType.Null) + { + return null; // strings are always nullable + } + if (json.Type != JTokenType.String) + { + throw BadTypeException(); + } + return json.Value(); + }, + ValueToJson = value => value == null ? null : new JValue(value) + }; + + public static ValueType Json = new ValueType + { + ValueFromJson = json => json, + ValueToJson = value => value + }; + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs index bd990c54..5b804a88 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs @@ -14,7 +14,7 @@ public void ReturnsFlagVersionAsVersion() flag.flagVersion = 123; flag.version = 456; var flagEvent = new FeatureFlagEvent("my-flag", flag); - Assert.Equal(123, flagEvent.Version); + Assert.Equal(123, flagEvent.EventVersion); } [Fact] @@ -23,7 +23,7 @@ public void FallsBackToVersionAsVersion() var flag = new FeatureFlag(); flag.version = 456; var flagEvent = new FeatureFlagEvent("my-flag", flag); - Assert.Equal(456, flagEvent.Version); + Assert.Equal(456, flagEvent.EventVersion); } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index f93d0ab2..2045f2a2 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs index 78d9fe9a..84bc3bb0 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs @@ -36,6 +36,17 @@ public void BoolVariationReturnsDefaultForUnknownFlag() Assert.False(client.BoolVariation(nonexistentFlagKey)); } + [Fact] + public void BoolVariationDetailReturnsValue() + { + var reason = EvaluationReason.Off.Instance; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(true), 1, reason); + var client = ClientWithFlagsJson(flagsJson); + + var expected = new EvaluationDetail(true, 1, reason); + Assert.Equal(expected, client.BoolVariationDetail("flag-key", false)); + } + [Fact] public void IntVariationReturnsValue() { @@ -45,6 +56,15 @@ public void IntVariationReturnsValue() Assert.Equal(3, client.IntVariation("flag-key", 0)); } + [Fact] + public void IntVariationCoercesFloatValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3.0f)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(3, client.IntVariation("flag-key", 0)); + } + [Fact] public void IntVariationReturnsDefaultForUnknownFlag() { @@ -52,6 +72,17 @@ public void IntVariationReturnsDefaultForUnknownFlag() Assert.Equal(1, client.IntVariation(nonexistentFlagKey, 1)); } + [Fact] + public void IntVariationDetailReturnsValue() + { + var reason = EvaluationReason.Off.Instance; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3), 1, reason); + var client = ClientWithFlagsJson(flagsJson); + + var expected = new EvaluationDetail(3, 1, reason); + Assert.Equal(expected, client.IntVariationDetail("flag-key", 0)); + } + [Fact] public void FloatVariationReturnsValue() { @@ -61,6 +92,15 @@ public void FloatVariationReturnsValue() Assert.Equal(2.5f, client.FloatVariation("flag-key", 0)); } + [Fact] + public void FloatVariationCoercesIntValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(2.0f, client.FloatVariation("flag-key", 0)); + } + [Fact] public void FloatVariationReturnsDefaultForUnknownFlag() { @@ -68,6 +108,17 @@ public void FloatVariationReturnsDefaultForUnknownFlag() Assert.Equal(0.5f, client.FloatVariation(nonexistentFlagKey, 0.5f)); } + [Fact] + public void FloatVariationDetailReturnsValue() + { + var reason = EvaluationReason.Off.Instance; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2.5f), 1, reason); + var client = ClientWithFlagsJson(flagsJson); + + var expected = new EvaluationDetail(2.5f, 1, reason); + Assert.Equal(expected, client.FloatVariationDetail("flag-key", 0.5f)); + } + [Fact] public void StringVariationReturnsValue() { @@ -84,6 +135,17 @@ public void StringVariationReturnsDefaultForUnknownFlag() Assert.Equal("d", client.StringVariation(nonexistentFlagKey, "d")); } + [Fact] + public void StringVariationDetailReturnsValue() + { + var reason = EvaluationReason.Off.Instance; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value"), 1, reason); + var client = ClientWithFlagsJson(flagsJson); + + var expected = new EvaluationDetail("string value", 1, reason); + Assert.Equal(expected, client.StringVariationDetail("flag-key", "")); + } + [Fact] public void JsonVariationReturnsValue() { @@ -102,6 +164,22 @@ public void JsonVariationReturnsDefaultForUnknownFlag() Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); } + [Fact] + public void JsonVariationDetailReturnsValue() + { + var jsonValue = new JObject { { "thing", new JValue("stuff") } }; + var reason = EvaluationReason.Off.Instance; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue, 1, reason); + var client = ClientWithFlagsJson(flagsJson); + + var expected = new EvaluationDetail(jsonValue, 1, reason); + var result = client.JsonVariationDetail("flag-key", new JValue(3)); + // Note, JToken.Equals() doesn't work, so we need to test each property separately + Assert.True(JToken.DeepEquals(expected.Value, result.Value)); + Assert.Equal(expected.VariationIndex, result.VariationIndex); + Assert.Equal(expected.Reason, result.Reason); + } + [Fact] public void AllFlagsReturnsAllFlagValues() { @@ -124,6 +202,17 @@ public void DefaultValueReturnedIfValueTypeIsDifferent() Assert.Equal(3, client.IntVariation("flag-key", 3)); } + [Fact] + public void DefaultValueAndReasonIsReturnedIfValueTypeIsDifferent() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var client = TestUtil.CreateClient(config, user); + + var expected = new EvaluationDetail(3, null, new EvaluationReason.Error(EvaluationErrorKind.WRONG_TYPE)); + Assert.Equal(expected, client.IntVariationDetail("flag-key", 3)); + } + [Fact] public void DefaultValueReturnedIfFlagValueIsNull() { diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs index 074605e4..387467a1 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -68,6 +68,7 @@ public void VariationSendsFeatureEventForValidFlag() Assert.Equal("b", fe.Default); Assert.True(fe.TrackEvents); Assert.Equal(2000, fe.DebugEventsUntilDate); + Assert.Null(fe.Reason); }); } } @@ -113,6 +114,7 @@ public void VariationSendsFeatureEventForDefaultValue() Assert.Null(fe.Variation); Assert.Equal(1000, fe.Version); Assert.Equal("b", fe.Default); + Assert.Null(fe.Reason); }); } } @@ -133,6 +135,88 @@ public void VariationSendsFeatureEventForUnknownFlag() Assert.Null(fe.Variation); Assert.Null(fe.Version); Assert.Equal("b", fe.Default); + Assert.Null(fe.Reason); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventWithTrackingAndReasonIfTrackReasonIsTrue() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""trackReason"":true, ""reason"":{""kind"":""OFF""} + }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("a", result); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Null(fe.DebugEventsUntilDate); + Assert.Equal(EvaluationReason.Off.Instance, fe.Reason); + }); + } + } + + [Fact] + public void VariationDetailSendsFeatureEventWithReasonForValidFlag() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""trackEvents"":true, ""debugEventsUntilDate"":2000, + ""reason"":{""kind"":""OFF""} + }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + EvaluationDetail result = client.StringVariationDetail("flag", "b"); + Assert.Equal("a", result.Value); + Assert.Equal(EvaluationReason.Off.Instance, result.Reason); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Equal(2000, fe.DebugEventsUntilDate); + Assert.Equal(EvaluationReason.Off.Instance, fe.Reason); + }); + } + } + + [Fact] + public void VariationDetailSendsFeatureEventWithReasonForUnknownFlag() + { + using (LdClient client = MakeClient(user, "{}")) + { + EvaluationDetail result = client.StringVariationDetail("flag", "b"); + var expectedReason = new EvaluationReason.Error(EvaluationErrorKind.FLAG_NOT_FOUND); + Assert.Equal("b", result.Value); + Assert.Equal(expectedReason, result.Reason); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + Assert.False(fe.TrackEvents); + Assert.Null(fe.DebugEventsUntilDate); + Assert.Equal(expectedReason, fe.Reason); }); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index fcb0e08e..7969e83c 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using System.Text; using System.Threading.Tasks; using LaunchDarkly.Client; using LaunchDarkly.Common; @@ -17,32 +19,82 @@ public class MobileStreamingProcessorTests "\"string-flag\":{\"value\":\"markw@magenic.com\",\"version\":100}" + "}"; - User user = User.WithKey("user key"); - EventSourceMock mockEventSource; - TestEventSourceFactory eventSourceFactory; - IFlagCacheManager mockFlagCacheMgr; + private readonly User user = User.WithKey("me"); + private const string encodedUser = "eyJrZXkiOiJtZSIsImN1c3RvbSI6e319"; - private IMobileUpdateProcessor MobileStreamingProcessorStarted() + private EventSourceMock mockEventSource; + private TestEventSourceFactory eventSourceFactory; + private IFlagCacheManager mockFlagCacheMgr; + private Configuration config; + + public MobileStreamingProcessorTests() { mockEventSource = new EventSourceMock(); eventSourceFactory = new TestEventSourceFactory(mockEventSource); - // stub with an empty InMemoryCache, so Stream updates can be tested mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); - var config = Configuration.Default("someKey") - .WithConnectionManager(new MockConnectionManager(true)) - .WithIsStreamingEnabled(true) - .WithFlagCacheManager(mockFlagCacheMgr); - + config = Configuration.Default("someKey") + .WithConnectionManager(new MockConnectionManager(true)) + .WithIsStreamingEnabled(true) + .WithFlagCacheManager(mockFlagCacheMgr); + + } + + private IMobileUpdateProcessor MobileStreamingProcessorStarted() + { var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); processor.Start(); return processor; } [Fact] - public void CanCreateMobileStreamingProcFromFactory() + public void StreamUriInGetModeHasUser() + { + config.WithUseReport(false); + var streamingProcessor = MobileStreamingProcessorStarted(); + var props = eventSourceFactory.ReceivedProperties; + Assert.Equal(HttpMethod.Get, props.Method); + Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + encodedUser), props.StreamUri); + } + + [Fact] + public void StreamUriInGetModeHasReasonsParameterIfConfigured() + { + config.WithUseReport(false); + config.WithEvaluationReasons(true); + var streamingProcessor = MobileStreamingProcessorStarted(); + var props = eventSourceFactory.ReceivedProperties; + Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + encodedUser + "?withReasons=true"), props.StreamUri); + } + + [Fact] + public void StreamUriInReportModeHasNoUser() + { + config.WithUseReport(true); + var streamingProcessor = MobileStreamingProcessorStarted(); + var props = eventSourceFactory.ReceivedProperties; + Assert.Equal(new HttpMethod("REPORT"), props.Method); + Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH), props.StreamUri); + } + + [Fact] + public void StreamUriInReportModeHasReasonsParameterIfConfigured() + { + config.WithUseReport(true); + config.WithEvaluationReasons(true); + var streamingProcessor = MobileStreamingProcessorStarted(); + var props = eventSourceFactory.ReceivedProperties; + Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + "?withReasons=true"), props.StreamUri); + } + + [Fact] + public async void StreamRequestBodyInReportModeHasUser() { + config.WithUseReport(true); var streamingProcessor = MobileStreamingProcessorStarted(); - Assert.IsType(streamingProcessor); + var props = eventSourceFactory.ReceivedProperties; + var body = Assert.IsType(props.RequestBody); + var s = await body.ReadAsStringAsync(); + Assert.Equal(user.AsJson(), s); } [Fact] diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 8653b37a..57c8b013 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -25,9 +25,17 @@ public static LdClient CreateClient(Configuration config, User user) } } - public static string JsonFlagsWithSingleFlag(string flagKey, JToken value) + public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) { - JObject fo = new JObject { { "value", value } }; + JObject fo = new JObject { { "value", value } }; + if (variation != null) + { + fo["variation"] = new JValue(variation.Value); + } + if (reason != null) + { + fo["reason"] = JToken.FromObject(reason); + } JObject o = new JObject { { flagKey, fo } }; return JsonConvert.SerializeObject(o); } From a1f08ca2574098d74f81160cc5e457b41d899554 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 13 Mar 2019 13:54:33 -0700 Subject: [PATCH 057/254] always dispose of CancellationTokenSource --- .../FeatureFlagRequestor.cs | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs b/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs index 14c609a4..10174600 100644 --- a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs +++ b/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.cs @@ -81,48 +81,45 @@ private Uri MakeRequestUriWithPath(string path) private async Task MakeRequest(HttpRequestMessage request) { - var cts = new CancellationTokenSource(_configuration.HttpClientTimeout); - - if (_etag != null) - { - request.Headers.IfNoneMatch.Add(_etag); - } + using (var cts = new CancellationTokenSource(_configuration.HttpClientTimeout)) + { + if (_etag != null) + { + request.Headers.IfNoneMatch.Add(_etag); + } - try - { - Log.DebugFormat("Getting flags with uri: {0}", request.RequestUri.AbsoluteUri); - using (var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false)) + try { - if (response.StatusCode == HttpStatusCode.NotModified) - { - Log.Debug("Get all flags returned 304: not modified"); - return new WebResponse(304, null, "Get all flags returned 304: not modified"); - } - _etag = response.Headers.ETag; - //We ensure the status code after checking for 304, because 304 isn't considered success - if (!response.IsSuccessStatusCode) + Log.DebugFormat("Getting flags with uri: {0}", request.RequestUri.AbsoluteUri); + using (var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false)) { - throw new UnsuccessfulResponseException((int)response.StatusCode); + if (response.StatusCode == HttpStatusCode.NotModified) + { + Log.Debug("Get all flags returned 304: not modified"); + return new WebResponse(304, null, "Get all flags returned 304: not modified"); + } + _etag = response.Headers.ETag; + //We ensure the status code after checking for 304, because 304 isn't considered success + if (!response.IsSuccessStatusCode) + { + throw new UnsuccessfulResponseException((int)response.StatusCode); + } + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return new WebResponse(200, content, null); } - - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - return new WebResponse(200, content, null); } - } - catch (TaskCanceledException tce) - { - if (tce.CancellationToken == cts.Token) + catch (TaskCanceledException tce) { - //Indicates the task was cancelled by something other than a request timeout - throw; - } - //Otherwise this was a request timeout. - throw new TimeoutException("Get item with URL: " + request.RequestUri + - " timed out after : " + _configuration.HttpClientTimeout); - } - catch (Exception) - { - throw; + if (tce.CancellationToken == cts.Token) + { + //Indicates the task was cancelled by something other than a request timeout + throw; + } + //Otherwise this was a request timeout. + throw new TimeoutException("Get item with URL: " + request.RequestUri + + " timed out after : " + _configuration.HttpClientTimeout); + } } } From a23efce5c57bac5061358a57ba94c1fa7cae7132 Mon Sep 17 00:00:00 2001 From: torchhound Date: Mon, 18 Mar 2019 13:05:23 -0700 Subject: [PATCH 058/254] feat(./): removed Xamarin.Essentials, build succeeds for all platforms, added platform specific unit tests --- .../Assets/AboutAssets.txt | 19 ++ .../LDAndroidTests.cs | 23 ++ .../LaunchDarkly.Xamarin.Android.Tests.csproj | 98 +++++++++ .../MainActivity.cs | 23 ++ .../Properties/AndroidManifest.xml | 5 + .../Properties/AssemblyInfo.cs | 27 +++ .../Resources/AboutResources.txt | 44 ++++ .../Resources/Resource.designer.cs | 175 +++++++++++++++ .../Resources/mipmap-hdpi/Icon.png | Bin 0 -> 2201 bytes .../Resources/mipmap-mdpi/Icon.png | Bin 0 -> 1410 bytes .../Resources/mipmap-xhdpi/Icon.png | Bin 0 -> 3237 bytes .../Resources/mipmap-xxhdpi/Icon.png | Bin 0 -> 5414 bytes .../Resources/mipmap-xxxhdpi/Icon.png | Bin 0 -> 7825 bytes .../designtime/build.props | 24 +++ .../packages.config | 57 +++++ .../Entitlements.plist | 6 + LaunchDarkly.Xamarin.iOS.Tests/Info.plist | 36 ++++ LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs | 25 +++ .../LaunchDarkly.Xamarin.iOS.Tests.csproj | 128 +++++++++++ .../LaunchScreen.storyboard | 27 +++ LaunchDarkly.Xamarin.iOS.Tests/Main.cs | 20 ++ .../UnitTestAppDelegate.cs | 45 ++++ .../packages.config | 56 +++++ LaunchDarkly.Xamarin.sln | 48 +++++ .../BackgroundAdapter.netstandard.cs | 2 +- .../Connectivity/Connectivity.android.cs | 18 +- .../Connectivity/Connectivity.netstandard.cs | 1 - .../Connectivity/Connectivity.shared.cs | 15 ++ .../IPlatformAdapter.shared.cs | 5 +- .../LaunchDarkly.Xamarin.csproj | 28 +-- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 2 +- .../MainThread/MainThread.android.cs | 51 +++++ .../MainThread/MainThread.ios.cs | 38 ++++ .../MainThread/MainThread.netstandard.cs | 35 +++ .../MainThread/MainThread.shared.cs | 100 +++++++++ .../Permissions/Permissions.android.cs | 201 ++++++++++++++++++ .../Permissions/Permissions.ios.cs | 124 +++++++++++ .../Permissions/Permissions.netstandard.cs | 39 ++++ .../Permissions/Permissions.shared.cs | 46 ++++ .../Permissions/Permissions.shared.enums.cs | 53 +++++ .../Platform/Platform.android.cs | 177 +++++++++++++++ .../Platform/Platform.ios.cs | 104 +++++++++ .../Platform/Platform.shared.cs | 30 +++ .../Properties/AssemblyInfo.shared.cs | 2 + 44 files changed, 1925 insertions(+), 32 deletions(-) create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt create mode 100644 LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs create mode 100644 LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj create mode 100644 LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-hdpi/Icon.png create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-mdpi/Icon.png create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xhdpi/Icon.png create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxhdpi/Icon.png create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png create mode 100644 LaunchDarkly.Xamarin.Android.Tests/designtime/build.props create mode 100644 LaunchDarkly.Xamarin.Android.Tests/packages.config create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/Info.plist create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/Main.cs create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs create mode 100644 LaunchDarkly.Xamarin.iOS.Tests/packages.config create mode 100644 src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs create mode 100644 src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs create mode 100644 src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs create mode 100644 src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs create mode 100644 src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs create mode 100644 src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs create mode 100644 src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs create mode 100644 src/LaunchDarkly.Xamarin/Platform/Platform.android.cs create mode 100644 src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs diff --git a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt new file mode 100644 index 00000000..a9b0638e --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with your package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs new file mode 100644 index 00000000..0904b94b --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs @@ -0,0 +1,23 @@ +using System; +using NUnit.Framework; + +namespace LaunchDarkly.Xamarin.Android.Tests +{ + [TestFixture] + public class LDAndroidTests + { + [SetUp] + public void Setup() { } + + + [TearDown] + public void Tear() { } + + [Test] + public void Pass() + { + Console.WriteLine("test1"); + Assert.True(true); + } + } +} diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj new file mode 100644 index 00000000..47653e52 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -0,0 +1,98 @@ + + + + Debug + AnyCPU + {0B18C336-C770-42C1-B77A-E4A49F789677} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + LaunchDarkly.Xamarin.Android.Tests + LaunchDarkly.Xamarin.Android.Tests + v9.0 + MonoAndroid90 + True + Resources\Resource.designer.cs + Resource + Resources + Assets + Properties\AndroidManifest.xml + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + None + + arm64-v8a;armeabi;armeabi-v7a;x86 + + + true + pdbonly + true + bin\Release + prompt + 4 + true + false + + + + + + + + + + + + ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll + + + ..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll + + + + + ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll + + + ..\packages\LaunchDarkly.EventSource.3.2.3\lib\netstandard1.4\LaunchDarkly.EventSource.dll + + + ..\packages\LaunchDarkly.Common.1.2.3\lib\netstandard2.0\LaunchDarkly.Common.dll + + + ..\packages\Plugin.CurrentActivity.2.1.0.4\lib\monoandroid44\Plugin.CurrentActivity.dll + + + + ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\monoandroid71\Plugin.DeviceInfo.dll + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs b/LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs new file mode 100644 index 00000000..22d1b416 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs @@ -0,0 +1,23 @@ +using System.Reflection; + +using Android.App; +using Android.OS; +using Xamarin.Android.NUnitLite; + +namespace LaunchDarkly.Xamarin.Android.Tests +{ + [Activity(Label = "LaunchDarkly.Xamarin.Android.Tests", MainLauncher = true)] + public class MainActivity : TestSuiteActivity + { + protected override void OnCreate(Bundle bundle) + { + // tests can be inside the main assembly + AddTest(Assembly.GetExecutingAssembly()); + // or in any reference assemblies + // AddTest (typeof (Your.Library.TestClass).Assembly); + + // Once you called base.OnCreate(), you cannot add more assemblies. + base.OnCreate(bundle); + } + } +} diff --git a/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml b/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml new file mode 100644 index 00000000..5aedade7 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs b/LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..80ab1048 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Android.App; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("LaunchDarkly.Xamarin.Android.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.0")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt b/LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt new file mode 100644 index 00000000..10f52d46 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs b/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs new file mode 100644 index 00000000..b7d502eb --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs @@ -0,0 +1,175 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +[assembly: global::Android.Runtime.ResourceDesignerAttribute("LaunchDarkly.Xamarin.Android.Tests.Resource", IsApplication=true)] + +namespace LaunchDarkly.Xamarin.Android.Tests +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + public partial class Resource + { + + static Resource() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + public static void UpdateIdValues() + { + global::Xamarin.Android.NUnitLite.Resource.Id.OptionHostName = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionHostName; + global::Xamarin.Android.NUnitLite.Resource.Id.OptionPort = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionPort; + global::Xamarin.Android.NUnitLite.Resource.Id.OptionRemoteServer = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionRemoteServer; + global::Xamarin.Android.NUnitLite.Resource.Id.OptionsButton = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionsButton; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultFullName = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultFullName; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultMessage = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultMessage; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultResultState = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultResultState; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultRunSingleMethodTest = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultRunSingleMethodTest; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultStackTrace = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultStackTrace; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsFailed = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsFailed; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsId = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsId; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsIgnored = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsIgnored; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsInconclusive = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsInconclusive; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsMessage = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsMessage; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsPassed = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsPassed; + global::Xamarin.Android.NUnitLite.Resource.Id.ResultsResult = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsResult; + global::Xamarin.Android.NUnitLite.Resource.Id.RunTestsButton = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.RunTestsButton; + global::Xamarin.Android.NUnitLite.Resource.Id.TestSuiteListView = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.TestSuiteListView; + global::Xamarin.Android.NUnitLite.Resource.Layout.options = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.options; + global::Xamarin.Android.NUnitLite.Resource.Layout.results = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.results; + global::Xamarin.Android.NUnitLite.Resource.Layout.test_result = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.test_result; + global::Xamarin.Android.NUnitLite.Resource.Layout.test_suite = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.test_suite; + } + + public partial class Attribute + { + + static Attribute() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Attribute() + { + } + } + + public partial class Id + { + + // aapt resource value: 0x7f040001 + public const int OptionHostName = 2130968577; + + // aapt resource value: 0x7f040002 + public const int OptionPort = 2130968578; + + // aapt resource value: 0x7f040000 + public const int OptionRemoteServer = 2130968576; + + // aapt resource value: 0x7f040010 + public const int OptionsButton = 2130968592; + + // aapt resource value: 0x7f04000b + public const int ResultFullName = 2130968587; + + // aapt resource value: 0x7f04000d + public const int ResultMessage = 2130968589; + + // aapt resource value: 0x7f04000c + public const int ResultResultState = 2130968588; + + // aapt resource value: 0x7f04000a + public const int ResultRunSingleMethodTest = 2130968586; + + // aapt resource value: 0x7f04000e + public const int ResultStackTrace = 2130968590; + + // aapt resource value: 0x7f040006 + public const int ResultsFailed = 2130968582; + + // aapt resource value: 0x7f040003 + public const int ResultsId = 2130968579; + + // aapt resource value: 0x7f040007 + public const int ResultsIgnored = 2130968583; + + // aapt resource value: 0x7f040008 + public const int ResultsInconclusive = 2130968584; + + // aapt resource value: 0x7f040009 + public const int ResultsMessage = 2130968585; + + // aapt resource value: 0x7f040005 + public const int ResultsPassed = 2130968581; + + // aapt resource value: 0x7f040004 + public const int ResultsResult = 2130968580; + + // aapt resource value: 0x7f04000f + public const int RunTestsButton = 2130968591; + + // aapt resource value: 0x7f040011 + public const int TestSuiteListView = 2130968593; + + static Id() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Id() + { + } + } + + public partial class Layout + { + + // aapt resource value: 0x7f030000 + public const int options = 2130903040; + + // aapt resource value: 0x7f030001 + public const int results = 2130903041; + + // aapt resource value: 0x7f030002 + public const int test_result = 2130903042; + + // aapt resource value: 0x7f030003 + public const int test_suite = 2130903043; + + static Layout() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Layout() + { + } + } + + public partial class Mipmap + { + + // aapt resource value: 0x7f020000 + public const int Icon = 2130837504; + + static Mipmap() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Mipmap() + { + } + } + } +} +#pragma warning restore 1591 diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-hdpi/Icon.png b/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-hdpi/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c804644c5e47daffed8e8573b5da99d0939bc0 GIT binary patch literal 2201 zcmV;K2xj+*P){EiEnLgO@jbP<$a7N&ovF_Lrmiarpgv{=M+flYGZm(ImUDn zsAf#fZ>ai}o;M9vU2#3IZ=RBHP7XP&P4}yV4qu62^%e-0{`@gjdh#Yzg(r7h<(?d- z%$`Gn`QHC3yQd!$eb$YO{G6@7Kxhb9g2xAsTBj786aJc=4S|ZF1m$%&@*zPmRQIG$ z3W)-eK1*MdW1u8h@3-!p5^#=R%AM^B*dJQ^4HOQQTre0QBIq6}0LkeyKg<>P7IQ#i z9;vi@4(6AbQKwViSH7ywZysgB!2#iL zARxp+TB314L`S_vqqf_{tD^3n6aJ&^%0r7S3I_s$kSHJsDO+gpmA6OLMGbeYNu=Ki zAt}pt4()wpI*1Cwk!1B41V>MCQdHmwf@PX3VDmHJ$aN2^_X)FmsOo%Wev7#Ghy!X0 zR2!;%MR;gYFr1+U!9X}r5K#7*D#X8CKVUILwkhng%y0BtpQ0Tz6c-!_O2>2$gaaXo zY2m4*D|ddx0Ew4Fm(5!UpvVp@y!YX14%zu9dt4>%tg)YK@LOCFfn&dET<#n&kKnH1 zzm(#+hX|H;2#5)Zl>HI&&D`Z-FY8T#ste@41)v~34?i5da^ddLz5$1bJlFdwE`+u0 zH+ag|)rvRdkjgm~M`)q$fpFL%1|V6zhG2D{Xo4SXj`FId1MY!Bog+O%K;-w2dCTp5J&(`w z!7;!2`f^f!F?K8ft6VDp@Z9PzRw$hW2gcBDWY@CXYRQW>- zLL$%g{>n2UCG486k~NM2(U5D4mFYA7L2LlXG zuA$FhgaO>c(evPa4|ENLB;FD_WVxu(ZP_8lC53^85e`Pab97M5u;Qp(6b?{jf1Xh5 zHtm=c&UxYdbg`@ta>C(n1SuTo9<;3Uh8I+=zjTl=U|%WRmK>Y6fCmUZ^n%|`K;fYU za}I>V4an@#3pMVN9A@rudd=M^1RM}ntr+gn%N&F$au2F1Pf<537frYI7YuVd2JejV zYXq8enC{_(0%dj|8YV1&dqmAFyK`tbykL-0F8517`Qr@5po~44!-NHO5d}1>s6*2; z2@o8LCp0K{Na;ArxiSakUP(kQ`O@)IYf>#E-2}D9m#way>?&-}C zRINLWEI>1ttXQ(R@Tz*~H9!aqcCL{G@(f2vK`?aA6IKW1v>zBE3}A>!Nm|srl;-bL zre5uYKqw66|LriUlO6$tfPs(zAf=&oxLYmkZr-9I3}BL`E}AWBQQlwa*C7yQ;^j}m zNWa68khuZr{@iz+oSiUWlzHl+ZPtw&Ot-b82pm>^!5jl2$#)J$UK{(3X1Mgb6j@;Dmtyy1w|(D6&Lp?2%PITsd|mL>7e$?#kqMw61NE4Kf$r&4 zjgYgDGl82cXOc?E8A5Fe zLuwuq5) zskA8!`+_qg{iT!+1{f@yq7F%SX0U2udtV|>=5xwz+SB+9z1nbDW~SAS}_kv_~BLJ@aQ{*AO@vPZeI zp2(E_b2}YJ?r35F1)ue(&Luk(mUZQ2|D)B5m*lmlK8n~CfxoOjB75T~>)|i^dt0`u z3p!Q7*nojDbfyRQw0x_ML|NyZ87)$^CfDa(OAQuvXOQF=btvSWq~9TXNdI-mF18x&iFTuHsk2{JN}K1<4>E! zHd3TZLnB#~QacHu^v86fF{3j+C&ofTpmFOaL4@S?p3y6l)A647Uf-tRh0kYvk9WUw z?z!jPDEh|XI2?!L;(FltHygqEslP(omp4WjhTq%ezF;`X*ZT}5nM@McWh1bCX?>Hys{U7bU2YPYC$dl1 zKMrD8^=z=s4$)Bxb6RLmgKW*rVsQ>6_qVp*d zC+i-|1GsdO;OwIy2IHQmL(#UQ9OWPmz5&@!49=o{Ps(F(=CQm$hzoh3o&P?jcF!7G zg^AU^QzHrAb0KeZ897nkm_GDwCf{ z9%5P4aniL*SlY)nH(jR)PL-?&Eal1SQ#msluawyywU)K zGzOlBb5ggoltS~K*GCgwTOf(0Dfv{4$fts{F9@jKEkAeUC<5`zZuajFP5WbRJ>t{{ zi#YcnkUS0m?vLH;nNGV3p{4c{!8P9wA&ulUZ(2g$Ny-cOsbFU?;yJ-lfaDK|XhQA1 zBBW_3aUn0jEmK?O#T65b`>x-bg*q|- z43@?jr?Zd6X?dU}Vut{%F9hJeZkrLJdg@$UwRIe#_KDIF)Q3xEZ~&Il$&XLAE;x21 z*(Q2CE*mUwtaLEVk;@-{2fWcLR`b(m)8Lw} zm0I#UPQT&3RlS8u1c_sC*6|4wNX@$O0Gj^zdMh#+|^E$Q!c=JfXHM{RSi_$s&<`H#J& zF!=skoXHML|rvJS|S+y<5c`KYTilKU%t1 zANUe0K-Vq?b%7 literal 0 HcmV?d00001 diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xhdpi/Icon.png b/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xhdpi/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e2e57aa90f8687bdfa19d82ff6bda71535b8d5 GIT binary patch literal 3237 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 Xx9ez7Rf!D(00000NkvXXu0mjfmS|-l literal 0 HcmV?d00001 diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxhdpi/Icon.png b/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxhdpi/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8d20a38d1782921964638e873ffc98d148ebda6a GIT binary patch literal 5414 zcmV+>71`>EP)!<)rJIgSxVM!XYsZt`QDSc z1;TDV#wdHjR{DU=bo7Oh+_5`|X>u!MlH4@;D|&L>WSh82G2?j1Frd(#Le81#J)UQe zIQTSiSj=M-y>zKakddtu?rpVxjYpKJjU+5WiD(MtVG|?}KqejnX*dkHXbeU=A|f6A zy`}!$MpB;6GPjMOu*fTf$U_nesD0oKByJyoMBMh#xDpYDbqi}a$Si~0k(7u(9d~E; z{d{_&l7NBA0}<)s=HZA#ckwSwD9%-DUu4fGd#CrQM9j!h&g@+aY*bYmj-6fWis<5k zN(ELC5m!O2tdA`aLANYQVL{8Q(8ti}OiN2Uop##JwAd$5%9{maltp~S2Z*S+4G^$A zEK*v^Jc`>G5{V%pU=#?FfMxdncqbWm=U=|t)5GnZ(&Wn>ce7i2&zJB2zyEyaOyc6_ zZd-k{=0pXfJh1uzBnn3wy#EP(5>Xf>Vo1cP55WbHXqkV(33^w*?uo9KuHUToS1qmb zYJP{I%bsU%WSru?3zM;0ulcc#9(>!quBbLQY>Z41s58AyR}0*RL| zoIGIp@`ND`&;z*;0;!)D`ijrnH=76lQp&EY&u>@xsyFe(;R%G3h$W6@^1wvG6-cuR z(SKj(E!#xzuBWSzscffxbnQ`hhq&_K3WO^Upg_FyQlGf{a`D9ZpMS3MIXX(}b4K@M z6^c@D<4S+!0hBy?TX7h~TU(x}g{DEBBm<32}9Pi&~85;7jYd1YJ0F;X9`3Z#Ae0SE!)r&Y`w{~E$`8~w$h8*$%B&! zbYJ4+VN^#2IzR*(XT{zqG~5XgN3?Nf8fb<%7=ZZ?pg;g| z_y(HD!oeC?>M7f)+WH$J)3)y`nY6gASfFfS;E1C>r8du+cHYB$=7|&f3{RjdZnhzR zQY1FiQA36diC)!K=@(KS97x#`x9xlh)806m$!`X(tasx_u|$TNqa_VT4lUch7vG2lt6%O2jZ&;fta2LDqjqFf>`J+_$mJ*%TQVaL)-S% z-l{F+4&$RNH=_%uJ_AWtS`Usk7?>bI*9`0^Ap-LN9yg2P(fTG#km*N5vP! z_foPx-=d|9AX2&rxcZ0_N&DbX1?;>+hlhZDCxnnD_za@fSMqIq!H8b8HpU3ad#Wmx z%q~kBfJc5hNfysE0|bKRJqU56O`StN1H>V)4fIt>L~ByokT487)`x92ZU?Ag0Oy*s=i1SEtlRebNmd7qZ_? z9Y$-Z(-ED@Pw0SuSM@qro=5yVyGUS6(~dhZ^TE`IB~GWv6Y`nP)_23A&sP24^AG@3lda7t}Bzz zI^%gC&3#We&^xMXEU;V)TOSo?j_bYLUw*ukkyB~&9fm;Iw3&db4<1z&*mXyShn)dK zBsvOooYt1sSgvh9QeIQxwPSq^P8tb{!;mKA3G_WzAx%~&X|$IuSbc0k(ug$B{}=S{ zz4&lRjZ9l_D*7N}P@TW@tngkIUaI7lFZ_rOq(I0834B~bUil&*B&zlnoXu6%nATc~ z`mFF)y#2B#msJ>$FpM3SG+o9?qkT|_s}JtHaMHj)2~>RLD3JSY87gFQf12||t>1MC zby9KyU_60X%YFoIoEgiW$vg$__kp6S1gpFTnn9&E|B~}l+Y_h?;kMmd(GZRZpz3gB zkvm=1co~%XlzzVtHhIQLgA5L}kf_$1XF-2|ZHuc& zM1`W})J?gguC4SmFt3ri1tFg2(eynQe6|NWnhc(@;0A?wR8^>i0a)mBo+b{kDg@l? zv$ouu7nUUAeT{f}kOfX2A=}OX2*=Br`QYTiP(t6wainZ*XKcM`ROYBYi~X)MOexZV zG@R;Sc^=U#=y1m+dcTiNzT?t>DzD=Vae+9YNqrVJmdsb(mXw|b91Iemas_OR0uyE| zAchf1_uFY^I;WI>)lN?O+`Cp5eFa*P^N_PJOuD01#lf5Flp}0{XRH= zZ3_XEAGFi`)375Wsp?hHY;TuA7NN~hJPgJW32dCj17f`L;6Zu$>Vv_3D65?~LEM1K zmxA8qM{>_yYr?>40}Kg4@(4yVYytr>#%&3dE?Fo!Y&%DsK+4z+>InNbck?_61j3@G zD5+G4rwIYoaf4q)iK`EuFa!xCO@*9ImUF}j;s#2f-#hnbw96R$;o)|%YgPco;paTq zcX&a44^J9C=OaKKDahNC(oUSvM4-I-xeYS<8SPCn27&Dff%1i2iU253w{jjxY*pw_puBx4XNe1Z6V2Fo1$UU=d}(88 z>pXNi4D>ooE?K}?ffgOg^Wa+)Dp7F9C2NmMDv6ve>47iFz-8YIz@EoI4KskkLvsT!tr%ZR9N^0r|6>ZOP;~F;2XeQk z90RO3R+`k$;1n4ySt~($|KW)&Py~{Pt1!}r;vX%zni#)76>Pdg#HF{TKIQU zXwUNiMB-0Y=sPe3hK6(A`TG`HYjw@BFYg6Z=w=1|9vpY?7{|W!bDdBYygc#55wPWm zO~>;*sPZ6}f8+BW`9zw(C;8m{${N#J)0zY7gC8tZN7;AJdUet|vh|?SaEb$5Vt~$e zfXz_BJdfz@sRUh17J$O$xq<1gOjt*2OlyrFEW~a7?-cx@&wxH7@^+d}Fs=!1hGEhG zgu%&UgKaua9z2@Sog3%u9Z4r>ZE20wwMK2bc3VGXQM|kOkQFLTJYk?=AaDy9Xwe1F zZvj`&3G_Yss!u_aIgn5@&Mj)&;o+i!`?Eiv?E#1akc5zlD-M220w7FgkSMs~;5WwW zzt082`q^|3ttmD%6jz~J6x=y3=c!cZ!Hmm1aS&j{!3d?@o_E3agp~4iEeVwWRodle za{io3Yf5V?rari>@1x*e%ZJmpyIPsvg(V7Kmf(FWJih}B2KCMJ;Qfvq+n)CR)a|sE zw5H;=UE6T6U+mt*!QPicx3Xh@wAo# zLqj23KOp6eDH~mf!zc(8T*n=i$o2z%KM#806HgosWRavmc-CjuKPPRZHRNr(ydRS~ zXzDxg;r=5XU!7&)@zmL(TzYkyXTyywWBqpwUp5~BiZR&6~rVi z|J$OHW-CsdP=WxT0mWfH1B3z8$2c<{JnJ)MdHf<;J6c1zUv&Ts7b!QAQ=j||P?n&Q zh1+s1U!SBsb_z_EC2Ww&N9Mm~enV>}YiQW1;o=1M=(>mus5b%dOG=$DL~do!Vi`i8kG9Zi<@1oHna%_ zhJd)M!Uux5(?W>@kVw{?2haMvHF*=Q5s>wf@B?OshK?FE%D&fpR&$I(VB0}^S)a8ptWmQ* zx5^(Hif4v~&)<>q9oP8sL_y0E-W>CUv2lYyTy1a_(iQWzCwxpFNK}!cf9;j^Lkgq!Wmn>vxXzm!3H8L5<`snb39tCwzOWI{pBep@$QO#XI(!FW|R%p=Ap{Rs+m+ z4AS%9ujmRko8%gutA@!kA%!n z%!ruS+}$asG+x96Zg}Ez9PzX1NP&F+5wiO;s*HE5p@a0TE#MJoP5N)u@|9mx8;Iq3r+>tkpYG6ge3_dPWz&fbvvBjBPq;>dJXhEt&2hU6dVorYw6 zOt|_O)Yi{j`}|7Utry>Fh}b3>zmE4G&##IU+YV=jZn^80KV@%EI?j}R zgFJ@qb%3nTHX~+zq-2I3n()9l`@XdEU0t>eN*drhyi(88J2Pt5M@nXB)@uo`JDbyG zII4&u5*^W*_1X)s8$0VGAv4tP{(g^U@0fj3qq?x4BNg*@B%P#pC7$((JX_!Y+5D%i zN-4VpPK_guEWp=KUK;E8O9t>AUJ>?#9|gKMaovP<_Pwcy!t^K-ETG!qliNZLwXleSH!QNcY`<1J+q>>WjKh!IPsh zV&^5lIl12QapKXe`k804w$BPgGU#ti|9INh^f%MrPVYc{7k8@fCYmo@*GpNDM7>p) zakqK}f9!wSF=G@2Vkin0yDA7l7HmXO&tPW}i1)wLCNM%8K_u082Ic_o@ySn8eO7a| z;47E~Gg%*GKhs6(hUeqza)?-GJW&sncjDjU%J1tvs5dfR$b6)O{d;XWJm24m=5CM@ zMK0|H65NHXsT-X6_dAcL(L{k8%G?Ea+p>~*8h7eAO#kONL_|bHL_|bHR{PNX%DOYD Qy#N3J07*qoM6N<$f>DK8^1Vq1d3mpOi1{5jMYZ8zmND)wwk|0QvE?r7! zQltc=N--26NK2>*A?5LVf4rHuGkdppvt{n?bGJ9MZ_P{$I5~tl0002z!v}hgPWz7k zF+1C7`)J++1OSLzJ=D{A5(M1pR0f_-c1fHRH}sx&JQe>R@!?SssXQ^Cl@QY^-aHl# z(QQjJ15n0Jz_uHFD-Z}S`IVv zH1i8#%sN8h(s;y;N3`7a+K7JRXQ@1y)T4Mcy2UUFg@^VxQR&;YZ*455-SU3o^j4w1{(Y8R+iueAX!az@hlfthJer_)E_Qj+(dN zEa)wJsGFTDKf#C=wLay~?GmUH#vOKnr6qx@J#PIT*e*;-qJ@&zV*K21Zp};97berz zoc77&#>1?M0M^f1?5x)4uO;WhX-0OFIIV=$F}@=N>dsyKRE!V;^hv^wtts{5=lY{bL}?A7T2$!jhW6kAXzT=S{Ov8Vyu z*+jc&A1J7#b0F(%0Jj5?E&O63xQN98-1;!n7?z@G_(LG)1sIrtWdIld)k@oVUrRek@@-`3Q@7b#XddB8WNFbl^p2WeVDrZ|7NnO0@Sf^}ML-9)5@+gF z?VqIxuv%B9y5B%ZEy>f^S|Xm_viYO=7IRxL(fN0pDRO4f6d_4wsC0g5aW>KAkK@4Y zUi=+v_=osS@7Vkc*6hHbZPOe9Ao?hK#GEjLBto(vHMAvu{vPhbe>;DI4u>8^oOul1 z|CxNe?uDY{32U=rye>KfxI3??zvSkN&u6GO_*5@0Ie?kD+Up>IZXrNUE888XOF2!? zE^Xlq9`IMpM(tg{KZ^dj2P|466=e*%bK;n3*O{W;;#kv`G+nk?WK9t|Vvp51=yMVq%?93IQ zb&B0#wm=|M4zAlK48&Yl+k4G!tr|~PH+3O^t85{Q>&cx}y2qbG%Yay~P<9*gGui5U znl$75^(tZi1iB+eGYH;s{enK@w@n0)c_C+SCEJHyWuru|%0e)7IaoyV@q67h^GuMl zdXU}H@q%o35#(`~zr; zyMf4~0qZr1DgYr`Qre^#YYJ=rI^^g%&P#T)S#kC%r7Tb-p{ja0surLS=EINJ;PPf# ztwi830&NolpLzpDU6Ie*=!eS@OJRyioL`gk2q4q&t85eCk4#^_LW0=EMjjX~xaK?`9AFbD$OhG!fefG-=b_Mw^CH_hA)VYf1FWXZ4~9e$1d-5IT%dF9gEi@r3 zE;;6un{EWzH^8ng;oJk^&?%pjgP}u?oDPW3dpY<^Wf+nog5$whBU0{D0=JU=VkUw$CJ zrb~S)s?bIliK|cCx-W&eJ}atBzj4aCxK%!(7Y6g@+Urduq2spb-*nzx7&PR@yX zaM&18N!^=?mQblg9DYfEH_VkfVroVGx=5EQW9y%RT$4=Y>L?v+4HscPGnXIzT@-cfx?Ry_6Fhw!xfO zOaXLv;D_mZArX%JZZFi^mbktP3tlQOpW>61?%ST`@YD9!W>e}Xts3#;l^ag|ir;E?GVJfbt{=a@qf@FDhEmeA+8E`N)t4ij&%EyUJ5{zwD~5Cu6$|am zMQR=-Ar;Nw?5$R`&W|?pM)=jI*X|}%PrO|~-8P*x(3A(QGwn%56O0H_j~ryAh- zjqq4Gf?#R}%7dso3}{V1JViN1IS8*eOzAcA8BC^A_`n`iPOg5zF*L19J&&C7wh6;d zhP2@q=w9Wwc>;MF)O3#yde85axQ!i5syCvchS|)2Hl{Xcbl{&Ow+Ni+oss0+Mfm-N z{K=tClN5dDw0)^;w;yklO1#?9P;nZN*vt*hJ7!*#lNAo^355qSD@C5umYqz#oZ$Ha zfBnMd#)b{#u}H-RK=VpaVfPlrGRKj_C1kQ$iBcQ$$Mz#DHO*tb_UjwvE}NE}LwJ^p zsaPJqQF&0Rtz>Zj-ESHl!C?Lw#Rs0>ufhm@KxvOV;wx&b9+~jQ@<;MBn-&lq77*rFd3vflw#6j}+;~Tkr#BjUWlVhh&IgrN2 z!9+U_Iz>Q_as_#0#IDyW)<<0z_Dwp!VEeTYlc5(;*}xU=;M}HpAVk=mXkIp}#(_1c zl4r{ROXdb|RUC^2{ZHDlV%u#3ETueUrk4W57r zhBKnmqLR7!Ex$~|zk6cJB5hb(^SlhIQ|GDU_7`ZAX#%vH=NjKUju?7L*U535H^j8? zs4(wZ-$wh+M_53#*ffhY-7G9s@c1f~zPebWKN|oO+6nFAeH108#%T3w2v&QdOauCVBR|m(JEq8;ov7*^2!=p$!kBc;?I(zH5Yp5if~ZO9bG;N%lAG zYck*!_;wRH?sfv_Ps-|-CZ7#oaAvr*@ zDD?Y)`~dWs2w6YCNlmkUn{=BiOi4v4BIM3^{B><{zr#1#rTYb8p?8_!hP6#?LcA@V zt0NY_(2G=D=33Qo@3koZu1eLjJ^Q{~gn5JWeWC%%^3W>(lJ zUHJB)H&A7o1aPwAp;cWQE=FfCJ1CNM4Re~-`Ho1Jl?k9Kg=cEfj$MY>2>AYc@Z9t$3O$Q~$9>ypmd9>(-(2Ll3|M8^_W-otoh=_&dNAI_-{Y!SQT}y+Vx<{XXywTLD zRl9vpL?6yz$rJDnH$KIJ7;5p{MOx^+f!vC}beY^s;c7leB_pfzg>+R9-L;8v0SjI- zV$OMS@Mu5bhe%$CdIvy5a@7CswJnwDX9q}ooYHXWta>aLzm7tF)y`%1~62Cr1BJRPgJAXED$W8OFvxa;wF5Yf##OE}` z{!&lZ$z*}uV|VGj)M?z;YbD0F)-+IPrkz(P|6uvu_z%HnO80Yn2YKJu;?k%eRb#h4 zFZ45aAHF!Q$x!dV_samkx5Iul8ltj=)o6 z9u9_w17jvpL{yq&5MoLd(hW?>?$~~(gTRB>W;llTT;xst^0OUKkc*#Jl6=27eHM`L z>p08+uIwIT1;2T7?##JrN!^89)b_WeiYtcU30Q?H6s|O0J0Tpse%2d)RHS-B+1YDn z7yZ5DqhU?tkjzdz7)Iewb#q!ZyJ5EOz2H!iStIGQai`{|B_ya)a|+Ks(-PkUJ!MP5?21XJO5J* z(`tjfot4S>kT&j%TiR+TmyaNqj!y#MiqO@`u6mmlc;O4KUO_*mewe<0 z{;);zN>6gg7agwq`ufsO#e7?h@AU=-w&MAB_d1`%GF=G2@com$I5wHLg&G8{+EK>{ zbxJifJ}moU{kY}c_WOgidFLL}cYCsNuZf$zWteVFz>WNIa0@y8EK9**8 z=(5uNu(W^Ul1H(?>Mrt(2N{?# zurGupabZny3G?G~>UBDDy`Z=lxpwc}gij|Avy1K*Am_mWZDJ5PkzZPQadnAe+$pweD;QWg(D(u z*XVIzN@?$Mizchdi+huH9*v;a;r}!sJ4Xk+t@oo9M1%Vm-XEQF|1}}w&f8=pf{!Yx z6BVFoA57eoU6{7+TeCs8g^aE7_DsXxVEtY_#2-sBw(-E*$K#@GtAl2{FUI_TNxG-j zz4eAjPA)C=Si8_)=&XFNM3g+ey1c}!jv;SvF7F>lb!gfHCVvmkiQEJ(Ao4S0Tm4pA z@24FLDVQBbzZwfQ9$6lX#|S{TV|6R~CvIjg07Lv)f1xDg!sZEmY%@PwTSKa;OzE_W z#cS@P6U%K)+gBY#4KpooEi^Ys5yh%cRFXIuLdB{C>O0lqg76k9?0pUxQ~6*@d#;nG zK0^aHysNcynKPcZ?G5)CDVY$`D%cwS;h}3LR=blUF3(K$@Gfyj{!jxlLe4~gm_?1W z$t9&6IsVm&lvh)``sz^#E&5m0jzzQ>`y5Zn+70#ao;v(~y_ z_NDI3zb~5B4YH3!rT+T0h6f%Iru{)XAss(-TfqXbpv4yoN@b7lVyimMji&EXNC+MTFnd~UJpy&&uN&R7~Y5MYV@G)YLS=0}R0z=j}Abp^OMW;F2 zFUr4!M>pjdmU%F2M}F4t&fcUO&jSVYQjyx{8-gN|Rcip}Fn~Yh0&~f=^s{BKbc1U8 zMgY7B=9r2p%row&@1#S{aCZoX%w?Kf`cq!Xg{{&fY6l(R8y-5+kdQo>gtXUdlLdX8 zvBESyIILWV6*9lvU3w?%SrsC&2{LXq|UCuT8FGhm2_?&!#?p?f~ zl_bv$Oq5AxKNH-3-ck_At+q{0uXFiESLTU#;0a}GP@Jo&98^>YQL3<3REh%Sb_?x_ zX`#XRWB-6q?e;0BPzDJ?Rt-K8;VY{*G?E2K#Ibg}APISgi7xy;7cUmF?twjTdMA8kKIFI@ zLq#FcY+SSI#!KH?)Ex?=!@>}APGMo`tH4U>RJ+xu*nMX65XYyo?jRTq^jThQF89&A zcj*DYZ61?W6u->qNA^N>M2mBTMcv9~`jOd$ogXf13H7MF<0$=?iuTl`-ZXmu{LS~( zl7T@!0-zm?egFf7G=0&b3r9h74g;xIPf`Xb$?2UGxqm_r?1;qFjeV4&^z28fB49Lb z#4_M_5g*G-$ADn_WAR<9@R42DMyLl&AhSwQ%@>-p zR1*}Tu+h@mLm|jiWM&C{#qD4Z&Yd%DBH*7^RB~zev2|WdIi}30-nze~(LNNnz!QFX zlL2&Raitwr>kp%ld>99+i*qsf03R$4MmNQnK$$82pGLFs8CVMJl{MLgYYHkdcd!NenEd}XT``kwmkbX|*AaZJ(4WDT7oge?p{4B<4g1UwQ3SVZYz zp=!f%;ykT%4T$(o#%t3w8M7&3!jZ!3T`Kk za1p6C`iCz^yi$E-Tt7A7m2zL1Ho@}#MsJ00R+2BB2jO?k@5lQ3G408!u4DIg%Q3X* z@^H0k`s)z3RA;}GVtzX4*$OQI$R9Oly-;k$rTGYPLABC`eyfs|*s>x3Hs>ghHUvOB^G^pRW?E7Ff-QC!9Vxym8xGBUgTJ691 z)F!9TK;&2R*H?c@dkMC<OHepPeMV(?GGIq<0ip$Mq3iL!IUB7zCiM>Lh z9NsCCRr^1LTzgL0m~CquTgctt9VW2x*f_V3Yit9p7|nd+;gtWYqA?s?JenSZ-(B@p z*G4g-W@rn;cZ&~TG`_=gMgvAeCBeh{H@^+4e6acOFX>IG$LY~mR=IN?!f2DH3Rgn3 zdlq0?-V;*F{JsiA8jhhA9iXYPjdLY>QDXM9PA zTwaEMq;2F*_mVS&t>|~!tD6Fvxby8$0Ee90tJARu|E8jE`cD)eNTe$L8s7ghZ{6wI zNiJgP>dQSFLUCxN(5=X?xBBordI|_RtD5f>e|>F4Y^0jYF_S7vo|CnM0k`~)U2H#D zIB-?dgRu$=4_`Xh&|fDwyOjR;(Zgw6s^LJ50QV>$`x^6?gT_wkWMnqsH~?>3ISRR& zjrS^fNqW#vsIUjWoec;KI$nc%R9i8SjrwUC!_BWabG2CXs{`l}mE`YPzO!!fY=4gG zLfCljOD8?Jd97#K*fOqOP6|`oqqsw1N#b`XzL{5V)=jZ3c+zVccv(HMrI4;0`%D5W zz}x1(UI;V6qNX~8eeylK{BpS8s@Oj^jEX($HQ^uRj=a22=HIxW;nAXloi8W%a*|zO zo0c6K7qUxJxRZNL#p~zyA9==_OXbOU7%uV%&v8~kjIf!go0|LLrOCm>`T#f|-!ivM z&`tOLluN$9vQaaCUk-$7PS}W6wlC&K7p$E#>~S1bDS#PZQ&7^HW)bjY{L%mulPLitqcvA>@jw-154EsR0)w$`UDS<0(LTtD}Rn%>Eys49` zir3+b!iaO?^tnKh44<11?SE~>#C#|cOEgTse>Lcbqe`#sdR5MNWr3?s^J(4ct>pht c0$sN|hZ~f>0q(p1ua5js-$bwSo@3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist b/LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist new file mode 100644 index 00000000..9ae59937 --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Info.plist b/LaunchDarkly.Xamarin.iOS.Tests/Info.plist new file mode 100644 index 00000000..1cbfd464 --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleName + LaunchDarkly.Xamarin.iOS.Tests + CFBundleIdentifier + com.launchdarkly.LaunchDarkly-Xamarin-iOS-Tests + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 12.1 + UIDeviceFamily + + 1 + 2 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UILaunchStoryboardName + LaunchScreen + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs new file mode 100644 index 00000000..c70de6b8 --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs @@ -0,0 +1,25 @@ +using System; +using NUnit.Framework; +//using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + [TestFixture] + public class LDiOSTests + { + [SetUp] + public void Setup() { + //LdClient client = LdClient.Init(config, user, TimeSpan.Zero); + } + + [TearDown] + public void Tear() { } + + [Test] + public void Pass() + { + Console.WriteLine("test1"); + Assert.True(true); + } + } +} diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj new file mode 100644 index 00000000..5ad98579 --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj @@ -0,0 +1,128 @@ + + + + Debug + iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923} + {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Exe + LaunchDarkly.Xamarin.iOS.Tests + LaunchDarkly.Xamarin.iOS.Tests + Resources + + + true + full + false + bin\iPhoneSimulator\Debug + DEBUG; + prompt + 4 + iPhone Developer + true + true + true + 46596 + None + x86_64 + NSUrlSessionHandler + false + + + + pdbonly + true + bin\iPhone\Release + + prompt + 4 + iPhone Developer + true + true + Entitlements.plist + SdkOnly + ARM64 + NSUrlSessionHandler + + + + pdbonly + true + bin\iPhoneSimulator\Release + + prompt + 4 + iPhone Developer + true + None + x86_64 + NSUrlSessionHandler + + + + true + full + false + bin\iPhone\Debug + DEBUG; + prompt + 4 + iPhone Developer + true + true + true + true + true + Entitlements.plist + 42164 + SdkOnly + ARM64 + NSUrlSessionHandler + + + + + ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\Xamarin.iOS + + + + + + + + + ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll + + + ..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll + + + + + ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll + + + ..\packages\LaunchDarkly.EventSource.3.2.3\lib\netstandard1.4\LaunchDarkly.EventSource.dll + + + ..\packages\LaunchDarkly.Common.1.2.3\lib\netstandard2.0\LaunchDarkly.Common.dll + + + ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\xamarinios10\Plugin.DeviceInfo.dll + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard b/LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard new file mode 100644 index 00000000..5d2e905a --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Main.cs b/LaunchDarkly.Xamarin.iOS.Tests/Main.cs new file mode 100644 index 00000000..c040831c --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/Main.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +using Foundation; +using UIKit; + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + public class Application + { + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "UnitTestAppDelegate" + // you can specify it here. + UIApplication.Main(args, null, "UnitTestAppDelegate"); + } + } +} diff --git a/LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs b/LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs new file mode 100644 index 00000000..d5e666aa --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +using Foundation; +using UIKit; +using MonoTouch.NUnit.UI; + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + // The UIApplicationDelegate for the application. This class is responsible for launching the + // User Interface of the application, as well as listening (and optionally responding) to + // application events from iOS. + [Register("UnitTestAppDelegate")] + public partial class UnitTestAppDelegate : UIApplicationDelegate + { + // class-level declarations + UIWindow window; + TouchRunner runner; + + // + // This method is invoked when the application has loaded and is ready to run. In this + // method you should instantiate the window, load the UI into it and then make the window + // visible. + // + // You have 17 seconds to return from this method, or iOS will terminate your application. + // + public override bool FinishedLaunching(UIApplication app, NSDictionary options) + { + // create a new window instance based on the screen size + window = new UIWindow(UIScreen.MainScreen.Bounds); + runner = new TouchRunner(window); + + // register every tests included in the main application/assembly + runner.Add(System.Reflection.Assembly.GetExecutingAssembly()); + + window.RootViewController = new UINavigationController(runner.GetViewController()); + + // make the window visible + window.MakeKeyAndVisible(); + + return true; + } + } +} diff --git a/LaunchDarkly.Xamarin.iOS.Tests/packages.config b/LaunchDarkly.Xamarin.iOS.Tests/packages.config new file mode 100644 index 00000000..d0029198 --- /dev/null +++ b/LaunchDarkly.Xamarin.iOS.Tests/packages.config @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.sln b/LaunchDarkly.Xamarin.sln index 386a7301..82f88ff0 100644 --- a/LaunchDarkly.Xamarin.sln +++ b/LaunchDarkly.Xamarin.sln @@ -6,20 +6,68 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.Tests", "tests\LaunchDarkly.Xamarin.Tests\LaunchDarkly.Xamarin.Tests.csproj", "{F6B71DFE-314C-4F27-A219-A14569C8CF48}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.iOS.Tests", "LaunchDarkly.Xamarin.iOS.Tests\LaunchDarkly.Xamarin.iOS.Tests.csproj", "{066AA0F9-449A-48F5-9492-D698F0EFD923}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.Android.Tests", "LaunchDarkly.Xamarin.Android.Tests\LaunchDarkly.Xamarin.Android.Tests.csproj", "{0B18C336-C770-42C1-B77A-E4A49F789677}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Debug|iPhoneSimulator = Debug|iPhoneSimulator + Release|iPhone = Release|iPhone + Release|iPhoneSimulator = Release|iPhoneSimulator + Debug|iPhone = Debug|iPhone EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7717A2B2-9905-40A7-989F-790139D69543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7717A2B2-9905-40A7-989F-790139D69543}.Debug|Any CPU.Build.0 = Debug|Any CPU {7717A2B2-9905-40A7-989F-790139D69543}.Release|Any CPU.ActiveCfg = Release|Any CPU {7717A2B2-9905-40A7-989F-790139D69543}.Release|Any CPU.Build.0 = Release|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Release|iPhone.ActiveCfg = Release|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Release|iPhone.Build.0 = Release|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {7717A2B2-9905-40A7-989F-790139D69543}.Debug|iPhone.Build.0 = Debug|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|Any CPU.Build.0 = Release|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhone.ActiveCfg = Release|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhone.Build.0 = Release|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.Build.0 = Debug|Any CPU + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|Any CPU.ActiveCfg = Release|iPhone + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|Any CPU.Build.0 = Release|iPhone + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhone.ActiveCfg = Release|iPhone + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhone.Build.0 = Release|iPhone + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhone.ActiveCfg = Debug|iPhone + {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhone.Build.0 = Debug|iPhone + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|Any CPU.Build.0 = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhone.ActiveCfg = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhone.Build.0 = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs index 403d6eae..f5f66010 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs @@ -10,7 +10,7 @@ public void Dispose() throw new NotImplementedException(); } - public void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval) + public void EnableBackgrounding(IBackgroundingState backgroundingState) { throw new NotImplementedException(); } diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs index 29dd8742..94c6fd47 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs @@ -36,11 +36,11 @@ public partial class Connectivity static void StartListeners() { - Permissions.EnsureDeclared(PermissionType.NetworkState); + Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); conectivityReceiver = new ConnectivityBroadcastReceiver(OnConnectivityChanged); - Platform.AppContext.RegisterReceiver(conectivityReceiver, new IntentFilter(ConnectivityManager.ConnectivityAction)); + Platform.Platform.AppContext.RegisterReceiver(conectivityReceiver, new IntentFilter(ConnectivityManager.ConnectivityAction)); } static void StopListeners() @@ -49,7 +49,7 @@ static void StopListeners() return; try { - Platform.AppContext.UnregisterReceiver(conectivityReceiver); + Platform.Platform.AppContext.UnregisterReceiver(conectivityReceiver); } catch (Java.Lang.IllegalArgumentException) { @@ -66,14 +66,14 @@ static NetworkAccess PlatformNetworkAccess { get { - Permissions.EnsureDeclared(PermissionType.NetworkState); + Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); try { var currentAccess = NetworkAccess.None; - var manager = Platform.ConnectivityManager; + var manager = Platform.Platform.ConnectivityManager; - if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + if (Platform.Platform.HasApiLevel(BuildVersionCodes.Lollipop)) { foreach (var network in manager.GetAllNetworks()) { @@ -140,10 +140,10 @@ static IEnumerable PlatformConnectionProfiles { get { - Permissions.EnsureDeclared(PermissionType.NetworkState); + Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); - var manager = Platform.ConnectivityManager; - if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + var manager = Platform.Platform.ConnectivityManager; + if (Platform.Platform.HasApiLevel(BuildVersionCodes.Lollipop)) { foreach (var network in manager.GetAllNetworks()) { diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs index 38af6cec..22c73f8f 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs @@ -21,7 +21,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; - using System.Collections.Generic; namespace LaunchDarkly.Xamarin.Connectivity diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs index e8a23616..00e4ec58 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs @@ -70,6 +70,21 @@ static void SetCurrent() currentAccess = NetworkAccess; currentProfiles = new List(ConnectionProfiles); } + + static void OnConnectivityChanged(NetworkAccess access, IEnumerable profiles) + => OnConnectivityChanged(new ConnectivityChangedEventArgs(access, profiles)); + + static void OnConnectivityChanged() + => OnConnectivityChanged(NetworkAccess, ConnectionProfiles); + + static void OnConnectivityChanged(ConnectivityChangedEventArgs e) + { + if (currentAccess != e.NetworkAccess || !currentProfiles.SequenceEqual(e.ConnectionProfiles)) + { + SetCurrent(); + MainThread.MainThread.BeginInvokeOnMainThread(() => ConnectivityChangedInternal?.Invoke(null, e)); + } + } } public class ConnectivityChangedEventArgs : EventArgs diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs index a50c924e..ad49c2c1 100644 --- a/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs @@ -25,13 +25,12 @@ public interface IPlatformAdapter : IDisposable /// the application, and provides a callback object for it to use when the state changes. /// /// An implementation of IBackgroundingState provided by the client - /// if non-null, the interval at which polling should happen in the background - void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval); + void EnableBackgrounding(IBackgroundingState backgroundingState); } internal class NullPlatformAdapter : IPlatformAdapter { - public void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval) { } + public void EnableBackgrounding(IBackgroundingState backgroundingState) { } public void Dispose() { } } diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index b1463d06..4b02a400 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,26 +1,19 @@ + + netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; + netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; 1.0.0-beta15 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin false + bin\$(Configuration)\$(Framework) + true + latest - - netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81 - - - netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81 - - - - - - - - @@ -28,6 +21,10 @@ + + + + @@ -70,8 +67,5 @@ - - - - + diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 272e1f4a..1af85dc4 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -234,7 +234,7 @@ static void CreateInstance(Configuration configuration, User user) } try { - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), bgPollInterval); + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); } catch { diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs new file mode 100644 index 00000000..338cf637 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs @@ -0,0 +1,51 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using Android.OS; + +namespace LaunchDarkly.Xamarin.MainThread +{ + public static partial class MainThread + { + static Handler handler; + + static bool PlatformIsMainThread + { + get + { + if (Platform.Platform.HasApiLevel(BuildVersionCodes.M)) + return Looper.MainLooper.IsCurrentThread; + + return Looper.MyLooper() == Looper.MainLooper; + } + } + + static void PlatformBeginInvokeOnMainThread(Action action) + { + if (handler?.Looper != Looper.MainLooper) + handler = new Handler(Looper.MainLooper); + + handler.Post(action); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs new file mode 100644 index 00000000..9ac0fb94 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs @@ -0,0 +1,38 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using Foundation; + +namespace LaunchDarkly.Xamarin.MainThread +{ + public static partial class MainThread + { + static bool PlatformIsMainThread => + NSThread.Current.IsMainThread; + + static void PlatformBeginInvokeOnMainThread(Action action) + { + NSRunLoop.Main.BeginInvokeOnMainThread(action.Invoke); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs new file mode 100644 index 00000000..0ef92edf --- /dev/null +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs @@ -0,0 +1,35 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; + +namespace LaunchDarkly.Xamarin.MainThread +{ + public static partial class MainThread + { + static void PlatformBeginInvokeOnMainThread(Action action) => + throw new NotImplementedException(); + + static bool PlatformIsMainThread => + throw new NotImplementedException(); + } +} diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs new file mode 100644 index 00000000..3b72d83c --- /dev/null +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs @@ -0,0 +1,100 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin.MainThread +{ + public static partial class MainThread + { + public static bool IsMainThread => + PlatformIsMainThread; + + public static void BeginInvokeOnMainThread(Action action) + { + if (IsMainThread) + { + action(); + } + else + { + PlatformBeginInvokeOnMainThread(action); + } + } + + internal static Task InvokeOnMainThread(Action action) + { + if (IsMainThread) + { + action(); +#if NETSTANDARD1_0 + return Task.FromResult(true); +#else + return Task.CompletedTask; +#endif + } + + var tcs = new TaskCompletionSource(); + + BeginInvokeOnMainThread(() => + { + try + { + action(); + tcs.TrySetResult(true); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + + return tcs.Task; + } + + internal static Task InvokeOnMainThread(Func action) + { + if (IsMainThread) + { + return Task.FromResult(action()); + } + + var tcs = new TaskCompletionSource(); + + BeginInvokeOnMainThread(() => + { + try + { + var result = action(); + tcs.TrySetResult(result); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + + return tcs.Task; + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs new file mode 100644 index 00000000..5e5fe6c8 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs @@ -0,0 +1,201 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android; +using Android.Content.PM; +using Android.OS; +using Android.Support.V4.App; +using Android.Support.V4.Content; + +namespace LaunchDarkly.Xamarin.Permissions +{ + internal static partial class Permissions + { + static readonly object locker = new object(); + static int requestCode = 0; + + static Dictionary tcs)> requests = + new Dictionary)>(); + + static void PlatformEnsureDeclared(PermissionType permission) + { + var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: false); + + // No actual android permissions required here, just return + if (androidPermissions == null || !androidPermissions.Any()) + return; + + var context = Platform.Platform.AppContext; + + foreach (var ap in androidPermissions) + { + var packageInfo = context.PackageManager.GetPackageInfo(context.PackageName, PackageInfoFlags.Permissions); + var requestedPermissions = packageInfo?.RequestedPermissions; + + // If the manifest is missing any of the permissions we need, throw + if (!requestedPermissions?.Any(r => r.Equals(ap, StringComparison.OrdinalIgnoreCase)) ?? false) + throw new UnauthorizedAccessException($"You need to declare the permission: `{ap}` in your AndroidManifest.xml"); + } + } + + static Task PlatformCheckStatusAsync(PermissionType permission) + { + EnsureDeclared(permission); + + // If there are no android permissions for the given permission type + // just return granted since we have none to ask for + var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true); + + if (androidPermissions == null || !androidPermissions.Any()) + return Task.FromResult(PermissionStatus.Granted); + + var context = Platform.Platform.AppContext; + var targetsMOrHigher = context.ApplicationInfo.TargetSdkVersion >= BuildVersionCodes.M; + + foreach (var ap in androidPermissions) + { + if (targetsMOrHigher) + { + if (ContextCompat.CheckSelfPermission(context, ap) != Permission.Granted) + return Task.FromResult(PermissionStatus.Denied); + } + else + { + if (PermissionChecker.CheckSelfPermission(context, ap) != PermissionChecker.PermissionGranted) + return Task.FromResult(PermissionStatus.Denied); + } + } + + return Task.FromResult(PermissionStatus.Granted); + } + + static async Task PlatformRequestAsync(PermissionType permission) + { + // Check status before requesting first + if (await PlatformCheckStatusAsync(permission) == PermissionStatus.Granted) + return PermissionStatus.Granted; + + TaskCompletionSource tcs; + var doRequest = true; + + lock (locker) + { + if (requests.ContainsKey(permission)) + { + tcs = requests[permission].tcs; + doRequest = false; + } + else + { + tcs = new TaskCompletionSource(); + + // Get new request code and wrap it around for next use if it's going to reach max + if (++requestCode >= int.MaxValue) + requestCode = 1; + + requests.Add(permission, (requestCode, tcs)); + } + } + + if (!doRequest) + return await tcs.Task; + + if (!MainThread.MainThread.IsMainThread) + throw new System.UnauthorizedAccessException("Permission request must be invoked on main thread."); + + var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true).ToArray(); + + ActivityCompat.RequestPermissions(Platform.Platform.GetCurrentActivity(true), androidPermissions, requestCode); + + return await tcs.Task; + } + + internal static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + lock (locker) + { + // Check our pending requests for one with a matching request code + foreach (var kvp in requests) + { + if (kvp.Value.requestCode == requestCode) + { + var tcs = kvp.Value.tcs; + + // Look for any denied requests, and deny the whole request if so + // Remember, each PermissionType is tied to 1 or more android permissions + // so if any android permissions denied the whole PermissionType is considered denied + if (grantResults.Any(g => g == Permission.Denied)) + tcs.TrySetResult(PermissionStatus.Denied); + else + tcs.TrySetResult(PermissionStatus.Granted); + break; + } + } + } + } + } + + static class PermissionTypeExtensions + { + internal static IEnumerable ToAndroidPermissions(this PermissionType permissionType, bool onlyRuntimePermissions) + { + var permissions = new List<(string permission, bool runtimePermission)>(); + + switch (permissionType) + { + case PermissionType.Battery: + permissions.Add((Manifest.Permission.BatteryStats, false)); + break; + case PermissionType.Camera: + permissions.Add((Manifest.Permission.Camera, true)); + break; + case PermissionType.Flashlight: + permissions.Add((Manifest.Permission.Camera, true)); + permissions.Add((Manifest.Permission.Flashlight, false)); + break; + case PermissionType.LocationWhenInUse: + permissions.Add((Manifest.Permission.AccessFineLocation, true)); + permissions.Add((Manifest.Permission.AccessCoarseLocation, true)); + break; + case PermissionType.NetworkState: + permissions.Add((Manifest.Permission.AccessNetworkState, false)); + break; + case PermissionType.Vibrate: + permissions.Add((Manifest.Permission.Vibrate, true)); + break; + } + + if (onlyRuntimePermissions) + { + return permissions + .Where(p => p.runtimePermission) + .Select(p => p.permission); + } + + return permissions.Select(p => p.permission); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs new file mode 100644 index 00000000..1d2807b1 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs @@ -0,0 +1,124 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System.Threading.Tasks; +using System; +using CoreLocation; +using Foundation; + +namespace LaunchDarkly.Xamarin.Permissions +{ + internal static partial class Permissions + { + static void PlatformEnsureDeclared(PermissionType permission) + { + var info = NSBundle.MainBundle.InfoDictionary; + + if (permission == PermissionType.LocationWhenInUse) + { + if (!info.ContainsKey(new NSString("NSLocationWhenInUseUsageDescription"))) + throw new UnauthorizedAccessException("You must set `NSLocationWhenInUseUsageDescription` in your Info.plist file to enable Authorization Requests for Location updates."); + } + } + + static Task PlatformCheckStatusAsync(PermissionType permission) + { + EnsureDeclared(permission); + + switch (permission) + { + case PermissionType.LocationWhenInUse: + return Task.FromResult(GetLocationStatus()); + } + + return Task.FromResult(PermissionStatus.Granted); + } + + static async Task PlatformRequestAsync(PermissionType permission) + { + // Check status before requesting first and only request if Unknown + var status = await PlatformCheckStatusAsync(permission); + if (status != PermissionStatus.Unknown) + return status; + + EnsureDeclared(permission); + + switch (permission) + { + case PermissionType.LocationWhenInUse: + + if (!MainThread.MainThread.IsMainThread) + throw new UnauthorizedAccessException("Permission request must be invoked on main thread."); + + return await RequestLocationAsync(); + default: + return PermissionStatus.Granted; + } + } + + static PermissionStatus GetLocationStatus() + { + if (!CLLocationManager.LocationServicesEnabled) + return PermissionStatus.Disabled; + + var status = CLLocationManager.Status; + + switch (status) + { + case CLAuthorizationStatus.AuthorizedAlways: + case CLAuthorizationStatus.AuthorizedWhenInUse: + return PermissionStatus.Granted; + case CLAuthorizationStatus.Denied: + return PermissionStatus.Denied; + case CLAuthorizationStatus.Restricted: + return PermissionStatus.Restricted; + default: + return PermissionStatus.Unknown; + } + } + + static CLLocationManager locationManager; + + static Task RequestLocationAsync() + { + locationManager = new CLLocationManager(); + + var tcs = new TaskCompletionSource(locationManager); + + locationManager.AuthorizationChanged += LocationAuthCallback; + locationManager.RequestWhenInUseAuthorization(); + + return tcs.Task; + + void LocationAuthCallback(object sender, CLAuthorizationChangedEventArgs e) + { + if (e.Status == CLAuthorizationStatus.NotDetermined) + return; + + locationManager.AuthorizationChanged -= LocationAuthCallback; + tcs.TrySetResult(GetLocationStatus()); + locationManager.Dispose(); + locationManager = null; + } + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs new file mode 100644 index 00000000..2495b379 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs @@ -0,0 +1,39 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin.Permissions +{ + internal static partial class Permissions + { + static void PlatformEnsureDeclared(PermissionType permission) => + throw new NotImplementedException(); + + static Task PlatformCheckStatusAsync(PermissionType permission) => + throw new NotImplementedException(); + + static Task PlatformRequestAsync(PermissionType permission) => + throw new NotImplementedException(); + } +} diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs new file mode 100644 index 00000000..f7d28c98 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs @@ -0,0 +1,46 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin.Permissions +{ + internal static partial class Permissions + { + internal static void EnsureDeclared(PermissionType permission) => + PlatformEnsureDeclared(permission); + + internal static Task CheckStatusAsync(PermissionType permission) => + PlatformCheckStatusAsync(permission); + + internal static Task RequestAsync(PermissionType permission) => + PlatformRequestAsync(permission); + + internal static async Task RequireAsync(PermissionType permission) + { + if (await RequestAsync(permission) != PermissionStatus.Granted) + throw new System.UnauthorizedAccessException($"{permission} was not granted."); + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs new file mode 100644 index 00000000..5e4366b3 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs @@ -0,0 +1,53 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +namespace LaunchDarkly.Xamarin.Permissions +{ + enum PermissionStatus + { + // Denied by user + Denied, + + // Feature is disabled on device + Disabled, + + // Granted by user + Granted, + + // Restricted (only iOS) + Restricted, + + // Permission is in an unknown state + Unknown + } + + enum PermissionType + { + Unknown, + Battery, + Camera, + Flashlight, + LocationWhenInUse, + NetworkState, + Vibrate, + } +} diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs new file mode 100644 index 00000000..bf019408 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs @@ -0,0 +1,177 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Linq; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Hardware; +using Android.Hardware.Camera2; +using Android.Locations; +using Android.Net; +using Android.Net.Wifi; +using Android.OS; + +namespace LaunchDarkly.Xamarin.Platform +{ + public static partial class Platform + { + static ActivityLifecycleContextListener lifecycleListener; + + internal static Context AppContext => + Application.Context; + + internal static Activity GetCurrentActivity(bool throwOnNull) + { + var activity = lifecycleListener?.Activity; + if (throwOnNull && activity == null) + throw new NullReferenceException("The current Activity can not be detected. Ensure that you have called Init in your Activity or Application class."); + + return activity; + } + + public static void Init(Application application) + { + lifecycleListener = new ActivityLifecycleContextListener(); + application.RegisterActivityLifecycleCallbacks(lifecycleListener); + } + + public static void Init(Activity activity, Bundle bundle) + { + Init(activity.Application); + lifecycleListener.Activity = activity; + } + + public static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) => + Permissions.Permissions.OnRequestPermissionsResult(requestCode, permissions, grantResults); + + internal static bool HasSystemFeature(string systemFeature) + { + var packageManager = AppContext.PackageManager; + foreach (var feature in packageManager.GetSystemAvailableFeatures()) + { + if (feature.Name.Equals(systemFeature, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + internal static bool IsIntentSupported(Intent intent) + { + var manager = AppContext.PackageManager; + var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly); + return activities.Any(); + } + + internal static bool HasApiLevel(BuildVersionCodes versionCode) => + (int)Build.VERSION.SdkInt >= (int)versionCode; + + internal static CameraManager CameraManager => + AppContext.GetSystemService(Context.CameraService) as CameraManager; + + internal static ConnectivityManager ConnectivityManager => + AppContext.GetSystemService(Context.ConnectivityService) as ConnectivityManager; + + internal static Vibrator Vibrator => + AppContext.GetSystemService(Context.VibratorService) as Vibrator; + + internal static WifiManager WifiManager => + AppContext.GetSystemService(Context.WifiService) as WifiManager; + + internal static SensorManager SensorManager => + AppContext.GetSystemService(Context.SensorService) as SensorManager; + + internal static ClipboardManager ClipboardManager => + AppContext.GetSystemService(Context.ClipboardService) as ClipboardManager; + + internal static LocationManager LocationManager => + AppContext.GetSystemService(Context.LocationService) as LocationManager; + + internal static PowerManager PowerManager => + AppContext.GetSystemService(Context.PowerService) as PowerManager; + + internal static Java.Util.Locale GetLocale() + { + var resources = AppContext.Resources; + var config = resources.Configuration; + if (HasApiLevel(BuildVersionCodes.N)) + return config.Locales.Get(0); + + return config.Locale; + } + + internal static void SetLocale(Java.Util.Locale locale) + { + Java.Util.Locale.Default = locale; + var resources = AppContext.Resources; + var config = resources.Configuration; + if (HasApiLevel(BuildVersionCodes.N)) + config.SetLocale(locale); + else + config.Locale = locale; + +#pragma warning disable CS0618 // Type or member is obsolete + resources.UpdateConfiguration(config, resources.DisplayMetrics); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks + { + WeakReference currentActivity = new WeakReference(null); + + internal Context Context => + Activity ?? Application.Context; + + internal Activity Activity + { + get => currentActivity.TryGetTarget(out var a) ? a : null; + set => currentActivity.SetTarget(value); + } + + void Application.IActivityLifecycleCallbacks.OnActivityCreated(Activity activity, Bundle savedInstanceState) => + Activity = activity; + + void Application.IActivityLifecycleCallbacks.OnActivityDestroyed(Activity activity) + { + } + + void Application.IActivityLifecycleCallbacks.OnActivityPaused(Activity activity) => + Activity = activity; + + void Application.IActivityLifecycleCallbacks.OnActivityResumed(Activity activity) => + Activity = activity; + + void Application.IActivityLifecycleCallbacks.OnActivitySaveInstanceState(Activity activity, Bundle outState) + { + } + + void Application.IActivityLifecycleCallbacks.OnActivityStarted(Activity activity) + { + } + + void Application.IActivityLifecycleCallbacks.OnActivityStopped(Activity activity) + { + } + } +} diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs new file mode 100644 index 00000000..d337326d --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs @@ -0,0 +1,104 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Linq; +using System.Runtime.InteropServices; +using CoreMotion; +using Foundation; +using ObjCRuntime; +using UIKit; + +namespace LaunchDarkly.Xamarin.Platform +{ + public static partial class Platform + { + [DllImport(ObjCRuntime.Constants.SystemLibrary, EntryPoint = "sysctlbyname")] + internal static extern int SysctlByName([MarshalAs(UnmanagedType.LPStr)] string property, IntPtr output, IntPtr oldLen, IntPtr newp, uint newlen); + + internal static string GetSystemLibraryProperty(string property) + { + var lengthPtr = Marshal.AllocHGlobal(sizeof(int)); + SysctlByName(property, IntPtr.Zero, lengthPtr, IntPtr.Zero, 0); + + var propertyLength = Marshal.ReadInt32(lengthPtr); + + if (propertyLength == 0) + { + Marshal.FreeHGlobal(lengthPtr); + throw new InvalidOperationException("Unable to read length of property."); + } + + var valuePtr = Marshal.AllocHGlobal(propertyLength); + SysctlByName(property, valuePtr, lengthPtr, IntPtr.Zero, 0); + + var returnValue = Marshal.PtrToStringAnsi(valuePtr); + + Marshal.FreeHGlobal(lengthPtr); + Marshal.FreeHGlobal(valuePtr); + + return returnValue; + } + + internal static bool HasOSVersion(int major, int minor) => + UIDevice.CurrentDevice.CheckSystemVersion(major, minor); + + internal static UIViewController GetCurrentViewController(bool throwIfNull = true) + { + UIViewController viewController = null; + + var window = UIApplication.SharedApplication.KeyWindow; + + if (window.WindowLevel == UIWindowLevel.Normal) + viewController = window.RootViewController; + + if (viewController == null) + { + window = UIApplication.SharedApplication + .Windows + .OrderByDescending(w => w.WindowLevel) + .FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal); + + if (window == null) + throw new InvalidOperationException("Could not find current view controller."); + else + viewController = window.RootViewController; + } + + while (viewController.PresentedViewController != null) + viewController = viewController.PresentedViewController; + + if (throwIfNull && viewController == null) + throw new InvalidOperationException("Could not find current view controller."); + + return viewController; + } + + static CMMotionManager motionManager; + + internal static CMMotionManager MotionManager => + motionManager ?? (motionManager = new CMMotionManager()); + + internal static NSOperationQueue GetCurrentQueue() => + NSOperationQueue.CurrentQueue ?? new NSOperationQueue(); + } +} diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs new file mode 100644 index 00000000..0120f880 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs @@ -0,0 +1,30 @@ +/* +Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License + +The MIT License(MIT) +Copyright(c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +namespace LaunchDarkly.Xamarin.Platform +{ +#if !NETSTANDARD + public static partial class Platform + { + } +#endif +} diff --git a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs index fcb57d2a..25bb9808 100644 --- a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs +++ b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs @@ -2,3 +2,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests")] +[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.iOS.Tests")] +[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Android.Tests")] From 2b5faf0728003d0dd43fa98471cfc85e623f1849 Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 19 Mar 2019 15:36:20 -0700 Subject: [PATCH 059/254] feat(MobilePollingProcessor.shared.cs, LdClient.shared.cs, BackgroundAdapter.android.cs, Configuration.shared.cs): Android lifecycle callbacks and background polling work --- .../LDAndroidTests.cs | 24 ++++++++++++++----- .../LaunchDarkly.Xamarin.Android.Tests.csproj | 4 +++- LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs | 21 +++++++++++----- .../LaunchDarkly.Xamarin.iOS.Tests.csproj | 2 +- .../BackgroundAdapter.android.cs | 4 ++++ .../BackgroundAdapter.ios.cs | 5 ++++ .../Configuration.shared.cs | 14 +---------- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 14 +++++++++-- .../MobilePollingProcessor.shared.cs | 9 +++++-- 9 files changed, 66 insertions(+), 31 deletions(-) diff --git a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs index 0904b94b..07069142 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs +++ b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs @@ -6,18 +6,30 @@ namespace LaunchDarkly.Xamarin.Android.Tests [TestFixture] public class LDAndroidTests { - [SetUp] - public void Setup() { } + private ILdMobileClient client; + [SetUp] + public void Setup() + { + var user = LaunchDarkly.Client.User.WithKey("test-user"); + client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, TimeSpan.Zero); + } [TearDown] - public void Tear() { } + public void Tear() { LdClient.Instance = null; } + + [Test] + public void BooleanFeatureFlag() + { + Console.WriteLine("Test Boolean Variation"); + Assert.True(client.BoolVariation("boolean-feature-flag")); + } [Test] - public void Pass() + public void IntFeatureFlag() { - Console.WriteLine("test1"); - Assert.True(true); + Console.WriteLine("Test Integer Variation"); + Assert.True(client.IntVariation("int-feature-flag") == 2); } } } diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index 47653e52..f0c65929 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -41,7 +41,9 @@ - + + ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\monoandroid81\LaunchDarkly.Xamarin.dll + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs index c70de6b8..d02cc5f2 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs +++ b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs @@ -1,25 +1,34 @@ using System; using NUnit.Framework; -//using LaunchDarkly.Client; namespace LaunchDarkly.Xamarin.iOS.Tests { [TestFixture] public class LDiOSTests { + private ILdMobileClient client; + [SetUp] public void Setup() { - //LdClient client = LdClient.Init(config, user, TimeSpan.Zero); + var user = LaunchDarkly.Client.User.WithKey("test-user"); + client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, TimeSpan.Zero); } [TearDown] - public void Tear() { } + public void Tear() { LdClient.Instance = null;} + + [Test] + public void BooleanFeatureFlag() + { + Console.WriteLine("Test Boolean Variation"); + Assert.True(client.BoolVariation("boolean-feature-flag")); + } [Test] - public void Pass() + public void IntFeatureFlag() { - Console.WriteLine("test1"); - Assert.True(true); + Console.WriteLine("Test Integer Variation"); + Assert.True(client.IntVariation("int-feature-flag") == 1); } } } diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj index 5ad98579..8d99a7f8 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj @@ -82,7 +82,7 @@ - ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\Xamarin.iOS + ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\xamarin.ios10\LaunchDarkly.Xamarin.dll diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs index 586168c0..b2914758 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs @@ -2,6 +2,7 @@ using LaunchDarkly.Xamarin; using Android.App; using Android.OS; +using Android.Util; namespace LaunchDarkly.Xamarin.BackgroundAdapter { @@ -14,6 +15,7 @@ public void EnableBackgrounding(IBackgroundingState backgroundingState) { if (_callbacks == null) { + Log.Info("Xamarin", "Enable Backgrounding"); _callbacks = new ActivityLifecycleCallbacks(backgroundingState); application = (Application)Application.Context; application.RegisterActivityLifecycleCallbacks(_callbacks); @@ -65,11 +67,13 @@ public void OnActivityDestroyed(Activity activity) public void OnActivityPaused(Activity activity) { + Log.Info("Xamarin", "Entering Background"); _backgroundingState.EnterBackgroundAsync(); } public void OnActivityResumed(Activity activity) { + Log.Info("Xamarin", "Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs index 47190d6c..b34aadc4 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -1,15 +1,18 @@ using System; using LaunchDarkly.Xamarin; using UIKit; +using Common.Logging; namespace LaunchDarkly.Xamarin.BackgroundAdapter { public class BackgroundAdapter : UIApplicationDelegate, IPlatformAdapter { private IBackgroundingState _backgroundingState; + private static readonly ILog Log = LogManager.GetLogger(typeof(BackgroundAdapter)); public void EnableBackgrounding(IBackgroundingState backgroundingState) { + Log.Debug("Enable Backgrounding"); _backgroundingState = backgroundingState; } @@ -40,11 +43,13 @@ protected void _Dispose(bool disposing) public override void WillEnterForeground(UIApplication application) { + Log.Debug("Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } public override void DidEnterBackground(UIApplication application) { + Log.Debug("Entering Background"); _backgroundingState.EnterBackgroundAsync(); } } diff --git a/src/LaunchDarkly.Xamarin/Configuration.shared.cs b/src/LaunchDarkly.Xamarin/Configuration.shared.cs index 0435e21e..b2979d51 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.shared.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.shared.cs @@ -220,7 +220,7 @@ public static Configuration Default(string mobileKey) UserKeysCapacity = DefaultUserKeysCapacity, UserKeysFlushInterval = DefaultUserKeysFlushInterval, InlineUsersInEvents = false, - EnableBackgroundUpdating = false, + EnableBackgroundUpdating = true, UseReport = true }; @@ -593,18 +593,6 @@ internal static Configuration WithUpdateProcessor(this Configuration configurati return configuration; } - /// - /// Sets the Events URI. - /// - /// Configuration. - /// Events URI. - /// the same Configuration instance - public static Configuration WithEventsURI(this Configuration configuration, Uri eventsUri) - { - configuration.EventsUri = eventsUri; - return configuration; - } - /// /// Sets the ISimplePersistance instance, used internally for stubbing mock instances. /// diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 1af85dc4..32758a4a 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -71,6 +71,8 @@ public sealed class LdClient : ILdMobileClient configuration.PlatformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); + Log.Debug("After Platform Adapter"); + Config = configuration; connectionLock = new SemaphoreSlim(1, 1); @@ -577,10 +579,12 @@ internal async Task EnterBackgroundAsync() // if using Streaming, processor needs to be reset if (Config.IsStreamingEnabled) { + Console.WriteLine("StreamingEnabled"); ClearUpdateProcessor(); Config.IsStreamingEnabled = false; if (Config.EnableBackgroundUpdating) { + Console.WriteLine("BackgroundEnabled"); await RestartUpdateProcessorAsync(); } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); @@ -589,6 +593,7 @@ internal async Task EnterBackgroundAsync() { if (Config.EnableBackgroundUpdating) { + Console.WriteLine("BackgroundEnabled"); await PingPollingProcessorAsync(); } } @@ -626,7 +631,8 @@ void PingPollingProcessor() var pollingProcessor = updateProcessor as MobilePollingProcessor; if (pollingProcessor != null) { - var waitTask = pollingProcessor.PingAndWait(); + Console.WriteLine("PingPollingProcessor"); + var waitTask = pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); waitTask.Wait(); } } @@ -636,7 +642,8 @@ async Task PingPollingProcessorAsync() var pollingProcessor = updateProcessor as MobilePollingProcessor; if (pollingProcessor != null) { - await pollingProcessor.PingAndWait(); + Console.WriteLine("PingPollingProcessorAsync"); + await pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); } } @@ -662,16 +669,19 @@ internal LdClientBackgroundingState(LdClient client) public async Task EnterBackgroundAsync() { + Console.WriteLine("EnterBackgroundAsyncc"); await _client.EnterBackgroundAsync(); } public async Task ExitBackgroundAsync() { + Console.WriteLine("ExitBackgroundAsync"); await _client.EnterForegroundAsync(); } public async Task BackgroundUpdateAsync() { + Console.WriteLine("BackgroundUpdateAsync"); await _client.BackgroundTickAsync(); } } diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs index b36e341c..2a0b9e19 100644 --- a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs +++ b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs @@ -54,9 +54,14 @@ bool IMobileUpdateProcessor.Initialized() return _initialized == INITIALIZED; } - public async Task PingAndWait() + public async Task PingAndWait(TimeSpan backgroundPollingInterval) { - await UpdateTaskAsync(); + while (!_disposed) + { + Console.WriteLine("PingAndWait"); + await UpdateTaskAsync(); + await Task.Delay(backgroundPollingInterval); + } } private async Task UpdateTaskLoopAsync() From d468886d6df58504e1482466c02bdec90e793298 Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 19 Mar 2019 16:30:49 -0700 Subject: [PATCH 060/254] feat(BackgroundAdapter.ios.cs): iOS foreground and background polling works but some errors do appear in the log, further investigation required --- .../BackgroundAdapter/BackgroundAdapter.android.cs | 7 +++---- .../BackgroundAdapter/BackgroundAdapter.ios.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs index b2914758..4780e8dc 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs @@ -2,7 +2,6 @@ using LaunchDarkly.Xamarin; using Android.App; using Android.OS; -using Android.Util; namespace LaunchDarkly.Xamarin.BackgroundAdapter { @@ -15,7 +14,7 @@ public void EnableBackgrounding(IBackgroundingState backgroundingState) { if (_callbacks == null) { - Log.Info("Xamarin", "Enable Backgrounding"); + Console.WriteLine("Enable Backgrounding"); _callbacks = new ActivityLifecycleCallbacks(backgroundingState); application = (Application)Application.Context; application.RegisterActivityLifecycleCallbacks(_callbacks); @@ -67,13 +66,13 @@ public void OnActivityDestroyed(Activity activity) public void OnActivityPaused(Activity activity) { - Log.Info("Xamarin", "Entering Background"); + Console.WriteLine("Entering Background"); _backgroundingState.EnterBackgroundAsync(); } public void OnActivityResumed(Activity activity) { - Log.Info("Xamarin", "Entering Foreground"); + Console.WriteLine("Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs index b34aadc4..da07da28 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -2,17 +2,22 @@ using LaunchDarkly.Xamarin; using UIKit; using Common.Logging; +using Foundation; namespace LaunchDarkly.Xamarin.BackgroundAdapter { - public class BackgroundAdapter : UIApplicationDelegate, IPlatformAdapter + public class BackgroundAdapter : IPlatformAdapter { private IBackgroundingState _backgroundingState; + private NSObject _foregroundHandle; + private NSObject _backgroundHandle; private static readonly ILog Log = LogManager.GetLogger(typeof(BackgroundAdapter)); public void EnableBackgrounding(IBackgroundingState backgroundingState) { Log.Debug("Enable Backgrounding"); + _foregroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleWillEnterForeground); + _backgroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, HandleWillEnterBackground); _backgroundingState = backgroundingState; } @@ -29,6 +34,7 @@ protected void _Dispose(bool disposing) } _backgroundingState = null; + _foregroundHandle = null; disposedValue = true; } @@ -41,13 +47,13 @@ protected void _Dispose(bool disposing) } #endregion - public override void WillEnterForeground(UIApplication application) + private void HandleWillEnterForeground(NSNotification notification) { Log.Debug("Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } - public override void DidEnterBackground(UIApplication application) + private void HandleWillEnterBackground(NSNotification notification) { Log.Debug("Entering Background"); _backgroundingState.EnterBackgroundAsync(); From b26a8ce149b9df8bc742973bd4601b7655bcca9d Mon Sep 17 00:00:00 2001 From: torchhound Date: Tue, 19 Mar 2019 17:14:52 -0700 Subject: [PATCH 061/254] feat(src/): changed xamarin essentials code to internal --- .../BackgroundAdapter/BackgroundAdapter.android.cs | 2 +- .../BackgroundAdapter/BackgroundAdapter.ios.cs | 2 +- .../BackgroundAdapter/BackgroundAdapter.netstandard.cs | 2 +- src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs | 2 +- src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs | 2 +- .../Connectivity/Connectivity.ios.reachability.cs | 2 +- .../Connectivity/Connectivity.netstandard.cs | 2 +- src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs | 4 ++-- .../Connectivity/Connectivity.shared.enums.cs | 4 ++-- src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs | 2 +- src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs | 2 +- src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs | 2 +- src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs | 2 +- src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs | 2 +- .../Permissions/Permissions.shared.enums.cs | 4 ++-- src/LaunchDarkly.Xamarin/Platform/Platform.android.cs | 4 ++-- src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs | 2 +- src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs | 2 +- src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs | 2 +- src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs | 2 +- .../Preferences/Preferences.netstandard.cs | 2 +- src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs | 2 +- 22 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs index 4780e8dc..786690cf 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin.BackgroundAdapter { - public class BackgroundAdapter : IPlatformAdapter + internal class BackgroundAdapter : IPlatformAdapter { private static ActivityLifecycleCallbacks _callbacks; private Application application; diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs index da07da28..a4b15d37 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -6,7 +6,7 @@ namespace LaunchDarkly.Xamarin.BackgroundAdapter { - public class BackgroundAdapter : IPlatformAdapter + internal class BackgroundAdapter : IPlatformAdapter { private IBackgroundingState _backgroundingState; private NSObject _foregroundHandle; diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs index f5f66010..b7e2038b 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin.BackgroundAdapter { - public class BackgroundAdapter : IPlatformAdapter + internal class BackgroundAdapter : IPlatformAdapter { public void Dispose() { diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs index 94c6fd47..d91290db 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs @@ -30,7 +30,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Connectivity { - public partial class Connectivity + internal partial class Connectivity { static ConnectivityBroadcastReceiver conectivityReceiver; diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs index 6a53fad9..162f2d18 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs @@ -25,7 +25,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Connectivity { - public static partial class Connectivity + internal static partial class Connectivity { static ReachabilityListener listener; diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs index 4963da02..0c8f5018 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs @@ -36,7 +36,7 @@ enum NetworkStatus ReachableViaWiFiNetwork } - static class Reachability + internal static class Reachability { internal const string HostName = "www.microsoft.com"; diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs index 22c73f8f..738f7738 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs @@ -25,7 +25,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Connectivity { - public static partial class Connectivity + internal static partial class Connectivity { static NetworkAccess PlatformNetworkAccess => throw new NotImplementedException(); diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs index 00e4ec58..3282deeb 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs @@ -26,7 +26,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Connectivity { - public static partial class Connectivity + internal static partial class Connectivity { static event EventHandler ConnectivityChangedInternal; @@ -87,7 +87,7 @@ static void OnConnectivityChanged(ConnectivityChangedEventArgs e) } } - public class ConnectivityChangedEventArgs : EventArgs + internal class ConnectivityChangedEventArgs : EventArgs { public ConnectivityChangedEventArgs(NetworkAccess access, IEnumerable connectionProfiles) { diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs index e6a7c423..88e37766 100644 --- a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs +++ b/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs @@ -22,7 +22,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Connectivity { - public enum ConnectionProfile + internal enum ConnectionProfile { Unknown = 0, Bluetooth = 1, @@ -31,7 +31,7 @@ public enum ConnectionProfile WiFi = 4 } - public enum NetworkAccess + internal enum NetworkAccess { Unknown = 0, None = 1, diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs index 338cf637..1162805d 100644 --- a/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs @@ -25,7 +25,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.MainThread { - public static partial class MainThread + internal static partial class MainThread { static Handler handler; diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs index 9ac0fb94..3ae74c81 100644 --- a/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs @@ -25,7 +25,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.MainThread { - public static partial class MainThread + internal static partial class MainThread { static bool PlatformIsMainThread => NSThread.Current.IsMainThread; diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs index 0ef92edf..1a80cc56 100644 --- a/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs @@ -24,7 +24,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.MainThread { - public static partial class MainThread + internal static partial class MainThread { static void PlatformBeginInvokeOnMainThread(Action action) => throw new NotImplementedException(); diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs b/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs index 3b72d83c..021164e2 100644 --- a/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs +++ b/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs @@ -25,7 +25,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.MainThread { - public static partial class MainThread + internal static partial class MainThread { public static bool IsMainThread => PlatformIsMainThread; diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs index 5e5fe6c8..17499e08 100644 --- a/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs @@ -158,7 +158,7 @@ internal static void OnRequestPermissionsResult(int requestCode, string[] permis } } - static class PermissionTypeExtensions + internal static class PermissionTypeExtensions { internal static IEnumerable ToAndroidPermissions(this PermissionType permissionType, bool onlyRuntimePermissions) { diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs index 5e4366b3..ce24091a 100644 --- a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs +++ b/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs @@ -22,7 +22,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Permissions { - enum PermissionStatus + internal enum PermissionStatus { // Denied by user Denied, @@ -40,7 +40,7 @@ enum PermissionStatus Unknown } - enum PermissionType + internal enum PermissionType { Unknown, Battery, diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs index bf019408..435768ac 100644 --- a/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs @@ -34,7 +34,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Platform { - public static partial class Platform + internal static partial class Platform { static ActivityLifecycleContextListener lifecycleListener; @@ -136,7 +136,7 @@ internal static void SetLocale(Java.Util.Locale locale) } } - class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks + internal class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks { WeakReference currentActivity = new WeakReference(null); diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs index d337326d..b416338f 100644 --- a/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs @@ -30,7 +30,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Platform { - public static partial class Platform + internal static partial class Platform { [DllImport(ObjCRuntime.Constants.SystemLibrary, EntryPoint = "sysctlbyname")] internal static extern int SysctlByName([MarshalAs(UnmanagedType.LPStr)] string property, IntPtr output, IntPtr oldLen, IntPtr newp, uint newlen); diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs b/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs index 0120f880..59395a07 100644 --- a/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs +++ b/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Platform { #if !NETSTANDARD - public static partial class Platform + internal static partial class Platform { } #endif diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs index b3616733..381029fb 100644 --- a/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs @@ -28,7 +28,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { - public static partial class Preferences + internal static partial class Preferences { static readonly object locker = new object(); diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs index ad508f29..9497cd09 100644 --- a/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs @@ -26,7 +26,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { - public static partial class Preferences + internal static partial class Preferences { static readonly object locker = new object(); diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs index 02694894..6aa16790 100644 --- a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs @@ -24,7 +24,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { - public static partial class Preferences + internal static partial class Preferences { static bool PlatformContainsKey(string key, string sharedName) => throw new NotImplementedException(); diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs b/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs index 6786e940..62997c12 100644 --- a/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs +++ b/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs @@ -24,7 +24,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { - public static partial class Preferences + internal static partial class Preferences { internal static string GetPrivatePreferencesSharedName(string feature) => $"LaunchDarkly.Xamarin.{feature}"; From 158e71004084c8407846c9964ea2832f95919619 Mon Sep 17 00:00:00 2001 From: torchhound Date: Thu, 21 Mar 2019 11:08:00 -0700 Subject: [PATCH 062/254] fix(LDAndroidTests.cs, LDiOSTests.cs, AndroidManifest.xml, packages.config): platform specific unit tests build and pass --- .../LDAndroidTests.cs | 3 +- .../LaunchDarkly.Xamarin.Android.Tests.csproj | 116 +++ .../Properties/AndroidManifest.xml | 1 + .../Resources/Resource.designer.cs | 803 +++++++++++++++++- .../packages.config | 29 + LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs | 5 +- .../BackgroundAdapter.ios.cs | 2 +- 7 files changed, 909 insertions(+), 50 deletions(-) diff --git a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs index 07069142..502cabd8 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs +++ b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs @@ -12,7 +12,8 @@ public class LDAndroidTests public void Setup() { var user = LaunchDarkly.Client.User.WithKey("test-user"); - client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, TimeSpan.Zero); + var timeSpan = TimeSpan.FromSeconds(10); + client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, timeSpan); } [TearDown] diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index f0c65929..4f15dec9 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -74,6 +74,93 @@ ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\monoandroid71\Plugin.DeviceInfo.dll + + ..\packages\Xamarin.Android.Support.Annotations.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Annotations.dll + + + ..\packages\Xamarin.Android.Arch.Core.Common.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Core.Common.dll + + + ..\packages\Xamarin.Android.Arch.Core.Runtime.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Core.Runtime.dll + + + ..\packages\Xamarin.Android.Arch.Lifecycle.Common.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.Common.dll + + + ..\packages\Xamarin.Android.Arch.Lifecycle.LiveData.Core.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.LiveData.Core.dll + + + ..\packages\Xamarin.Android.Arch.Lifecycle.LiveData.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.LiveData.dll + + + ..\packages\Xamarin.Android.Arch.Lifecycle.Runtime.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.Runtime.dll + + + ..\packages\Xamarin.Android.Arch.Lifecycle.ViewModel.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.ViewModel.dll + + + ..\packages\Xamarin.Android.Support.Collections.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Collections.dll + + + ..\packages\Xamarin.Android.Support.CursorAdapter.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CursorAdapter.dll + + + ..\packages\Xamarin.Android.Support.DocumentFile.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.DocumentFile.dll + + + ..\packages\Xamarin.Android.Support.Interpolator.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Interpolator.dll + + + ..\packages\Xamarin.Android.Support.LocalBroadcastManager.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.LocalBroadcastManager.dll + + + ..\packages\Xamarin.Android.Support.Print.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Print.dll + + + ..\packages\Xamarin.Android.Support.VersionedParcelable.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.VersionedParcelable.dll + + + ..\packages\Xamarin.Android.Support.Compat.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Compat.dll + + + ..\packages\Xamarin.Android.Support.AsyncLayoutInflater.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.AsyncLayoutInflater.dll + + + ..\packages\Xamarin.Android.Support.CustomView.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CustomView.dll + + + ..\packages\Xamarin.Android.Support.CoordinaterLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CoordinaterLayout.dll + + + ..\packages\Xamarin.Android.Support.DrawerLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.DrawerLayout.dll + + + ..\packages\Xamarin.Android.Support.Loader.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Loader.dll + + + ..\packages\Xamarin.Android.Support.Core.Utils.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Core.Utils.dll + + + ..\packages\Xamarin.Android.Support.Media.Compat.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Media.Compat.dll + + + ..\packages\Xamarin.Android.Support.SlidingPaneLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.SlidingPaneLayout.dll + + + ..\packages\Xamarin.Android.Support.SwipeRefreshLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.SwipeRefreshLayout.dll + + + ..\packages\Xamarin.Android.Support.ViewPager.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.ViewPager.dll + + + ..\packages\Xamarin.Android.Support.Core.UI.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Core.UI.dll + + + ..\packages\Xamarin.Android.Support.Fragment.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Fragment.dll + + + ..\packages\Xamarin.Android.Support.v4.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.v4.dll + @@ -97,4 +184,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml b/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml index 5aedade7..1456678d 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml +++ b/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs b/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs index b7d502eb..8144be89 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs +++ b/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs @@ -53,6 +53,69 @@ public static void UpdateIdValues() public partial class Attribute { + // aapt resource value: 0x7f010009 + public const int alpha = 2130771977; + + // aapt resource value: 0x7f010000 + public const int coordinatorLayoutStyle = 2130771968; + + // aapt resource value: 0x7f010011 + public const int font = 2130771985; + + // aapt resource value: 0x7f01000a + public const int fontProviderAuthority = 2130771978; + + // aapt resource value: 0x7f01000d + public const int fontProviderCerts = 2130771981; + + // aapt resource value: 0x7f01000e + public const int fontProviderFetchStrategy = 2130771982; + + // aapt resource value: 0x7f01000f + public const int fontProviderFetchTimeout = 2130771983; + + // aapt resource value: 0x7f01000b + public const int fontProviderPackage = 2130771979; + + // aapt resource value: 0x7f01000c + public const int fontProviderQuery = 2130771980; + + // aapt resource value: 0x7f010010 + public const int fontStyle = 2130771984; + + // aapt resource value: 0x7f010013 + public const int fontVariationSettings = 2130771987; + + // aapt resource value: 0x7f010012 + public const int fontWeight = 2130771986; + + // aapt resource value: 0x7f010001 + public const int keylines = 2130771969; + + // aapt resource value: 0x7f010004 + public const int layout_anchor = 2130771972; + + // aapt resource value: 0x7f010006 + public const int layout_anchorGravity = 2130771974; + + // aapt resource value: 0x7f010003 + public const int layout_behavior = 2130771971; + + // aapt resource value: 0x7f010008 + public const int layout_dodgeInsetEdges = 2130771976; + + // aapt resource value: 0x7f010007 + public const int layout_insetEdge = 2130771975; + + // aapt resource value: 0x7f010005 + public const int layout_keyline = 2130771973; + + // aapt resource value: 0x7f010002 + public const int statusBarBackground = 2130771970; + + // aapt resource value: 0x7f010014 + public const int ttcIndex = 2130771988; + static Attribute() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); @@ -63,62 +126,383 @@ private Attribute() } } + public partial class Color + { + + // aapt resource value: 0x7f060003 + public const int notification_action_color_filter = 2131099651; + + // aapt resource value: 0x7f060004 + public const int notification_icon_bg_color = 2131099652; + + // aapt resource value: 0x7f060000 + public const int notification_material_background_media_default_color = 2131099648; + + // aapt resource value: 0x7f060001 + public const int primary_text_default_material_dark = 2131099649; + + // aapt resource value: 0x7f060005 + public const int ripple_material_light = 2131099653; + + // aapt resource value: 0x7f060002 + public const int secondary_text_default_material_dark = 2131099650; + + // aapt resource value: 0x7f060006 + public const int secondary_text_default_material_light = 2131099654; + + static Color() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Color() + { + } + } + + public partial class Dimension + { + + // aapt resource value: 0x7f070008 + public const int compat_button_inset_horizontal_material = 2131165192; + + // aapt resource value: 0x7f070009 + public const int compat_button_inset_vertical_material = 2131165193; + + // aapt resource value: 0x7f07000a + public const int compat_button_padding_horizontal_material = 2131165194; + + // aapt resource value: 0x7f07000b + public const int compat_button_padding_vertical_material = 2131165195; + + // aapt resource value: 0x7f07000c + public const int compat_control_corner_material = 2131165196; + + // aapt resource value: 0x7f07000d + public const int compat_notification_large_icon_max_height = 2131165197; + + // aapt resource value: 0x7f07000e + public const int compat_notification_large_icon_max_width = 2131165198; + + // aapt resource value: 0x7f07000f + public const int notification_action_icon_size = 2131165199; + + // aapt resource value: 0x7f070010 + public const int notification_action_text_size = 2131165200; + + // aapt resource value: 0x7f070011 + public const int notification_big_circle_margin = 2131165201; + + // aapt resource value: 0x7f070005 + public const int notification_content_margin_start = 2131165189; + + // aapt resource value: 0x7f070012 + public const int notification_large_icon_height = 2131165202; + + // aapt resource value: 0x7f070013 + public const int notification_large_icon_width = 2131165203; + + // aapt resource value: 0x7f070006 + public const int notification_main_column_padding_top = 2131165190; + + // aapt resource value: 0x7f070007 + public const int notification_media_narrow_margin = 2131165191; + + // aapt resource value: 0x7f070014 + public const int notification_right_icon_size = 2131165204; + + // aapt resource value: 0x7f070004 + public const int notification_right_side_padding_top = 2131165188; + + // aapt resource value: 0x7f070015 + public const int notification_small_icon_background_padding = 2131165205; + + // aapt resource value: 0x7f070016 + public const int notification_small_icon_size_as_large = 2131165206; + + // aapt resource value: 0x7f070017 + public const int notification_subtext_size = 2131165207; + + // aapt resource value: 0x7f070018 + public const int notification_top_pad = 2131165208; + + // aapt resource value: 0x7f070019 + public const int notification_top_pad_large_text = 2131165209; + + // aapt resource value: 0x7f070000 + public const int subtitle_corner_radius = 2131165184; + + // aapt resource value: 0x7f070001 + public const int subtitle_outline_width = 2131165185; + + // aapt resource value: 0x7f070002 + public const int subtitle_shadow_offset = 2131165186; + + // aapt resource value: 0x7f070003 + public const int subtitle_shadow_radius = 2131165187; + + static Dimension() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Dimension() + { + } + } + + public partial class Drawable + { + + // aapt resource value: 0x7f020000 + public const int notification_action_background = 2130837504; + + // aapt resource value: 0x7f020001 + public const int notification_bg = 2130837505; + + // aapt resource value: 0x7f020002 + public const int notification_bg_low = 2130837506; + + // aapt resource value: 0x7f020003 + public const int notification_bg_low_normal = 2130837507; + + // aapt resource value: 0x7f020004 + public const int notification_bg_low_pressed = 2130837508; + + // aapt resource value: 0x7f020005 + public const int notification_bg_normal = 2130837509; + + // aapt resource value: 0x7f020006 + public const int notification_bg_normal_pressed = 2130837510; + + // aapt resource value: 0x7f020007 + public const int notification_icon_background = 2130837511; + + // aapt resource value: 0x7f02000a + public const int notification_template_icon_bg = 2130837514; + + // aapt resource value: 0x7f02000b + public const int notification_template_icon_low_bg = 2130837515; + + // aapt resource value: 0x7f020008 + public const int notification_tile_bg = 2130837512; + + // aapt resource value: 0x7f020009 + public const int notify_panel_notification_icon_bg = 2130837513; + + static Drawable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Drawable() + { + } + } + public partial class Id { - // aapt resource value: 0x7f040001 - public const int OptionHostName = 2130968577; + // aapt resource value: 0x7f0a0032 + public const int OptionHostName = 2131361842; - // aapt resource value: 0x7f040002 - public const int OptionPort = 2130968578; + // aapt resource value: 0x7f0a0033 + public const int OptionPort = 2131361843; - // aapt resource value: 0x7f040000 - public const int OptionRemoteServer = 2130968576; + // aapt resource value: 0x7f0a0031 + public const int OptionRemoteServer = 2131361841; - // aapt resource value: 0x7f040010 - public const int OptionsButton = 2130968592; + // aapt resource value: 0x7f0a0041 + public const int OptionsButton = 2131361857; - // aapt resource value: 0x7f04000b - public const int ResultFullName = 2130968587; + // aapt resource value: 0x7f0a003c + public const int ResultFullName = 2131361852; - // aapt resource value: 0x7f04000d - public const int ResultMessage = 2130968589; + // aapt resource value: 0x7f0a003e + public const int ResultMessage = 2131361854; - // aapt resource value: 0x7f04000c - public const int ResultResultState = 2130968588; + // aapt resource value: 0x7f0a003d + public const int ResultResultState = 2131361853; - // aapt resource value: 0x7f04000a - public const int ResultRunSingleMethodTest = 2130968586; + // aapt resource value: 0x7f0a003b + public const int ResultRunSingleMethodTest = 2131361851; - // aapt resource value: 0x7f04000e - public const int ResultStackTrace = 2130968590; + // aapt resource value: 0x7f0a003f + public const int ResultStackTrace = 2131361855; - // aapt resource value: 0x7f040006 - public const int ResultsFailed = 2130968582; + // aapt resource value: 0x7f0a0037 + public const int ResultsFailed = 2131361847; - // aapt resource value: 0x7f040003 - public const int ResultsId = 2130968579; + // aapt resource value: 0x7f0a0034 + public const int ResultsId = 2131361844; - // aapt resource value: 0x7f040007 - public const int ResultsIgnored = 2130968583; + // aapt resource value: 0x7f0a0038 + public const int ResultsIgnored = 2131361848; - // aapt resource value: 0x7f040008 - public const int ResultsInconclusive = 2130968584; + // aapt resource value: 0x7f0a0039 + public const int ResultsInconclusive = 2131361849; - // aapt resource value: 0x7f040009 - public const int ResultsMessage = 2130968585; + // aapt resource value: 0x7f0a003a + public const int ResultsMessage = 2131361850; - // aapt resource value: 0x7f040005 - public const int ResultsPassed = 2130968581; + // aapt resource value: 0x7f0a0036 + public const int ResultsPassed = 2131361846; - // aapt resource value: 0x7f040004 - public const int ResultsResult = 2130968580; + // aapt resource value: 0x7f0a0035 + public const int ResultsResult = 2131361845; - // aapt resource value: 0x7f04000f - public const int RunTestsButton = 2130968591; + // aapt resource value: 0x7f0a0040 + public const int RunTestsButton = 2131361856; - // aapt resource value: 0x7f040011 - public const int TestSuiteListView = 2130968593; + // aapt resource value: 0x7f0a0042 + public const int TestSuiteListView = 2131361858; + + // aapt resource value: 0x7f0a0020 + public const int action0 = 2131361824; + + // aapt resource value: 0x7f0a001d + public const int action_container = 2131361821; + + // aapt resource value: 0x7f0a0024 + public const int action_divider = 2131361828; + + // aapt resource value: 0x7f0a001e + public const int action_image = 2131361822; + + // aapt resource value: 0x7f0a001f + public const int action_text = 2131361823; + + // aapt resource value: 0x7f0a002e + public const int actions = 2131361838; + + // aapt resource value: 0x7f0a0017 + public const int all = 2131361815; + + // aapt resource value: 0x7f0a0018 + public const int async = 2131361816; + + // aapt resource value: 0x7f0a0019 + public const int blocking = 2131361817; + + // aapt resource value: 0x7f0a0008 + public const int bottom = 2131361800; + + // aapt resource value: 0x7f0a0021 + public const int cancel_action = 2131361825; + + // aapt resource value: 0x7f0a0009 + public const int center = 2131361801; + + // aapt resource value: 0x7f0a000a + public const int center_horizontal = 2131361802; + + // aapt resource value: 0x7f0a000b + public const int center_vertical = 2131361803; + + // aapt resource value: 0x7f0a0029 + public const int chronometer = 2131361833; + + // aapt resource value: 0x7f0a000c + public const int clip_horizontal = 2131361804; + + // aapt resource value: 0x7f0a000d + public const int clip_vertical = 2131361805; + + // aapt resource value: 0x7f0a000e + public const int end = 2131361806; + + // aapt resource value: 0x7f0a0030 + public const int end_padder = 2131361840; + + // aapt resource value: 0x7f0a000f + public const int fill = 2131361807; + + // aapt resource value: 0x7f0a0010 + public const int fill_horizontal = 2131361808; + + // aapt resource value: 0x7f0a0011 + public const int fill_vertical = 2131361809; + + // aapt resource value: 0x7f0a001a + public const int forever = 2131361818; + + // aapt resource value: 0x7f0a002b + public const int icon = 2131361835; + + // aapt resource value: 0x7f0a002f + public const int icon_group = 2131361839; + + // aapt resource value: 0x7f0a002a + public const int info = 2131361834; + + // aapt resource value: 0x7f0a001b + public const int italic = 2131361819; + + // aapt resource value: 0x7f0a0012 + public const int left = 2131361810; + + // aapt resource value: 0x7f0a0000 + public const int line1 = 2131361792; + + // aapt resource value: 0x7f0a0001 + public const int line3 = 2131361793; + + // aapt resource value: 0x7f0a0023 + public const int media_actions = 2131361827; + + // aapt resource value: 0x7f0a0016 + public const int none = 2131361814; + + // aapt resource value: 0x7f0a001c + public const int normal = 2131361820; + + // aapt resource value: 0x7f0a002d + public const int notification_background = 2131361837; + + // aapt resource value: 0x7f0a0026 + public const int notification_main_column = 2131361830; + + // aapt resource value: 0x7f0a0025 + public const int notification_main_column_container = 2131361829; + + // aapt resource value: 0x7f0a0013 + public const int right = 2131361811; + + // aapt resource value: 0x7f0a002c + public const int right_icon = 2131361836; + + // aapt resource value: 0x7f0a0027 + public const int right_side = 2131361831; + + // aapt resource value: 0x7f0a0014 + public const int start = 2131361812; + + // aapt resource value: 0x7f0a0022 + public const int status_bar_latest_event_content = 2131361826; + + // aapt resource value: 0x7f0a0002 + public const int tag_transition_group = 2131361794; + + // aapt resource value: 0x7f0a0003 + public const int tag_unhandled_key_event_manager = 2131361795; + + // aapt resource value: 0x7f0a0004 + public const int tag_unhandled_key_listeners = 2131361796; + + // aapt resource value: 0x7f0a0005 + public const int text = 2131361797; + + // aapt resource value: 0x7f0a0006 + public const int text2 = 2131361798; + + // aapt resource value: 0x7f0a0028 + public const int time = 2131361832; + + // aapt resource value: 0x7f0a0007 + public const int title = 2131361799; + + // aapt resource value: 0x7f0a0015 + public const int top = 2131361813; static Id() { @@ -130,20 +514,84 @@ private Id() } } + public partial class Integer + { + + // aapt resource value: 0x7f080000 + public const int cancel_button_image_alpha = 2131230720; + + // aapt resource value: 0x7f080001 + public const int status_bar_notification_info_maxnum = 2131230721; + + static Integer() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Integer() + { + } + } + public partial class Layout { - // aapt resource value: 0x7f030000 - public const int options = 2130903040; + // aapt resource value: 0x7f040000 + public const int notification_action = 2130968576; + + // aapt resource value: 0x7f040001 + public const int notification_action_tombstone = 2130968577; + + // aapt resource value: 0x7f040002 + public const int notification_media_action = 2130968578; + + // aapt resource value: 0x7f040003 + public const int notification_media_cancel_action = 2130968579; + + // aapt resource value: 0x7f040004 + public const int notification_template_big_media = 2130968580; + + // aapt resource value: 0x7f040005 + public const int notification_template_big_media_custom = 2130968581; + + // aapt resource value: 0x7f040006 + public const int notification_template_big_media_narrow = 2130968582; + + // aapt resource value: 0x7f040007 + public const int notification_template_big_media_narrow_custom = 2130968583; + + // aapt resource value: 0x7f040008 + public const int notification_template_custom_big = 2130968584; + + // aapt resource value: 0x7f040009 + public const int notification_template_icon_group = 2130968585; - // aapt resource value: 0x7f030001 - public const int results = 2130903041; + // aapt resource value: 0x7f04000a + public const int notification_template_lines_media = 2130968586; + + // aapt resource value: 0x7f04000b + public const int notification_template_media = 2130968587; + + // aapt resource value: 0x7f04000c + public const int notification_template_media_custom = 2130968588; - // aapt resource value: 0x7f030002 - public const int test_result = 2130903042; + // aapt resource value: 0x7f04000d + public const int notification_template_part_chronometer = 2130968589; + + // aapt resource value: 0x7f04000e + public const int notification_template_part_time = 2130968590; + + // aapt resource value: 0x7f04000f + public const int options = 2130968591; + + // aapt resource value: 0x7f040010 + public const int results = 2130968592; + + // aapt resource value: 0x7f040011 + public const int test_result = 2130968593; - // aapt resource value: 0x7f030003 - public const int test_suite = 2130903043; + // aapt resource value: 0x7f040012 + public const int test_suite = 2130968594; static Layout() { @@ -158,8 +606,8 @@ private Layout() public partial class Mipmap { - // aapt resource value: 0x7f020000 - public const int Icon = 2130837504; + // aapt resource value: 0x7f030000 + public const int Icon = 2130903040; static Mipmap() { @@ -170,6 +618,269 @@ private Mipmap() { } } + + public partial class String + { + + // aapt resource value: 0x7f090000 + public const int status_bar_notification_info_overflow = 2131296256; + + static String() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private String() + { + } + } + + public partial class Style + { + + // aapt resource value: 0x7f050006 + public const int TextAppearance_Compat_Notification = 2131034118; + + // aapt resource value: 0x7f050007 + public const int TextAppearance_Compat_Notification_Info = 2131034119; + + // aapt resource value: 0x7f050000 + public const int TextAppearance_Compat_Notification_Info_Media = 2131034112; + + // aapt resource value: 0x7f05000c + public const int TextAppearance_Compat_Notification_Line2 = 2131034124; + + // aapt resource value: 0x7f050004 + public const int TextAppearance_Compat_Notification_Line2_Media = 2131034116; + + // aapt resource value: 0x7f050001 + public const int TextAppearance_Compat_Notification_Media = 2131034113; + + // aapt resource value: 0x7f050008 + public const int TextAppearance_Compat_Notification_Time = 2131034120; + + // aapt resource value: 0x7f050002 + public const int TextAppearance_Compat_Notification_Time_Media = 2131034114; + + // aapt resource value: 0x7f050009 + public const int TextAppearance_Compat_Notification_Title = 2131034121; + + // aapt resource value: 0x7f050003 + public const int TextAppearance_Compat_Notification_Title_Media = 2131034115; + + // aapt resource value: 0x7f05000a + public const int Widget_Compat_NotificationActionContainer = 2131034122; + + // aapt resource value: 0x7f05000b + public const int Widget_Compat_NotificationActionText = 2131034123; + + // aapt resource value: 0x7f050005 + public const int Widget_Support_CoordinatorLayout = 2131034117; + + static Style() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Style() + { + } + } + + public partial class Styleable + { + + public static int[] ColorStateListItem = new int[] { + 16843173, + 16843551, + 2130771977}; + + // aapt resource value: 2 + public const int ColorStateListItem_alpha = 2; + + // aapt resource value: 1 + public const int ColorStateListItem_android_alpha = 1; + + // aapt resource value: 0 + public const int ColorStateListItem_android_color = 0; + + public static int[] CoordinatorLayout = new int[] { + 2130771969, + 2130771970}; + + // aapt resource value: 0 + public const int CoordinatorLayout_keylines = 0; + + // aapt resource value: 1 + public const int CoordinatorLayout_statusBarBackground = 1; + + public static int[] CoordinatorLayout_Layout = new int[] { + 16842931, + 2130771971, + 2130771972, + 2130771973, + 2130771974, + 2130771975, + 2130771976}; + + // aapt resource value: 0 + public const int CoordinatorLayout_Layout_android_layout_gravity = 0; + + // aapt resource value: 2 + public const int CoordinatorLayout_Layout_layout_anchor = 2; + + // aapt resource value: 4 + public const int CoordinatorLayout_Layout_layout_anchorGravity = 4; + + // aapt resource value: 1 + public const int CoordinatorLayout_Layout_layout_behavior = 1; + + // aapt resource value: 6 + public const int CoordinatorLayout_Layout_layout_dodgeInsetEdges = 6; + + // aapt resource value: 5 + public const int CoordinatorLayout_Layout_layout_insetEdge = 5; + + // aapt resource value: 3 + public const int CoordinatorLayout_Layout_layout_keyline = 3; + + public static int[] FontFamily = new int[] { + 2130771978, + 2130771979, + 2130771980, + 2130771981, + 2130771982, + 2130771983}; + + // aapt resource value: 0 + public const int FontFamily_fontProviderAuthority = 0; + + // aapt resource value: 3 + public const int FontFamily_fontProviderCerts = 3; + + // aapt resource value: 4 + public const int FontFamily_fontProviderFetchStrategy = 4; + + // aapt resource value: 5 + public const int FontFamily_fontProviderFetchTimeout = 5; + + // aapt resource value: 1 + public const int FontFamily_fontProviderPackage = 1; + + // aapt resource value: 2 + public const int FontFamily_fontProviderQuery = 2; + + public static int[] FontFamilyFont = new int[] { + 16844082, + 16844083, + 16844095, + 16844143, + 16844144, + 2130771984, + 2130771985, + 2130771986, + 2130771987, + 2130771988}; + + // aapt resource value: 0 + public const int FontFamilyFont_android_font = 0; + + // aapt resource value: 2 + public const int FontFamilyFont_android_fontStyle = 2; + + // aapt resource value: 4 + public const int FontFamilyFont_android_fontVariationSettings = 4; + + // aapt resource value: 1 + public const int FontFamilyFont_android_fontWeight = 1; + + // aapt resource value: 3 + public const int FontFamilyFont_android_ttcIndex = 3; + + // aapt resource value: 6 + public const int FontFamilyFont_font = 6; + + // aapt resource value: 5 + public const int FontFamilyFont_fontStyle = 5; + + // aapt resource value: 8 + public const int FontFamilyFont_fontVariationSettings = 8; + + // aapt resource value: 7 + public const int FontFamilyFont_fontWeight = 7; + + // aapt resource value: 9 + public const int FontFamilyFont_ttcIndex = 9; + + public static int[] GradientColor = new int[] { + 16843165, + 16843166, + 16843169, + 16843170, + 16843171, + 16843172, + 16843265, + 16843275, + 16844048, + 16844049, + 16844050, + 16844051}; + + // aapt resource value: 7 + public const int GradientColor_android_centerColor = 7; + + // aapt resource value: 3 + public const int GradientColor_android_centerX = 3; + + // aapt resource value: 4 + public const int GradientColor_android_centerY = 4; + + // aapt resource value: 1 + public const int GradientColor_android_endColor = 1; + + // aapt resource value: 10 + public const int GradientColor_android_endX = 10; + + // aapt resource value: 11 + public const int GradientColor_android_endY = 11; + + // aapt resource value: 5 + public const int GradientColor_android_gradientRadius = 5; + + // aapt resource value: 0 + public const int GradientColor_android_startColor = 0; + + // aapt resource value: 8 + public const int GradientColor_android_startX = 8; + + // aapt resource value: 9 + public const int GradientColor_android_startY = 9; + + // aapt resource value: 6 + public const int GradientColor_android_tileMode = 6; + + // aapt resource value: 2 + public const int GradientColor_android_type = 2; + + public static int[] GradientColorItem = new int[] { + 16843173, + 16844052}; + + // aapt resource value: 0 + public const int GradientColorItem_android_color = 0; + + // aapt resource value: 1 + public const int GradientColorItem_android_offset = 1; + + static Styleable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Styleable() + { + } + } } } #pragma warning restore 1591 diff --git a/LaunchDarkly.Xamarin.Android.Tests/packages.config b/LaunchDarkly.Xamarin.Android.Tests/packages.config index f7f73da9..0da6d27d 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/packages.config +++ b/LaunchDarkly.Xamarin.Android.Tests/packages.config @@ -54,4 +54,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs index d02cc5f2..1f31ee3b 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs +++ b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs @@ -11,7 +11,8 @@ public class LDiOSTests [SetUp] public void Setup() { var user = LaunchDarkly.Client.User.WithKey("test-user"); - client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, TimeSpan.Zero); + var timeSpan = TimeSpan.FromSeconds(10); + client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, timeSpan); } [TearDown] @@ -28,7 +29,7 @@ public void BooleanFeatureFlag() public void IntFeatureFlag() { Console.WriteLine("Test Integer Variation"); - Assert.True(client.IntVariation("int-feature-flag") == 1); + Assert.True(client.IntVariation("int-feature-flag") == 2); } } } diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs index a4b15d37..7f0d9822 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -40,7 +40,7 @@ protected void _Dispose(bool disposing) } } - public new void Dispose() + public void Dispose() { _Dispose(true); GC.SuppressFinalize(this); From e9722151a8adebfb24fd6ad59f3ef2fdd6e84630 Mon Sep 17 00:00:00 2001 From: torchhound Date: Thu, 21 Mar 2019 17:12:53 -0700 Subject: [PATCH 063/254] feat(src/, LDAndroiTests.cs, LDiOSTests.cs): added additional platform specific unit tests, removed console.writelines, added background polling to default config, fixed polling processor start when streamin is enabled --- .../LDAndroidTests.cs | 28 ++++++++++++++++++- LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs | 28 ++++++++++++++++++- .../BackgroundAdapter.android.cs | 3 -- .../BackgroundAdapter.ios.cs | 3 -- .../Configuration.shared.cs | 1 + src/LaunchDarkly.Xamarin/LdClient.shared.cs | 11 +------- .../MobilePollingProcessor.shared.cs | 1 - 7 files changed, 56 insertions(+), 19 deletions(-) diff --git a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs index 502cabd8..931b7c19 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs +++ b/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin.Android.Tests { @@ -13,7 +14,7 @@ public void Setup() { var user = LaunchDarkly.Client.User.WithKey("test-user"); var timeSpan = TimeSpan.FromSeconds(10); - client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, timeSpan); + client = LdClient.Init("MOBILE_KEY", user, timeSpan); } [TearDown] @@ -32,5 +33,30 @@ public void IntFeatureFlag() Console.WriteLine("Test Integer Variation"); Assert.True(client.IntVariation("int-feature-flag") == 2); } + + [Test] + public void StringFeatureFlag() + { + Console.WriteLine("Test String Variation"); + Assert.True(client.StringVariation("string-feature-flag", "false").Equals("bravo")); + } + + [Test] + public void JsonFeatureFlag() + { + string json = @"{ + ""test2"": ""testing2"" + }"; + Console.WriteLine("Test JSON Variation"); + JToken jsonToken = JToken.FromObject(JObject.Parse(json)); + Assert.True(JToken.DeepEquals(jsonToken, client.JsonVariation("json-feature-flag", "false"))); + } + + [Test] + public void FloatFeatureFlag() + { + Console.WriteLine("Test Float Variation"); + Assert.True(client.FloatVariation("float-feature-flag") == 1.5); + } } } diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs index 1f31ee3b..8deef83b 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs +++ b/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin.iOS.Tests { @@ -12,7 +13,7 @@ public class LDiOSTests public void Setup() { var user = LaunchDarkly.Client.User.WithKey("test-user"); var timeSpan = TimeSpan.FromSeconds(10); - client = LdClient.Init("mob-368413a0-28e1-495d-ab32-7aa389ac33b6", user, timeSpan); + client = LdClient.Init("MOBILE_KEY", user, timeSpan); } [TearDown] @@ -31,5 +32,30 @@ public void IntFeatureFlag() Console.WriteLine("Test Integer Variation"); Assert.True(client.IntVariation("int-feature-flag") == 2); } + + [Test] + public void StringFeatureFlag() + { + Console.WriteLine("Test String Variation"); + Assert.True(client.StringVariation("string-feature-flag", "false").Equals("bravo")); + } + + [Test] + public void JsonFeatureFlag() + { + string json = @"{ + ""test2"": ""testing2"" + }"; + Console.WriteLine("Test JSON Variation"); + JToken jsonToken = JToken.FromObject(JObject.Parse(json)); + Assert.True(JToken.DeepEquals(jsonToken, client.JsonVariation("json-feature-flag", "false"))); + } + + [Test] + public void FloatFeatureFlag() + { + Console.WriteLine("Test Float Variation"); + Assert.True(client.FloatVariation("float-feature-flag") == 1.5); + } } } diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs index 786690cf..ab7853f0 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs @@ -14,7 +14,6 @@ public void EnableBackgrounding(IBackgroundingState backgroundingState) { if (_callbacks == null) { - Console.WriteLine("Enable Backgrounding"); _callbacks = new ActivityLifecycleCallbacks(backgroundingState); application = (Application)Application.Context; application.RegisterActivityLifecycleCallbacks(_callbacks); @@ -66,13 +65,11 @@ public void OnActivityDestroyed(Activity activity) public void OnActivityPaused(Activity activity) { - Console.WriteLine("Entering Background"); _backgroundingState.EnterBackgroundAsync(); } public void OnActivityResumed(Activity activity) { - Console.WriteLine("Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs index 7f0d9822..457ae6f7 100644 --- a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs +++ b/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs @@ -15,7 +15,6 @@ internal class BackgroundAdapter : IPlatformAdapter public void EnableBackgrounding(IBackgroundingState backgroundingState) { - Log.Debug("Enable Backgrounding"); _foregroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleWillEnterForeground); _backgroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, HandleWillEnterBackground); _backgroundingState = backgroundingState; @@ -49,13 +48,11 @@ public void Dispose() private void HandleWillEnterForeground(NSNotification notification) { - Log.Debug("Entering Foreground"); _backgroundingState.ExitBackgroundAsync(); } private void HandleWillEnterBackground(NSNotification notification) { - Log.Debug("Entering Background"); _backgroundingState.EnterBackgroundAsync(); } } diff --git a/src/LaunchDarkly.Xamarin/Configuration.shared.cs b/src/LaunchDarkly.Xamarin/Configuration.shared.cs index b2979d51..c2f965e4 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.shared.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.shared.cs @@ -208,6 +208,7 @@ public static Configuration Default(string mobileKey) EventQueueCapacity = DefaultEventQueueCapacity, EventQueueFrequency = DefaultEventQueueFrequency, PollingInterval = DefaultPollingInterval, + BackgroundPollingInterval = DefaultBackgroundPollingInterval, ReadTimeout = DefaultReadTimeout, ReconnectTime = DefaultReconnectTime, HttpClientTimeout = DefaultHttpClientTimeout, diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 32758a4a..98ab47d8 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -71,8 +71,6 @@ public sealed class LdClient : ILdMobileClient configuration.PlatformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); - Log.Debug("After Platform Adapter"); - Config = configuration; connectionLock = new SemaphoreSlim(1, 1); @@ -579,13 +577,12 @@ internal async Task EnterBackgroundAsync() // if using Streaming, processor needs to be reset if (Config.IsStreamingEnabled) { - Console.WriteLine("StreamingEnabled"); ClearUpdateProcessor(); Config.IsStreamingEnabled = false; if (Config.EnableBackgroundUpdating) { - Console.WriteLine("BackgroundEnabled"); await RestartUpdateProcessorAsync(); + await PingPollingProcessorAsync(); } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } @@ -593,7 +590,6 @@ internal async Task EnterBackgroundAsync() { if (Config.EnableBackgroundUpdating) { - Console.WriteLine("BackgroundEnabled"); await PingPollingProcessorAsync(); } } @@ -631,7 +627,6 @@ void PingPollingProcessor() var pollingProcessor = updateProcessor as MobilePollingProcessor; if (pollingProcessor != null) { - Console.WriteLine("PingPollingProcessor"); var waitTask = pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); waitTask.Wait(); } @@ -642,7 +637,6 @@ async Task PingPollingProcessorAsync() var pollingProcessor = updateProcessor as MobilePollingProcessor; if (pollingProcessor != null) { - Console.WriteLine("PingPollingProcessorAsync"); await pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); } } @@ -669,19 +663,16 @@ internal LdClientBackgroundingState(LdClient client) public async Task EnterBackgroundAsync() { - Console.WriteLine("EnterBackgroundAsyncc"); await _client.EnterBackgroundAsync(); } public async Task ExitBackgroundAsync() { - Console.WriteLine("ExitBackgroundAsync"); await _client.EnterForegroundAsync(); } public async Task BackgroundUpdateAsync() { - Console.WriteLine("BackgroundUpdateAsync"); await _client.BackgroundTickAsync(); } } diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs index 2a0b9e19..4b11ef1d 100644 --- a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs +++ b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs @@ -58,7 +58,6 @@ public async Task PingAndWait(TimeSpan backgroundPollingInterval) { while (!_disposed) { - Console.WriteLine("PingAndWait"); await UpdateTaskAsync(); await Task.Delay(backgroundPollingInterval); } From 137aa4123b1469f75793a886e50c5135e125ada7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 21 Mar 2019 18:32:44 -0700 Subject: [PATCH 064/254] fix merge --- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 80 +++++++++---------- .../{ValueType.cs => ValueType.shared.cs} | 72 ++++++++--------- 2 files changed, 76 insertions(+), 76 deletions(-) rename src/LaunchDarkly.Xamarin/{ValueType.cs => ValueType.shared.cs} (97%) diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 6399de4f..a38f064b 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -233,13 +233,13 @@ static void CreateInstance(Configuration configuration, User user) { bgPollInterval = configuration.BackgroundPollingInterval; } - try - { - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); - } - catch - { - Log.Info("Foreground/Background is only available on iOS and Android"); + try + { + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); + } + catch + { + Log.Info("Foreground/Background is only available on iOS and Android"); } } @@ -314,61 +314,61 @@ void MobileConnectionManager_ConnectionChanged(bool isOnline) /// public bool BoolVariation(string key, bool defaultValue = false) { - return VariationInternal(key, defaultValue, ValueType.Bool, eventFactoryDefault).Value; - } - + return VariationInternal(key, defaultValue, ValueTypes.Bool, eventFactoryDefault).Value; + } + /// public EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false) { - return VariationInternal(key, defaultValue, ValueType.Bool, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Bool, eventFactoryWithReasons); } /// public string StringVariation(string key, string defaultValue) { - return VariationInternal(key, defaultValue, ValueType.String, eventFactoryDefault).Value; - } - + return VariationInternal(key, defaultValue, ValueTypes.String, eventFactoryDefault).Value; + } + /// public EvaluationDetail StringVariationDetail(string key, string defaultValue) { - return VariationInternal(key, defaultValue, ValueType.String, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.String, eventFactoryWithReasons); } /// public float FloatVariation(string key, float defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueType.Float, eventFactoryDefault).Value; - } - + return VariationInternal(key, defaultValue, ValueTypes.Float, eventFactoryDefault).Value; + } + /// public EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueType.Float, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Float, eventFactoryWithReasons); } /// public int IntVariation(string key, int defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueType.Int, eventFactoryDefault).Value; - } - + return VariationInternal(key, defaultValue, ValueTypes.Int, eventFactoryDefault).Value; + } + /// public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueType.Int, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Int, eventFactoryWithReasons); } /// public JToken JsonVariation(string key, JToken defaultValue) { - return VariationInternal(key, defaultValue, ValueType.Json, eventFactoryDefault).Value; - } - + return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryDefault).Value; + } + /// public EvaluationDetail JsonVariationDetail(string key, JToken defaultValue) { - return VariationInternal(key, defaultValue, ValueType.Json, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryWithReasons); } EvaluationDetail VariationInternal(string featureKey, T defaultValue, ValueType desiredType, EventFactory eventFactory) @@ -383,15 +383,15 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); - } - + } + var flag = flagCacheManager.FlagForUser(featureKey, User); - if (flag == null) - { + if (flag == null) + { Log.InfoFormat("Unknown feature flag {0}; returning default value", featureKey); eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, - EvaluationErrorKind.FLAG_NOT_FOUND)); - return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); + EvaluationErrorKind.FLAG_NOT_FOUND)); + return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); } featureFlagEvent = new FeatureFlagEvent(featureKey, flag); @@ -556,13 +556,13 @@ void Dispose(bool disposing) if (disposing) { Log.InfoFormat("Shutting down the LaunchDarkly client"); - try - { - platformAdapter.Dispose(); - } - catch - { - Log.Info("Foreground/Background is only available on iOS and Android"); + try + { + platformAdapter.Dispose(); + } + catch + { + Log.Info("Foreground/Background is only available on iOS and Android"); } updateProcessor.Dispose(); eventProcessor.Dispose(); diff --git a/src/LaunchDarkly.Xamarin/ValueType.cs b/src/LaunchDarkly.Xamarin/ValueType.shared.cs similarity index 97% rename from src/LaunchDarkly.Xamarin/ValueType.cs rename to src/LaunchDarkly.Xamarin/ValueType.shared.cs index 3874f81e..eef0c6ed 100644 --- a/src/LaunchDarkly.Xamarin/ValueType.cs +++ b/src/LaunchDarkly.Xamarin/ValueType.shared.cs @@ -9,65 +9,65 @@ internal class ValueType public Func ValueToJson { get; internal set; } } - internal class ValueType + internal class ValueTypes { - private static ArgumentException BadTypeException() - { - return new ArgumentException("unexpected data type"); + private static ArgumentException BadTypeException() + { + return new ArgumentException("unexpected data type"); } public static ValueType Bool = new ValueType { - ValueFromJson = json => - { - if (json.Type != JTokenType.Boolean) - { - throw BadTypeException(); - } - return json.Value(); + ValueFromJson = json => + { + if (json.Type != JTokenType.Boolean) + { + throw BadTypeException(); + } + return json.Value(); }, ValueToJson = value => new JValue(value) }; public static ValueType Int = new ValueType { - ValueFromJson = json => - { - if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) - { - throw BadTypeException(); - } - return json.Value(); + ValueFromJson = json => + { + if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) + { + throw BadTypeException(); + } + return json.Value(); }, ValueToJson = value => new JValue(value) }; public static ValueType Float = new ValueType { - ValueFromJson = json => - { - if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) - { - throw BadTypeException(); - } - return json.Value(); + ValueFromJson = json => + { + if (json.Type != JTokenType.Integer && json.Type != JTokenType.Float) + { + throw BadTypeException(); + } + return json.Value(); }, ValueToJson = value => new JValue(value) }; public static ValueType String = new ValueType { - ValueFromJson = json => - { - if (json == null || json.Type == JTokenType.Null) - { - return null; // strings are always nullable - } - if (json.Type != JTokenType.String) - { - throw BadTypeException(); - } - return json.Value(); + ValueFromJson = json => + { + if (json == null || json.Type == JTokenType.Null) + { + return null; // strings are always nullable + } + if (json.Type != JTokenType.String) + { + throw BadTypeException(); + } + return json.Value(); }, ValueToJson = value => value == null ? null : new JValue(value) }; From 7099e7696ddd0251b1f51522ab38459082ec68a6 Mon Sep 17 00:00:00 2001 From: torchhound Date: Mon, 25 Mar 2019 10:38:59 -0700 Subject: [PATCH 065/254] fix(LdClient.shared.cs, Factory.shared.cs, Configuration.shared.cs): Handle platform adapter dispose exception, remove unnecessary android asset, remove platform adapter as a config option --- .../Assets/AboutAssets.txt | 19 ------------------- .../Configuration.shared.cs | 13 ------------- src/LaunchDarkly.Xamarin/Factory.shared.cs | 5 ----- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 8 +++----- 4 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt diff --git a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt deleted file mode 100644 index a9b0638e..00000000 --- a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with your package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/src/LaunchDarkly.Xamarin/Configuration.shared.cs b/src/LaunchDarkly.Xamarin/Configuration.shared.cs index c2f965e4..d5fc70c3 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.shared.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.shared.cs @@ -125,7 +125,6 @@ public class Configuration : IMobileConfiguration internal ISimplePersistance Persister { get; set; } internal IDeviceInfo DeviceInfo { get; set; } internal IFeatureFlagListenerManager FeatureFlagListenerManager { get; set; } - internal IPlatformAdapter PlatformAdapter { get; set; } /// /// Default value for . @@ -658,17 +657,5 @@ public static Configuration WithBackgroundPollingInterval(this Configuration con configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } - - /// - /// Specifies a component that provides special functionality for the current mobile platform. - /// - /// Configuration. - /// An implementation of . - /// the same Configuration instance - public static Configuration WithPlatformAdapter(this Configuration configuration, IPlatformAdapter adapter) - { - configuration.PlatformAdapter = adapter; - return configuration; - } } } diff --git a/src/LaunchDarkly.Xamarin/Factory.shared.cs b/src/LaunchDarkly.Xamarin/Factory.shared.cs index fa082f67..70e56e9a 100644 --- a/src/LaunchDarkly.Xamarin/Factory.shared.cs +++ b/src/LaunchDarkly.Xamarin/Factory.shared.cs @@ -92,10 +92,5 @@ internal static IFeatureFlagListenerManager CreateFeatureFlagListenerManager(Con { return configuration.FeatureFlagListenerManager ?? new FeatureFlagListenerManager(); } - - internal static IPlatformAdapter CreatePlatformAdapter(Configuration configuration) - { - return configuration.PlatformAdapter ?? new NullPlatformAdapter(); - } } } diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 98ab47d8..0b316921 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -69,8 +69,6 @@ public sealed class LdClient : ILdMobileClient throw new ArgumentNullException("user"); } - configuration.PlatformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); - Config = configuration; connectionLock = new SemaphoreSlim(1, 1); @@ -78,7 +76,7 @@ public sealed class LdClient : ILdMobileClient persister = Factory.CreatePersister(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - platformAdapter = Factory.CreatePlatformAdapter(configuration); + platformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); // If you pass in a user with a null or blank key, one will be assigned to them. if (String.IsNullOrEmpty(user.Key)) @@ -542,9 +540,9 @@ void Dispose(bool disposing) { platformAdapter.Dispose(); } - catch + catch(Exception error) { - Log.Info("Foreground/Background is only available on iOS and Android"); + Log.Error(error); } updateProcessor.Dispose(); eventProcessor.Dispose(); From 330a711f62bc290dabe9a0460faf477d53bd025b Mon Sep 17 00:00:00 2001 From: torchhound Date: Mon, 25 Mar 2019 14:37:42 -0700 Subject: [PATCH 066/254] feat(Factory.shared.cs, IBackgroundingState.shared.cs, LdClient.shared.cs, MobilePollingProcessor.shared.cs): removed updatebackgroundasync from backgrounding state, readded android asset to fix build, added polling interval based on foreground or background and streaming disabled, removed PingAndWait --- .../Assets/AboutAssets.txt | 20 ++++++ src/LaunchDarkly.Xamarin/Factory.shared.cs | 8 ++- .../IBackgroundingState.shared.cs | 5 -- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 70 ++++--------------- .../MobilePollingProcessor.shared.cs | 9 --- 5 files changed, 37 insertions(+), 75 deletions(-) create mode 100644 LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt diff --git a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt new file mode 100644 index 00000000..ef2ad785 --- /dev/null +++ b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt @@ -0,0 +1,20 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + + These files will be deployed with your package and will be accessible using Android's +AssetManager, like this: + + public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + + Additionally, some Android functions will automatically load asset files: + + Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); + diff --git a/src/LaunchDarkly.Xamarin/Factory.shared.cs b/src/LaunchDarkly.Xamarin/Factory.shared.cs index 70e56e9a..3dac0518 100644 --- a/src/LaunchDarkly.Xamarin/Factory.shared.cs +++ b/src/LaunchDarkly.Xamarin/Factory.shared.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System; +using System.Net.Http; using Common.Logging; using LaunchDarkly.Client; using LaunchDarkly.Common; @@ -33,7 +34,8 @@ internal static IConnectionManager CreateConnectionManager(Configuration configu internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, User user, - IFlagCacheManager flagCacheManager, + IFlagCacheManager flagCacheManager, + TimeSpan pollingInterval, StreamManager.EventSourceCreator source = null) { if (configuration.MobileUpdateProcessor != null) @@ -59,7 +61,7 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return new MobilePollingProcessor(featureFlagRequestor, flagCacheManager, user, - configuration.PollingInterval); + pollingInterval); } } diff --git a/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs b/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs index 019c2fbd..88708ddb 100644 --- a/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs +++ b/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs @@ -24,10 +24,5 @@ public interface IBackgroundingState /// resume the regular streaming or polling process. /// Task ExitBackgroundAsync(); - - /// - /// Tells the LaunchDarkly client to initiate a request for feature flag updates while in background mode. - /// - Task BackgroundUpdateAsync(); } } diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 59437688..1717d7c9 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -91,7 +91,7 @@ public sealed class LdClient : ILdMobileClient flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); + updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, configuration.PollingInterval); eventProcessor = Factory.CreateEventProcessor(configuration); eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); @@ -502,7 +502,7 @@ public async Task IdentifyAsync(User user) try { User = userWithKey; - await RestartUpdateProcessorAsync(); + await RestartUpdateProcessorAsync(Config.PollingInterval); } finally { @@ -512,16 +512,16 @@ public async Task IdentifyAsync(User user) eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(userWithKey)); } - async Task RestartUpdateProcessorAsync() + async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) { - ClearAndSetUpdateProcessor(); + ClearAndSetUpdateProcessor(pollingInterval); await StartUpdateProcessorAsync(); } - void ClearAndSetUpdateProcessor() + void ClearAndSetUpdateProcessor(TimeSpan pollingInterval) { ClearUpdateProcessor(); - updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager); + updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager, pollingInterval); } void ClearUpdateProcessor() @@ -591,31 +591,19 @@ public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener l internal async Task EnterBackgroundAsync() { - // if using Streaming, processor needs to be reset - if (Config.IsStreamingEnabled) + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + if (Config.EnableBackgroundUpdating) { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - if (Config.EnableBackgroundUpdating) - { - await RestartUpdateProcessorAsync(); - await PingPollingProcessorAsync(); - } - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); - } - else - { - if (Config.EnableBackgroundUpdating) - { - await PingPollingProcessorAsync(); - } + await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); } + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } internal async Task EnterForegroundAsync() { ResetProcessorForForeground(); - await RestartUpdateProcessorAsync(); + await RestartUpdateProcessorAsync(Config.PollingInterval); } void ResetProcessorForForeground() @@ -629,35 +617,6 @@ void ResetProcessorForForeground() } } - internal void BackgroundTick() - { - PingPollingProcessor(); - } - - internal async Task BackgroundTickAsync() - { - await PingPollingProcessorAsync(); - } - - void PingPollingProcessor() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - var waitTask = pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); - waitTask.Wait(); - } - } - - async Task PingPollingProcessorAsync() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - await pollingProcessor.PingAndWait(Config.BackgroundPollingInterval); - } - } - private Exception UnwrapAggregateException(AggregateException e) { if (e.InnerExceptions.Count == 1) @@ -687,10 +646,5 @@ public async Task ExitBackgroundAsync() { await _client.EnterForegroundAsync(); } - - public async Task BackgroundUpdateAsync() - { - await _client.BackgroundTickAsync(); - } } } \ No newline at end of file diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs index 4b11ef1d..9119e367 100644 --- a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs +++ b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs @@ -54,15 +54,6 @@ bool IMobileUpdateProcessor.Initialized() return _initialized == INITIALIZED; } - public async Task PingAndWait(TimeSpan backgroundPollingInterval) - { - while (!_disposed) - { - await UpdateTaskAsync(); - await Task.Delay(backgroundPollingInterval); - } - } - private async Task UpdateTaskLoopAsync() { while (!_disposed) From de6b1b708e305122b19a002602b90acd18276cd5 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 25 Mar 2019 14:44:10 -0700 Subject: [PATCH 067/254] fix(LdClient.shared.cs): forgot an argument to RestartUpdateProcessorAsync --- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 1717d7c9..9244059c 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -289,7 +289,7 @@ public async Task SetOnlineAsync(bool value) { if (online) { - await RestartUpdateProcessorAsync(); + await RestartUpdateProcessorAsync(Config.PollingInterval); } else { From f8a38396420e9c35c1a8cd7d4873f7c0e48d0c35 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 25 Mar 2019 14:46:59 -0700 Subject: [PATCH 068/254] fix(MobileStreamingProcessorTests.cs): added additional timespan argument to factory method --- .../LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index 7969e83c..c61fc10e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -41,7 +41,7 @@ public MobileStreamingProcessorTests() private IMobileUpdateProcessor MobileStreamingProcessorStarted() { - var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); + var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, TimeSpan.FromMinutes(5), eventSourceFactory.Create()); processor.Start(); return processor; } From d9796f49eec45365b14ead8b2e064fb9da151e9c Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 25 Mar 2019 14:53:17 -0700 Subject: [PATCH 069/254] fix(LaunchDarkly.Xamarin.Android.Tests.csproj): removed unnecessary AboutAssets.txt --- .../Assets/AboutAssets.txt | 20 ------------------- .../LaunchDarkly.Xamarin.Android.Tests.csproj | 3 --- 2 files changed, 23 deletions(-) delete mode 100644 LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt diff --git a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt b/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt deleted file mode 100644 index ef2ad785..00000000 --- a/LaunchDarkly.Xamarin.Android.Tests/Assets/AboutAssets.txt +++ /dev/null @@ -1,20 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - - These files will be deployed with your package and will be accessible using Android's -AssetManager, like this: - - public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - - Additionally, some Android functions will automatically load asset files: - - Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); - diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index 4f15dec9..e33858d0 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -180,9 +180,6 @@ - - - From ffccf37d31942908e85c38557c28eba11e3c1369 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 25 Mar 2019 15:50:11 -0700 Subject: [PATCH 070/254] ensure that LdClient instances are always cleaned up in tests --- .../LdClientEvaluationTests.cs | 187 ++++++++++-------- .../LdClientEventTests.cs | 12 +- .../LdClientTests.cs | 30 ++- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 9 + 4 files changed, 144 insertions(+), 94 deletions(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs index 84bc3bb0..5c47ce32 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs @@ -12,8 +12,18 @@ public class LdClientEvaluationTests { static readonly string appKey = "some app key"; static readonly string nonexistentFlagKey = "some flag key"; - static readonly User user = User.WithKey("userkey"); - + static readonly User user = User.WithKey("userkey"); + + public LdClientEvaluationTests() + { + TestUtil.ClearClient(); + } + + ~LdClientEvaluationTests() + { + TestUtil.ClearClient(); + } + private static LdClient ClientWithFlagsJson(string flagsJson) { var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); @@ -24,16 +34,19 @@ private static LdClient ClientWithFlagsJson(string flagsJson) public void BoolVariationReturnsValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(true)); - var client = ClientWithFlagsJson(flagsJson); - - Assert.True(client.BoolVariation("flag-key", false)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.True(client.BoolVariation("flag-key", false)); + } } [Fact] public void BoolVariationReturnsDefaultForUnknownFlag() { - var client = ClientWithFlagsJson("{}"); - Assert.False(client.BoolVariation(nonexistentFlagKey)); + using (var client = ClientWithFlagsJson("{}")) + { + Assert.False(client.BoolVariation(nonexistentFlagKey)); + } } [Fact] @@ -41,35 +54,40 @@ public void BoolVariationDetailReturnsValue() { var reason = EvaluationReason.Off.Instance; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(true), 1, reason); - var client = ClientWithFlagsJson(flagsJson); - - var expected = new EvaluationDetail(true, 1, reason); - Assert.Equal(expected, client.BoolVariationDetail("flag-key", false)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail(true, 1, reason); + Assert.Equal(expected, client.BoolVariationDetail("flag-key", false)); + } } [Fact] public void IntVariationReturnsValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3)); - var client = ClientWithFlagsJson(flagsJson); - - Assert.Equal(3, client.IntVariation("flag-key", 0)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(3, client.IntVariation("flag-key", 0)); + } } [Fact] public void IntVariationCoercesFloatValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3.0f)); - var client = ClientWithFlagsJson(flagsJson); - - Assert.Equal(3, client.IntVariation("flag-key", 0)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(3, client.IntVariation("flag-key", 0)); + } } [Fact] public void IntVariationReturnsDefaultForUnknownFlag() { - var client = ClientWithFlagsJson("{}"); - Assert.Equal(1, client.IntVariation(nonexistentFlagKey, 1)); + using (var client = ClientWithFlagsJson("{}")) + { + Assert.Equal(1, client.IntVariation(nonexistentFlagKey, 1)); + } } [Fact] @@ -77,35 +95,40 @@ public void IntVariationDetailReturnsValue() { var reason = EvaluationReason.Off.Instance; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3), 1, reason); - var client = ClientWithFlagsJson(flagsJson); - - var expected = new EvaluationDetail(3, 1, reason); - Assert.Equal(expected, client.IntVariationDetail("flag-key", 0)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail(3, 1, reason); + Assert.Equal(expected, client.IntVariationDetail("flag-key", 0)); + } } [Fact] public void FloatVariationReturnsValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2.5f)); - var client = ClientWithFlagsJson(flagsJson); - - Assert.Equal(2.5f, client.FloatVariation("flag-key", 0)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(2.5f, client.FloatVariation("flag-key", 0)); + } } [Fact] public void FloatVariationCoercesIntValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2)); - var client = ClientWithFlagsJson(flagsJson); - - Assert.Equal(2.0f, client.FloatVariation("flag-key", 0)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(2.0f, client.FloatVariation("flag-key", 0)); + } } [Fact] public void FloatVariationReturnsDefaultForUnknownFlag() { - var client = ClientWithFlagsJson("{}"); - Assert.Equal(0.5f, client.FloatVariation(nonexistentFlagKey, 0.5f)); + using (var client = ClientWithFlagsJson("{}")) + { + Assert.Equal(0.5f, client.FloatVariation(nonexistentFlagKey, 0.5f)); + } } [Fact] @@ -113,26 +136,30 @@ public void FloatVariationDetailReturnsValue() { var reason = EvaluationReason.Off.Instance; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2.5f), 1, reason); - var client = ClientWithFlagsJson(flagsJson); - - var expected = new EvaluationDetail(2.5f, 1, reason); - Assert.Equal(expected, client.FloatVariationDetail("flag-key", 0.5f)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail(2.5f, 1, reason); + Assert.Equal(expected, client.FloatVariationDetail("flag-key", 0.5f)); + } } [Fact] public void StringVariationReturnsValue() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); - var client = ClientWithFlagsJson(flagsJson); - - Assert.Equal("string value", client.StringVariation("flag-key", "")); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal("string value", client.StringVariation("flag-key", "")); + } } [Fact] public void StringVariationReturnsDefaultForUnknownFlag() { - var client = ClientWithFlagsJson("{}"); - Assert.Equal("d", client.StringVariation(nonexistentFlagKey, "d")); + using (var client = ClientWithFlagsJson("{}")) + { + Assert.Equal("d", client.StringVariation(nonexistentFlagKey, "d")); + } } [Fact] @@ -140,10 +167,11 @@ public void StringVariationDetailReturnsValue() { var reason = EvaluationReason.Off.Instance; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value"), 1, reason); - var client = ClientWithFlagsJson(flagsJson); - - var expected = new EvaluationDetail("string value", 1, reason); - Assert.Equal(expected, client.StringVariationDetail("flag-key", "")); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail("string value", 1, reason); + Assert.Equal(expected, client.StringVariationDetail("flag-key", "")); + } } [Fact] @@ -151,17 +179,20 @@ public void JsonVariationReturnsValue() { var jsonValue = new JObject { { "thing", new JValue("stuff") } }; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue); - var client = ClientWithFlagsJson(flagsJson); - - var defaultValue = new JValue(3); - Assert.Equal(jsonValue, client.JsonVariation("flag-key", defaultValue)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var defaultValue = new JValue(3); + Assert.Equal(jsonValue, client.JsonVariation("flag-key", defaultValue)); + } } [Fact] public void JsonVariationReturnsDefaultForUnknownFlag() { - var client = ClientWithFlagsJson("{}"); - Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); + using (var client = ClientWithFlagsJson("{}")) + { + Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); + } } [Fact] @@ -170,57 +201,59 @@ public void JsonVariationDetailReturnsValue() var jsonValue = new JObject { { "thing", new JValue("stuff") } }; var reason = EvaluationReason.Off.Instance; string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue, 1, reason); - var client = ClientWithFlagsJson(flagsJson); - - var expected = new EvaluationDetail(jsonValue, 1, reason); - var result = client.JsonVariationDetail("flag-key", new JValue(3)); - // Note, JToken.Equals() doesn't work, so we need to test each property separately - Assert.True(JToken.DeepEquals(expected.Value, result.Value)); - Assert.Equal(expected.VariationIndex, result.VariationIndex); - Assert.Equal(expected.Reason, result.Reason); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail(jsonValue, 1, reason); + var result = client.JsonVariationDetail("flag-key", new JValue(3)); + // Note, JToken.Equals() doesn't work, so we need to test each property separately + Assert.True(JToken.DeepEquals(expected.Value, result.Value)); + Assert.Equal(expected.VariationIndex, result.VariationIndex); + Assert.Equal(expected.Reason, result.Reason); + } } [Fact] public void AllFlagsReturnsAllFlagValues() { var flagsJson = @"{""flag1"":{""value"":""a""},""flag2"":{""value"":""b""}}"; - var client = ClientWithFlagsJson(flagsJson); - - var result = client.AllFlags(); - Assert.Equal(2, result.Count); - Assert.Equal(new JValue("a"), result["flag1"]); - Assert.Equal(new JValue("b"), result["flag2"]); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var result = client.AllFlags(); + Assert.Equal(2, result.Count); + Assert.Equal(new JValue("a"), result["flag1"]); + Assert.Equal(new JValue("b"), result["flag2"]); + } } [Fact] public void DefaultValueReturnedIfValueTypeIsDifferent() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); - var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); - var client = TestUtil.CreateClient(config, user); - - Assert.Equal(3, client.IntVariation("flag-key", 3)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } } [Fact] public void DefaultValueAndReasonIsReturnedIfValueTypeIsDifferent() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); - var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); - var client = TestUtil.CreateClient(config, user); - - var expected = new EvaluationDetail(3, null, new EvaluationReason.Error(EvaluationErrorKind.WRONG_TYPE)); - Assert.Equal(expected, client.IntVariationDetail("flag-key", 3)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + var expected = new EvaluationDetail(3, null, new EvaluationReason.Error(EvaluationErrorKind.WRONG_TYPE)); + Assert.Equal(expected, client.IntVariationDetail("flag-key", 3)); + } } [Fact] public void DefaultValueReturnedIfFlagValueIsNull() { string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", null); - var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); - var client = TestUtil.CreateClient(config, user); - - Assert.Equal(3, client.IntVariation("flag-key", 3)); + using (var client = ClientWithFlagsJson(flagsJson)) + { + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs index 387467a1..89973126 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -7,7 +7,17 @@ namespace LaunchDarkly.Xamarin.Tests public class LdClientEventTests { private static readonly User user = User.WithKey("userkey"); - private MockEventProcessor eventProcessor = new MockEventProcessor(); + private MockEventProcessor eventProcessor = new MockEventProcessor(); + + public LdClientEventTests() + { + TestUtil.ClearClient(); + } + + ~LdClientEventTests() + { + TestUtil.ClearClient(); + } public LdClient MakeClient(User user, string flagsJson) { diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 0f7f3272..46cdc271 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -9,6 +9,16 @@ public class DefaultLdClientTests static readonly string appKey = "some app key"; static readonly User simpleUser = User.WithKey("user-key"); + public DefaultLdClientTests() + { + TestUtil.ClearClient(); + } + + ~DefaultLdClientTests() + { + TestUtil.ClearClient(); + } + LdClient Client() { var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); @@ -39,14 +49,8 @@ public void CannotCreateClientWithNegativeWaitTime() public void CanCreateClientWithInfiniteWaitTime() { Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); - try - { - using (var client = LdClient.Init(config, simpleUser, System.Threading.Timeout.InfiniteTimeSpan)) { } - } - finally - { - LdClient.Instance = null; - } + using (var client = LdClient.Init(config, simpleUser, System.Threading.Timeout.InfiniteTimeSpan)) { } + TestUtil.ClearClient(); } [Fact] @@ -87,16 +91,10 @@ public void SharedClientIsTheOnlyClientAvailable() var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); using (var client = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { - try - { - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, simpleUser)); - } - finally - { - LdClient.Instance = null; - } + Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.Zero)); } } + TestUtil.ClearClient(); } [Fact] diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 57c8b013..abdd3d4e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -25,6 +25,15 @@ public static LdClient CreateClient(Configuration config, User user) } } + public static void ClearClient() + { + if (LdClient.Instance != null) + { + (LdClient.Instance as IDisposable).Dispose(); + LdClient.Instance = null; + } + } + public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) { JObject fo = new JObject { { "value", value } }; From ee940e3e7b16b39828d5ce9ad804fc470cbef05c Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 26 Mar 2019 09:33:37 -0700 Subject: [PATCH 071/254] Bump LD Common to beta4 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 0d329879..8e71ef7e 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -23,7 +23,7 @@ - + From 420c6b29865d94c522c7c7750dc786e4d2ae2ed5 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 26 Mar 2019 09:39:06 -0700 Subject: [PATCH 072/254] Fixed dotnet restore errors, forgot to bump LD Common beta4 in tests --- .../LaunchDarkly.Xamarin.Android.Tests.csproj | 3 ++- .../packages.config | 24 +++++++++---------- .../LaunchDarkly.Xamarin.iOS.Tests.csproj | 3 ++- .../packages.config | 24 +++++++++---------- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index e33858d0..419ce6c7 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -54,7 +54,7 @@ ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll - ..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.12.0.1\lib\netstandard2.0\Newtonsoft.Json.dll @@ -210,4 +210,5 @@ + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.Android.Tests/packages.config b/LaunchDarkly.Xamarin.Android.Tests/packages.config index 0da6d27d..dab9f17d 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/packages.config +++ b/LaunchDarkly.Xamarin.Android.Tests/packages.config @@ -4,16 +4,16 @@ - - + + - - + + - + @@ -26,32 +26,32 @@ - + - + - - + + - + - + - + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj index 8d99a7f8..5c0ed07e 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj @@ -94,7 +94,7 @@ ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll - ..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.12.0.1\lib\netstandard2.0\Newtonsoft.Json.dll @@ -125,4 +125,5 @@ + \ No newline at end of file diff --git a/LaunchDarkly.Xamarin.iOS.Tests/packages.config b/LaunchDarkly.Xamarin.iOS.Tests/packages.config index d0029198..6341915f 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/packages.config +++ b/LaunchDarkly.Xamarin.iOS.Tests/packages.config @@ -4,15 +4,15 @@ - - + + - - + + - + @@ -25,32 +25,32 @@ - + - + - - + + - + - + - + \ No newline at end of file diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index fc7af226..4db6963b 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 27c66edb3f39dcc21f36c63e1c97ef41a624a00a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 26 Mar 2019 20:40:39 -0700 Subject: [PATCH 073/254] Add "os" and "device" attributes to user --- src/LaunchDarkly.Xamarin/LdClient.shared.cs | 44 +++++++++---------- .../UserMetadata/UserMetadata.android.cs | 17 +++++++ .../UserMetadata/UserMetadata.ios.cs | 29 ++++++++++++ .../UserMetadata/UserMetadata.netstandard.cs | 16 +++++++ .../UserMetadata/UserMetadata.shared.cs | 21 +++++++++ 5 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs create mode 100644 src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs create mode 100644 src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs create mode 100644 src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.Xamarin/LdClient.shared.cs index 9244059c..ca33cde8 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.shared.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.shared.cs @@ -79,15 +79,7 @@ public sealed class LdClient : ILdMobileClient flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); platformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); - // If you pass in a user with a null or blank key, one will be assigned to them. - if (String.IsNullOrEmpty(user.Key)) - { - User = UserWithUniqueKey(user); - } - else - { - User = user; - } + User = DecorateUser(user); flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); connectionManager = Factory.CreateConnectionManager(configuration); @@ -492,16 +484,12 @@ public async Task IdentifyAsync(User user) throw new ArgumentNullException("user"); } - User userWithKey = user; - if (String.IsNullOrEmpty(user.Key)) - { - userWithKey = UserWithUniqueKey(user); - } + User newUser = DecorateUser(user); await connectionLock.WaitAsync(); try { - User = userWithKey; + User = newUser; await RestartUpdateProcessorAsync(Config.PollingInterval); } finally @@ -509,7 +497,7 @@ public async Task IdentifyAsync(User user) connectionLock.Release(); } - eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(userWithKey)); + eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(newUser)); } async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) @@ -533,14 +521,24 @@ void ClearUpdateProcessor() } } - User UserWithUniqueKey(User user) - { - string uniqueId = deviceInfo.UniqueDeviceId(); - return new User(user) + User DecorateUser(User user) + { + var newUser = new User(user); + if (UserMetadata.DeviceName != null) + { + newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); + } + if (UserMetadata.OSName != null) + { + newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); + } + // If you pass in a user with a null or blank key, one will be assigned to them. + if (String.IsNullOrEmpty(user.Key)) { - Key = uniqueId, - Anonymous = true - }; + newUser.Key = deviceInfo.UniqueDeviceId(); + newUser.Anonymous = true; + } + return newUser; } void IDisposable.Dispose() diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs new file mode 100644 index 00000000..e27ac884 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs @@ -0,0 +1,17 @@ +using Android.OS; + +namespace LaunchDarkly.Xamarin +{ + internal static partial class UserMetadata + { + private static string GetDevice() + { + return Build.Model + " " + Build.Product; + } + + private static string GetOS() + { + return "Android " + Build.VERSION.SdkInt; + } + } +} diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs new file mode 100644 index 00000000..da7f53e3 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs @@ -0,0 +1,29 @@ +using UIKit; + +namespace LaunchDarkly.Xamarin +{ + internal static partial class UserMetadata + { + private static string GetDevice() + { + switch (UIDevice.CurrentDevice.UserInterfaceIdiom) + { + case UIUserInterfaceIdiom.CarPlay: + return "CarPlay"; + case UIUserInterfaceIdiom.Pad: + return "iPad"; + case UIUserInterfaceIdiom.Phone: + return "iPhone"; + case UIUserInterfaceIdiom.TV: + return "Apple TV"; + default: + return "unknown"; + } + } + + private static string GetOS() + { + return "iOS " + UIDevice.CurrentDevice.SystemVersion; + } + } +} diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs new file mode 100644 index 00000000..9ef808d8 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs @@ -0,0 +1,16 @@ + +namespace LaunchDarkly.Xamarin +{ + internal static partial class UserMetadata + { + private static string GetDevice() + { + return null; + } + + private static string GetOS() + { + return null; + } + } +} diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs new file mode 100644 index 00000000..ba4e7d4e --- /dev/null +++ b/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs @@ -0,0 +1,21 @@ + +namespace LaunchDarkly.Xamarin +{ + internal static partial class UserMetadata + { + private static readonly string _os = GetOS(); + private static readonly string _device = GetDevice(); + + /// + /// Returns the string that should be passed in the "device" property for all users. + /// + /// The value for "device", or null if none. + internal static string DeviceName => _device; + + /// + /// Returns the string that should be passed in the "os" property for all users. + /// + /// The value for "os", or null if none. + internal static string OSName => _os; + } +} From 59191cd46fca30dbf9a81f9c19260174a9ea28ee Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 27 Mar 2019 16:45:28 -0700 Subject: [PATCH 074/254] create package during build + update dependency versions --- .../LaunchDarkly.Xamarin.Android.Tests.csproj | 4 ++-- LaunchDarkly.Xamarin.Android.Tests/packages.config | 4 ++-- .../LaunchDarkly.Xamarin.iOS.Tests.csproj | 4 ++-- LaunchDarkly.Xamarin.iOS.Tests/packages.config | 4 ++-- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 3 ++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index 419ce6c7..9f01b205 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -62,10 +62,10 @@ ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll - ..\packages\LaunchDarkly.EventSource.3.2.3\lib\netstandard1.4\LaunchDarkly.EventSource.dll + ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - ..\packages\LaunchDarkly.Common.1.2.3\lib\netstandard2.0\LaunchDarkly.Common.dll + ..\packages\LaunchDarkly.Common.2.0.0\lib\netstandard2.0\LaunchDarkly.Common.dll ..\packages\Plugin.CurrentActivity.2.1.0.4\lib\monoandroid44\Plugin.CurrentActivity.dll diff --git a/LaunchDarkly.Xamarin.Android.Tests/packages.config b/LaunchDarkly.Xamarin.Android.Tests/packages.config index dab9f17d..ca0397ce 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/packages.config +++ b/LaunchDarkly.Xamarin.Android.Tests/packages.config @@ -2,8 +2,8 @@ - - + + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj index 5c0ed07e..3eb9908b 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj @@ -102,10 +102,10 @@ ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll - ..\packages\LaunchDarkly.EventSource.3.2.3\lib\netstandard1.4\LaunchDarkly.EventSource.dll + ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - ..\packages\LaunchDarkly.Common.1.2.3\lib\netstandard2.0\LaunchDarkly.Common.dll + ..\packages\LaunchDarkly.Common.2.0.0\lib\netstandard2.0\LaunchDarkly.Common.dll ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\xamarinios10\Plugin.DeviceInfo.dll diff --git a/LaunchDarkly.Xamarin.iOS.Tests/packages.config b/LaunchDarkly.Xamarin.iOS.Tests/packages.config index 6341915f..db23f9b3 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/packages.config +++ b/LaunchDarkly.Xamarin.iOS.Tests/packages.config @@ -2,8 +2,8 @@ - - + + diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 8e71ef7e..3c3dcabd 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -12,6 +12,7 @@ bin\$(Configuration)\$(Framework) true latest + True @@ -23,7 +24,7 @@ - + From b1274fa728ccd436747f302748576eb1c26982ce Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 27 Mar 2019 16:51:16 -0700 Subject: [PATCH 075/254] fix dependency version --- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 4db6963b..1cde4188 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 94a39afc7caaea23201815feb0a302fdb943f160 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 28 Mar 2019 16:36:44 -0700 Subject: [PATCH 076/254] fix unstable test state --- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 41 +++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index abdd3d4e..ae3cbe2a 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -25,13 +25,16 @@ public static LdClient CreateClient(Configuration config, User user) } } - public static void ClearClient() - { - if (LdClient.Instance != null) - { - (LdClient.Instance as IDisposable).Dispose(); - LdClient.Instance = null; - } + public static void ClearClient() + { + lock (ClientInstanceLock) + { + if (LdClient.Instance != null) + { + (LdClient.Instance as IDisposable).Dispose(); + LdClient.Instance = null; + } + } } public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) @@ -41,9 +44,9 @@ public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? { fo["variation"] = new JValue(variation.Value); } - if (reason != null) - { - fo["reason"] = JToken.FromObject(reason); + if (reason != null) + { + fo["reason"] = JToken.FromObject(reason); } JObject o = new JObject { { flagKey, fo } }; return JsonConvert.SerializeObject(o); @@ -62,15 +65,15 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string { stubbedFlagCache.CacheFlagsForUser(flags, user); } - - Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) - .WithConnectionManager(new MockConnectionManager(true)) - .WithEventProcessor(new MockEventProcessor()) - .WithUpdateProcessor(new MockPollingProcessor()) - .WithPersister(new MockPersister()) - .WithDeviceInfo(new MockDeviceInfo("")) - .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); + + Configuration configuration = Configuration.Default(appKey) + .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) + .WithConnectionManager(new MockConnectionManager(true)) + .WithEventProcessor(new MockEventProcessor()) + .WithUpdateProcessor(new MockPollingProcessor()) + .WithPersister(new MockPersister()) + .WithDeviceInfo(new MockDeviceInfo("")) + .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); return configuration; } } From a324295888c572183e1e92a67678c20fbfae7d67 Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Thu, 2 May 2019 17:19:36 -0700 Subject: [PATCH 077/254] apply markdown templates (#33) --- CHANGELOG.md | 21 ++++++ CONTRIBUTING.md | 43 ++++++++++++ .../LaunchDarkly.Xamarin.Android.Tests.csproj | 2 +- .../LaunchDarkly.Xamarin.iOS.Tests.csproj | 2 +- README.md | 68 ++++++------------- 5 files changed, 85 insertions(+), 51 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2dfdcd6d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Change log + +All notable changes to the LaunchDarkly Client-side SDK for Xamarin will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org). + +# Note on future releases + +The LaunchDarkly SDK repositories are being renamed for consistency. This repository is now `xamarin-client-sdk` rather than `xamarin-client`. + +The package name will also change. In the 1.0.0-beta16 release, the published package was `LaunchDarkly.Xamarin`; in all future releases, it will be `LaunchDarkly.XamarinSdk`. + +## [1.0.0-beta16] - 2019-04-05 +### Added: +- In Android and iOS, when an app is in the background, the SDK should turn off the streaming connection and instead poll for flag updates at an interval determined by `Configuration.BackgroundPollingInterval` (default: 60 minutes). +- The SDK now supports [evaluation reasons](https://docs.launchdarkly.com/docs/evaluation-reasons). See `Configuration.WithEvaluationReasons` and `ILdMobileClient.BoolVariationDetail`. +- The SDK now sends custom attributes called `os` and `device` as part of the user data, indicating the user's platform and OS version. This is the same as what the native Android and iOS SDKs do, except that "iOS" or "Android" is also prepended to the `os` property. +### Changed: +- This is the first version that is built specifically for iOS and Android platforms. There is also still a .NET Standard 1.0 build in the same package. +- The SDK no longer uses Xamarin Essentials. +### Fixed: +- Under some circumstances, a `CancellationTokenSource` object could be leaked. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..39bd2b70 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +Contributing to the LaunchDarkly Client-side SDK for Xamarin +================================================ + +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. + +Submitting bug reports and feature requests +------------------ + +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/xamarin-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +Submitting pull requests +------------------ + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. + +Build instructions +------------------ + +### Prerequisites + +This SDK is built against .Net Standard 1.6 and 2.0 with the `microsoft/dotnet` Docker image. See the SDK's [CI configuration](.circleci/config.yml) to determine which image version in used by LaunchDarkly. + +To set up the project and dependencies, run the following command in the root SDK directory: + +``` +dotnet restore +``` + +### Building + +To build the SDK without running any tests: + +``` +msbuild +``` + +### Testing + +To build the SDK and run all unit tests: +``` +dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 +dotnet test tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj -f netcoreapp2.0 +``` diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj index 9f01b205..93a81662 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj @@ -42,7 +42,7 @@ - ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\monoandroid81\LaunchDarkly.Xamarin.dll + ..\..\xamarin-client-sdk-private\src\LaunchDarkly.Xamarin\bin\Debug\monoandroid81\LaunchDarkly.Xamarin.dll diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj index 3eb9908b..7f486cdf 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj +++ b/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj @@ -82,7 +82,7 @@ - ..\..\xamarin-client-private\src\LaunchDarkly.Xamarin\bin\Debug\xamarin.ios10\LaunchDarkly.Xamarin.dll + ..\..\xamarin-client-sdk-private\src\LaunchDarkly.Xamarin\bin\Debug\xamarin.ios10\LaunchDarkly.Xamarin.dll diff --git a/README.md b/README.md index ef2492cc..f3ebe7ff 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,30 @@ -LaunchDarkly SDK [BETA] for Xamarin +LaunchDarkly Client-side SDK for Xamarin =========================== -[![CircleCI](https://circleci.com/gh/launchdarkly/xamarin-client/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/xamarin-client/tree/master) -*This software is a **beta** version and should not be considered ready for production use until tagged at least 1.0.* +[![CircleCI](https://circleci.com/gh/launchdarkly/xamarin-client-sdk/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/xamarin-client-sdk/tree/master) + +*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) Supported platforms ------------------- This beta release is built for the following targets: Android 7.1, 8.0, 8.1; iOS 10; .NET Standard 1.6, 2.0. It has also been tested with Android 9/API 28 and iOS 12.1. -Quick setup +Getting started ----------- -1. Use [NuGet](http://docs.nuget.org/docs/start-here/using-the-package-manager-console) to add the Xamarin SDK to your project: - - Install-Package LaunchDarkly.Xamarin - -2. Import the LaunchDarkly packages: - - using LaunchDarkly.Client; - using LaunchDarkly.Xamarin; - -3. Initialize the LDClient with your Mobile key and user: - - User user = User.WithKey(username); - LdClient ldClient = LdClient.Init("YOUR_MOBILE_KEY", user); - -Your first feature flag ------------------------ - -1. Create a new feature flag on your [dashboard](https://app.launchdarkly.com). -2. In your application code, use the feature's key to check whether the flag is on for each user: - - bool showFeature = ldClient.BoolVariation("your.feature.key"); - if (showFeature) { - // application code to show the feature - } - else { - // the code to run if the feature is off - } +Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/xamarin-sdk-reference#section-getting-started) for instructions on getting started with using the SDK. Learn more ----------- -Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/v2.0/docs/xamarin-sdk-reference). +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/xamarin-sdk-reference). Testing ------- @@ -53,7 +34,7 @@ We run integration tests for all our SDKs using a centralized test harness. This Contributing ------------ -See [Contributing](https://github.com/launchdarkly/xamarin-client/blob/master/CONTRIBUTING.md). +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. About LaunchDarkly ----------- @@ -63,21 +44,10 @@ About LaunchDarkly * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for - * [Java](http://docs.launchdarkly.com/docs/java-sdk-reference "Java SDK") - * [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK") - * [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK") - * [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK") - * [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK") - * [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK") - * [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK") - * [Xamarin](http://docs.launchdarkly.com/docs/xamarin-sdk-reference "LaunchDarkly Xamarin SDK") - * [Ruby](http://docs.launchdarkly.com/docs/ruby-sdk-reference "LaunchDarkly Ruby SDK") - * [iOS](http://docs.launchdarkly.com/docs/ios-sdk-reference "LaunchDarkly iOS SDK") - * [Android](http://docs.launchdarkly.com/docs/android-sdk-reference "LaunchDarkly Android SDK") +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. * Explore LaunchDarkly - * [launchdarkly.com](http://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information - * [docs.launchdarkly.com](http://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDKs - * [apidocs.launchdarkly.com](http://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation - * [blog.launchdarkly.com](http://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies From bcffc7e8f17ad74f6c2e1e6cce2e7decce7b3b56 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 14 May 2019 16:05:25 -0700 Subject: [PATCH 078/254] use new package naming convention --- .circleci/config.yml | 4 ++-- CHANGELOG.md | 4 ++++ CONTRIBUTING.md | 4 ++-- .../LDAndroidTests.cs | 0 .../LaunchDarkly.XamarinSdk.Android.Tests.csproj | 8 ++++---- .../MainActivity.cs | 0 .../Properties/AndroidManifest.xml | 0 .../Properties/AssemblyInfo.cs | 0 .../Resources/AboutResources.txt | 0 .../Resources/Resource.designer.cs | 0 .../Resources/mipmap-hdpi/Icon.png | Bin .../Resources/mipmap-mdpi/Icon.png | Bin .../Resources/mipmap-xhdpi/Icon.png | Bin .../Resources/mipmap-xxhdpi/Icon.png | Bin .../Resources/mipmap-xxxhdpi/Icon.png | Bin .../designtime/build.props | 0 .../packages.config | 2 +- .../Entitlements.plist | 0 .../Info.plist | 0 .../LDiOSTests.cs | 0 .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 10 +++++----- .../LaunchScreen.storyboard | 0 .../Main.cs | 0 .../UnitTestAppDelegate.cs | 0 .../packages.config | 2 +- ...arkly.Xamarin.sln => LaunchDarkly.XamarinSdk.sln | 8 ++++---- .../Properties/AssemblyInfo.shared.cs | 6 ------ .../BackgroundAdapter/BackgroundAdapter.android.cs | 0 .../BackgroundAdapter/BackgroundAdapter.ios.cs | 0 .../BackgroundAdapter.netstandard.cs | 0 .../ClientRunMode.shared.cs | 0 .../Configuration.shared.cs | 0 .../Connectivity/Connectivity.android.cs | 0 .../Connectivity/Connectivity.ios.cs | 0 .../Connectivity/Connectivity.ios.reachability.cs | 0 .../Connectivity/Connectivity.netstandard.cs | 0 .../Connectivity/Connectivity.shared.cs | 0 .../Connectivity/Connectivity.shared.enums.cs | 0 .../Constants.shared.cs | 0 .../DeviceInfo.shared.cs | 0 .../Extensions.shared.cs | 0 .../Factory.shared.cs | 0 .../FeatureFlag.shared.cs | 0 .../FeatureFlagListenerManager.shared.cs | 0 .../FeatureFlagRequestor.shared.cs | 0 .../FlagCacheManager.shared.cs | 0 .../IBackgroundingState.shared.cs | 0 .../IConnectionManager.shared.cs | 0 .../IDeviceInfo.shared.cs | 0 .../IFeatureFlagListener.shared.cs | 0 .../IFeatureFlagListenerManager.shared.cs | 0 .../IFlagCacheManager.shared.cs | 0 .../ILdMobileClient.shared.cs | 0 .../IMobileConfiguration.shared.cs | 0 .../IMobileUpdateProcessor.shared.cs | 0 .../IPlatformAdapter.shared.cs | 0 .../ISimplePersistance.shared.cs | 0 .../IUserFlagCache.shared.cs | 0 .../LaunchDarkly.XamarinSdk.csproj} | 8 ++++---- .../LdClient.shared.cs | 0 .../MainThread/MainThread.android.cs | 0 .../MainThread/MainThread.ios.cs | 0 .../MainThread/MainThread.netstandard.cs | 0 .../MainThread/MainThread.shared.cs | 0 .../MobileConnectionManager.shared.cs | 0 .../MobilePollingProcessor.shared.cs | 0 .../MobileSideClientEnvironment.shared.cs | 0 .../MobileStreamingProcessor.shared.cs | 0 .../Permissions/Permissions.android.cs | 0 .../Permissions/Permissions.ios.cs | 0 .../Permissions/Permissions.netstandard.cs | 0 .../Permissions/Permissions.shared.cs | 0 .../Permissions/Permissions.shared.enums.cs | 0 .../Platform/Platform.android.cs | 0 .../Platform/Platform.ios.cs | 0 .../Platform/Platform.shared.cs | 0 .../Preferences/Preferences.android.cs | 0 .../Preferences/Preferences.ios.cs | 0 .../Preferences/Preferences.netstandard.cs | 0 .../Preferences/Preferences.shared.cs | 0 .../Properties/AssemblyInfo.shared.cs | 6 ++++++ .../SimpleInMemoryPersistance.shared.cs | 0 .../SimpleMobileDevicePersistance.shared.cs | 0 .../UserFlagDeviceCache.shared.cs | 0 .../UserFlagInMemoryCache.shared.cs | 0 .../UserMetadata/UserMetadata.android.cs | 0 .../UserMetadata/UserMetadata.ios.cs | 0 .../UserMetadata/UserMetadata.netstandard.cs | 0 .../UserMetadata/UserMetadata.shared.cs | 0 .../ValueType.shared.cs | 0 .../ConfigurationTest.cs | 0 .../FeatureFlagListenerTests.cs | 0 .../FeatureFlagTests.cs | 0 .../FlagCacheManagerTests.cs | 0 .../LaunchDarkly.XamarinSdk.Tests.csproj} | 5 +++-- .../LdClientEvaluationTests.cs | 0 .../LdClientEventTests.cs | 0 .../LdClientTests.cs | 0 .../MobilePollingProcessorTests.cs | 0 .../MobileStreamingProcessorTests.cs | 0 .../MockComponents.cs | 0 .../TestUtil.cs | 0 .../UserFlagCacheTests.cs | 0 .../xunit-to-junit.xslt | 0 104 files changed, 36 insertions(+), 31 deletions(-) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/LDAndroidTests.cs (100%) rename LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj => LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj (97%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/MainActivity.cs (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Properties/AndroidManifest.xml (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Properties/AssemblyInfo.cs (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/AboutResources.txt (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/Resource.designer.cs (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/mipmap-hdpi/Icon.png (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/mipmap-mdpi/Icon.png (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/mipmap-xhdpi/Icon.png (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/mipmap-xxhdpi/Icon.png (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/Resources/mipmap-xxxhdpi/Icon.png (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/designtime/build.props (100%) rename {LaunchDarkly.Xamarin.Android.Tests => LaunchDarkly.XamarinSdk.Android.Tests}/packages.config (98%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/Entitlements.plist (100%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/Info.plist (100%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/LDiOSTests.cs (100%) rename LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj => LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj (91%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/LaunchScreen.storyboard (100%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/Main.cs (100%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/UnitTestAppDelegate.cs (100%) rename {LaunchDarkly.Xamarin.iOS.Tests => LaunchDarkly.XamarinSdk.iOS.Tests}/packages.config (98%) rename LaunchDarkly.Xamarin.sln => LaunchDarkly.XamarinSdk.sln (89%) delete mode 100644 src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/BackgroundAdapter/BackgroundAdapter.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/BackgroundAdapter/BackgroundAdapter.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/BackgroundAdapter/BackgroundAdapter.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/ClientRunMode.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Configuration.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.ios.reachability.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Connectivity/Connectivity.shared.enums.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Constants.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/DeviceInfo.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Extensions.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Factory.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/FeatureFlag.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/FeatureFlagListenerManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/FeatureFlagRequestor.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/FlagCacheManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IBackgroundingState.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IConnectionManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IDeviceInfo.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IFeatureFlagListener.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IFeatureFlagListenerManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IFlagCacheManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/ILdMobileClient.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IMobileConfiguration.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IMobileUpdateProcessor.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IPlatformAdapter.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/ISimplePersistance.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/IUserFlagCache.shared.cs (100%) rename src/{LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj => LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj} (91%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/LdClient.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MainThread/MainThread.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MainThread/MainThread.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MainThread/MainThread.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MainThread/MainThread.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MobileConnectionManager.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MobilePollingProcessor.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MobileSideClientEnvironment.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/MobileStreamingProcessor.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Permissions/Permissions.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Permissions/Permissions.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Permissions/Permissions.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Permissions/Permissions.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Permissions/Permissions.shared.enums.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Platform/Platform.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Platform/Platform.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Platform/Platform.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Preferences/Preferences.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Preferences/Preferences.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Preferences/Preferences.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/Preferences/Preferences.shared.cs (100%) create mode 100644 src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/SimpleInMemoryPersistance.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/SimpleMobileDevicePersistance.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserFlagDeviceCache.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserFlagInMemoryCache.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserMetadata/UserMetadata.android.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserMetadata/UserMetadata.ios.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserMetadata/UserMetadata.netstandard.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/UserMetadata/UserMetadata.shared.cs (100%) rename src/{LaunchDarkly.Xamarin => LaunchDarkly.XamarinSdk}/ValueType.shared.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/ConfigurationTest.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/FeatureFlagListenerTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/FeatureFlagTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/FlagCacheManagerTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj => LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj} (78%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/LdClientEvaluationTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/LdClientEventTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/LdClientTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/MobilePollingProcessorTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/MobileStreamingProcessorTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/MockComponents.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/TestUtil.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/UserFlagCacheTests.cs (100%) rename tests/{LaunchDarkly.Xamarin.Tests => LaunchDarkly.XamarinSdk.Tests}/xunit-to-junit.xslt (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 93ae6397..da05f2db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,5 +11,5 @@ jobs: steps: - checkout - run: dotnet restore - - run: dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 - - run: dotnet test tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj -f netcoreapp2.0 + - run: dotnet build src/LaunchDarkly.XamarinSdk -f netstandard2.0 + - run: dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfdcd6d..0b78f488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The LaunchDarkly SDK repositories are being renamed for consistency. This reposi The package name will also change. In the 1.0.0-beta16 release, the published package was `LaunchDarkly.Xamarin`; in all future releases, it will be `LaunchDarkly.XamarinSdk`. +## [1.0.0-beta17] - 2019-05-15 +### Changed: +- The NuGet package name for this SDK is now `LaunchDarkly.XamarinSdk`. There are no other changes. Substituting `LaunchDarkly.Xamarin` 1.0.0-beta16 with `LaunchDarkly.XamarinSdk` 1.0.0-beta17 should not affect functionality. + ## [1.0.0-beta16] - 2019-04-05 ### Added: - In Android and iOS, when an app is in the background, the SDK should turn off the streaming connection and instead poll for flag updates at an interval determined by `Configuration.BackgroundPollingInterval` (default: 60 minutes). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39bd2b70..9f8918e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,6 @@ msbuild To build the SDK and run all unit tests: ``` -dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 -dotnet test tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj -f netcoreapp2.0 +dotnet build src/LaunchDarkly.XamarinSdk -f netstandard2.0 +dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 ``` diff --git a/LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/LDAndroidTests.cs rename to LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs diff --git a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj b/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj similarity index 97% rename from LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj rename to LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index 93a81662..4874f79b 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/LaunchDarkly.Xamarin.Android.Tests.csproj +++ b/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -7,7 +7,7 @@ {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library LaunchDarkly.Xamarin.Android.Tests - LaunchDarkly.Xamarin.Android.Tests + LaunchDarkly.XamarinSdk.Android.Tests v9.0 MonoAndroid90 True @@ -42,7 +42,7 @@ - ..\..\xamarin-client-sdk-private\src\LaunchDarkly.Xamarin\bin\Debug\monoandroid81\LaunchDarkly.Xamarin.dll + ..\src\LaunchDarkly.XamarinSdk\bin\Debug\monoandroid81\LaunchDarkly.XamarinSdk.dll @@ -64,8 +64,8 @@ ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - - ..\packages\LaunchDarkly.Common.2.0.0\lib\netstandard2.0\LaunchDarkly.Common.dll + + ..\packages\LaunchDarkly.CommonSdk.2.1.2\lib\netstandard2.0\LaunchDarkly.CommonSdk.dll ..\packages\Plugin.CurrentActivity.2.1.0.4\lib\monoandroid44\Plugin.CurrentActivity.dll diff --git a/LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs b/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/MainActivity.cs rename to LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs diff --git a/LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml b/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Properties/AndroidManifest.xml rename to LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml diff --git a/LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs b/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Properties/AssemblyInfo.cs rename to LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/AboutResources.txt rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/Resource.designer.cs rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-hdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/Icon.png similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-hdpi/Icon.png rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/Icon.png diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-mdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/Icon.png similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-mdpi/Icon.png rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/Icon.png diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xhdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/Icon.png similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xhdpi/Icon.png rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/Icon.png diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxhdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/Icon.png similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxhdpi/Icon.png rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/Icon.png diff --git a/LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png rename to LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png diff --git a/LaunchDarkly.Xamarin.Android.Tests/designtime/build.props b/LaunchDarkly.XamarinSdk.Android.Tests/designtime/build.props similarity index 100% rename from LaunchDarkly.Xamarin.Android.Tests/designtime/build.props rename to LaunchDarkly.XamarinSdk.Android.Tests/designtime/build.props diff --git a/LaunchDarkly.Xamarin.Android.Tests/packages.config b/LaunchDarkly.XamarinSdk.Android.Tests/packages.config similarity index 98% rename from LaunchDarkly.Xamarin.Android.Tests/packages.config rename to LaunchDarkly.XamarinSdk.Android.Tests/packages.config index ca0397ce..6619a7d0 100644 --- a/LaunchDarkly.Xamarin.Android.Tests/packages.config +++ b/LaunchDarkly.XamarinSdk.Android.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist b/LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/Entitlements.plist rename to LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Info.plist b/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/Info.plist rename to LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/LDiOSTests.cs rename to LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj similarity index 91% rename from LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj rename to LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index 7f486cdf..9260bad4 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/LaunchDarkly.Xamarin.iOS.Tests.csproj +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -7,7 +7,7 @@ {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Exe LaunchDarkly.Xamarin.iOS.Tests - LaunchDarkly.Xamarin.iOS.Tests + LaunchDarkly.XamarinSdk.iOS.Tests Resources @@ -81,8 +81,8 @@ - - ..\..\xamarin-client-sdk-private\src\LaunchDarkly.Xamarin\bin\Debug\xamarin.ios10\LaunchDarkly.Xamarin.dll + + ..\src\LaunchDarkly.XamarinSdk\bin\Debug\xamarin.ios10\LaunchDarkly.XamarinSdk.dll @@ -104,8 +104,8 @@ ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - - ..\packages\LaunchDarkly.Common.2.0.0\lib\netstandard2.0\LaunchDarkly.Common.dll + + ..\packages\LaunchDarkly.CommonSdk.2.1.2\lib\netstandard2.0\LaunchDarkly.CommonSdk.dll ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\xamarinios10\Plugin.DeviceInfo.dll diff --git a/LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/LaunchScreen.storyboard rename to LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard diff --git a/LaunchDarkly.Xamarin.iOS.Tests/Main.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/Main.cs rename to LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs diff --git a/LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs similarity index 100% rename from LaunchDarkly.Xamarin.iOS.Tests/UnitTestAppDelegate.cs rename to LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs diff --git a/LaunchDarkly.Xamarin.iOS.Tests/packages.config b/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config similarity index 98% rename from LaunchDarkly.Xamarin.iOS.Tests/packages.config rename to LaunchDarkly.XamarinSdk.iOS.Tests/packages.config index db23f9b3..3a5708a9 100644 --- a/LaunchDarkly.Xamarin.iOS.Tests/packages.config +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/LaunchDarkly.Xamarin.sln b/LaunchDarkly.XamarinSdk.sln similarity index 89% rename from LaunchDarkly.Xamarin.sln rename to LaunchDarkly.XamarinSdk.sln index 82f88ff0..348c10fb 100644 --- a/LaunchDarkly.Xamarin.sln +++ b/LaunchDarkly.XamarinSdk.sln @@ -2,13 +2,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin", "src\LaunchDarkly.Xamarin\LaunchDarkly.Xamarin.csproj", "{7717A2B2-9905-40A7-989F-790139D69543}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk", "src\LaunchDarkly.XamarinSdk\LaunchDarkly.XamarinSdk.csproj", "{7717A2B2-9905-40A7-989F-790139D69543}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.Tests", "tests\LaunchDarkly.Xamarin.Tests\LaunchDarkly.Xamarin.Tests.csproj", "{F6B71DFE-314C-4F27-A219-A14569C8CF48}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Tests", "tests\LaunchDarkly.XamarinSdk.Tests\LaunchDarkly.XamarinSdk.Tests.csproj", "{F6B71DFE-314C-4F27-A219-A14569C8CF48}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.iOS.Tests", "LaunchDarkly.Xamarin.iOS.Tests\LaunchDarkly.Xamarin.iOS.Tests.csproj", "{066AA0F9-449A-48F5-9492-D698F0EFD923}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.iOS.Tests", "LaunchDarkly.XamarinSdk.iOS.Tests\LaunchDarkly.XamarinSdk.iOS.Tests.csproj", "{066AA0F9-449A-48F5-9492-D698F0EFD923}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.Xamarin.Android.Tests", "LaunchDarkly.Xamarin.Android.Tests\LaunchDarkly.Xamarin.Android.Tests.csproj", "{0B18C336-C770-42C1-B77A-E4A49F789677}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Android.Tests", "LaunchDarkly.XamarinSdk.Android.Tests\LaunchDarkly.XamarinSdk.Android.Tests.csproj", "{0B18C336-C770-42C1-B77A-E4A49F789677}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs deleted file mode 100644 index 25bb9808..00000000 --- a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.shared.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests")] -[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.iOS.Tests")] -[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Android.Tests")] diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.android.cs rename to src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.android.cs diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.ios.cs rename to src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs diff --git a/src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/BackgroundAdapter/BackgroundAdapter.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/ClientRunMode.shared.cs b/src/LaunchDarkly.XamarinSdk/ClientRunMode.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ClientRunMode.shared.cs rename to src/LaunchDarkly.XamarinSdk/ClientRunMode.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.shared.cs b/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Configuration.shared.cs rename to src/LaunchDarkly.XamarinSdk/Configuration.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.android.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.android.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.reachability.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.ios.reachability.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.reachability.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Connectivity/Connectivity.shared.enums.cs rename to src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs diff --git a/src/LaunchDarkly.Xamarin/Constants.shared.cs b/src/LaunchDarkly.XamarinSdk/Constants.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Constants.shared.cs rename to src/LaunchDarkly.XamarinSdk/Constants.shared.cs diff --git a/src/LaunchDarkly.Xamarin/DeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/DeviceInfo.shared.cs rename to src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Extensions.shared.cs b/src/LaunchDarkly.XamarinSdk/Extensions.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Extensions.shared.cs rename to src/LaunchDarkly.XamarinSdk/Extensions.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Factory.shared.cs b/src/LaunchDarkly.XamarinSdk/Factory.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Factory.shared.cs rename to src/LaunchDarkly.XamarinSdk/Factory.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlag.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlag.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlag.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlag.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlagListenerManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlagRequestor.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FeatureFlagRequestor.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/FlagCacheManager.shared.cs b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/FlagCacheManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs b/src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IBackgroundingState.shared.cs rename to src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IConnectionManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IConnectionManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IConnectionManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IConnectionManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IDeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IDeviceInfo.shared.cs rename to src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFeatureFlagListener.shared.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFeatureFlagListener.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFeatureFlagListenerManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IFlagCacheManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IFlagCacheManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IFlagCacheManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFlagCacheManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/ILdMobileClient.shared.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ILdMobileClient.shared.cs rename to src/LaunchDarkly.XamarinSdk/ILdMobileClient.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IMobileConfiguration.shared.cs b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IMobileConfiguration.shared.cs rename to src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IMobileUpdateProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs b/src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IPlatformAdapter.shared.cs rename to src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs diff --git a/src/LaunchDarkly.Xamarin/ISimplePersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ISimplePersistance.shared.cs rename to src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs diff --git a/src/LaunchDarkly.Xamarin/IUserFlagCache.shared.cs b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/IUserFlagCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj similarity index 91% rename from src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj rename to src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 4f3a28d8..83070170 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -4,10 +4,10 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - 1.0.0-beta16 + 1.0.0-beta17 Library - LaunchDarkly.Xamarin - LaunchDarkly.Xamarin + LaunchDarkly.XamarinSdk + LaunchDarkly.XamarinSdk false bin\$(Configuration)\$(Framework) true @@ -24,7 +24,7 @@ - + diff --git a/src/LaunchDarkly.Xamarin/LdClient.shared.cs b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/LdClient.shared.cs rename to src/LaunchDarkly.XamarinSdk/LdClient.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MainThread/MainThread.android.cs rename to src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MainThread/MainThread.ios.cs rename to src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MainThread/MainThread.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MainThread/MainThread.shared.cs rename to src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobileConnectionManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobilePollingProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobileSideClientEnvironment.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.shared.cs diff --git a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/MobileStreamingProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Permissions/Permissions.android.cs rename to src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Permissions/Permissions.ios.cs rename to src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Permissions/Permissions.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.cs rename to src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.enums.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Permissions/Permissions.shared.enums.cs rename to src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.enums.cs diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.android.cs b/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Platform/Platform.android.cs rename to src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs b/src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Platform/Platform.ios.cs rename to src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs diff --git a/src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs b/src/LaunchDarkly.XamarinSdk/Platform/Platform.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Platform/Platform.shared.cs rename to src/LaunchDarkly.XamarinSdk/Platform/Platform.shared.cs diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Preferences/Preferences.android.cs rename to src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Preferences/Preferences.ios.cs rename to src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Preferences/Preferences.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/Preferences/Preferences.shared.cs rename to src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs new file mode 100644 index 00000000..ea362dc7 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs @@ -0,0 +1,6 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("LaunchDarkly.XamarinSdk.Tests")] +[assembly: InternalsVisibleTo("LaunchDarkly.XamarinSdk.iOS.Tests")] +[assembly: InternalsVisibleTo("LaunchDarkly.XamarinSdk.Android.Tests")] diff --git a/src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/SimpleInMemoryPersistance.shared.cs rename to src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs diff --git a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.shared.cs rename to src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs diff --git a/src/LaunchDarkly.Xamarin/UserFlagDeviceCache.shared.cs b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserFlagDeviceCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs diff --git a/src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.shared.cs b/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserFlagInMemoryCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.shared.cs diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.android.cs rename to src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.ios.cs rename to src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs diff --git a/src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/UserMetadata/UserMetadata.shared.cs rename to src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs diff --git a/src/LaunchDarkly.Xamarin/ValueType.shared.cs b/src/LaunchDarkly.XamarinSdk/ValueType.shared.cs similarity index 100% rename from src/LaunchDarkly.Xamarin/ValueType.shared.cs rename to src/LaunchDarkly.XamarinSdk/ValueType.shared.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagListenerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/FeatureFlagListenerTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj similarity index 78% rename from tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj rename to tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 1cde4188..1b212463 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -3,13 +3,14 @@ netcoreapp2.0 2.0.0 + LaunchDarkly.XamarinSdk.Tests - + @@ -21,6 +22,6 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs rename to tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/xunit-to-junit.xslt b/tests/LaunchDarkly.XamarinSdk.Tests/xunit-to-junit.xslt similarity index 100% rename from tests/LaunchDarkly.Xamarin.Tests/xunit-to-junit.xslt rename to tests/LaunchDarkly.XamarinSdk.Tests/xunit-to-junit.xslt From 32437cbb5101b5448c46674cb3a3f5bf52e207a7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 17 May 2019 17:09:44 -0700 Subject: [PATCH 079/254] clean up platform code, don't throw NotImplementedException --- .../BackgroundAdapter.netstandard.cs | 4 +- .../Connectivity/Connectivity.netstandard.cs | 56 ++--- .../Connectivity/Connectivity.shared.cs | 6 +- .../MainThread/MainThread.android.cs | 51 ----- .../MainThread/MainThread.ios.cs | 38 ---- .../MainThread/MainThread.netstandard.cs | 35 --- .../MainThread/MainThread.shared.cs | 100 -------- .../MobileConnectionManager.shared.cs | 17 +- .../Permissions/Permissions.android.cs | 205 +++++++++-------- .../Permissions/Permissions.ios.cs | 124 ---------- .../Permissions/Permissions.netstandard.cs | 39 ---- .../Permissions/Permissions.shared.cs | 46 ---- .../Platform/Platform.android.cs | 214 +++++++++--------- .../Platform/Platform.ios.cs | 104 --------- .../Preferences/Preferences.netstandard.cs | 42 +--- .../SimpleMobileDevicePersistance.shared.cs | 15 +- .../UserMetadata/UserMetadata.android.cs | 12 +- .../UserMetadata/UserMetadata.ios.cs | 33 +-- .../UserMetadata/UserMetadata.netstandard.cs | 12 +- .../UserMetadata/UserMetadata.shared.cs | 8 +- 20 files changed, 287 insertions(+), 874 deletions(-) delete mode 100644 src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs index b7e2038b..047c801a 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs @@ -3,16 +3,16 @@ namespace LaunchDarkly.Xamarin.BackgroundAdapter { + // This is a stub implementation for .NET Standard where there's no such thing as backgrounding. + internal class BackgroundAdapter : IPlatformAdapter { public void Dispose() { - throw new NotImplementedException(); } public void EnableBackgrounding(IBackgroundingState backgroundingState) { - throw new NotImplementedException(); } } } diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs index 738f7738..d0fcf671 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs @@ -1,42 +1,34 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace LaunchDarkly.Xamarin.Connectivity { + // This code is not from Xamarin Essentials, though it implements the same Connectivity abstraction. + // It is a stub that always reports that we do have network connectivity. + // + // Unfortunately, in .NET Standard that is the best we can do. There is (at least in 2.0) a + // NetworkInterface.GetIsNetworkAvailable() method, but that doesn't test whether we actuually have + // Internet access, just whether we have a network interface (i.e. if we're running a desktop app + // on a laptop, and the wi-fi is turned off, it will still return true as long as the laptop has an + // Ethernet card-- even if it's not plugged in). + // + // So, in order to support connectivity detection on non-mobile platforms, we would need to add more + // platform-specific variants. For instance, here's how Xamarin Essentials does it for UWP: + // https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/Connectivity/Connectivity.uwp.cs + internal static partial class Connectivity { - static NetworkAccess PlatformNetworkAccess => - throw new NotImplementedException(); + static NetworkAccess PlatformNetworkAccess => NetworkAccess.Internet; - static IEnumerable PlatformConnectionProfiles => - throw new NotImplementedException(); + static IEnumerable PlatformConnectionProfiles + { + get + { + yield return ConnectionProfile.Unknown; + } + } - static void StartListeners() => - throw new NotImplementedException(); + static void StartListeners() { } - static void StopListeners() => - throw new NotImplementedException(); + static void StopListeners() { } } } diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs index 3282deeb..1889f9e4 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs @@ -82,7 +82,11 @@ static void OnConnectivityChanged(ConnectivityChangedEventArgs e) if (currentAccess != e.NetworkAccess || !currentProfiles.SequenceEqual(e.ConnectionProfiles)) { SetCurrent(); - MainThread.MainThread.BeginInvokeOnMainThread(() => ConnectivityChangedInternal?.Invoke(null, e)); + // Modified: Xamarin Essentials did this to guarantee that event handlers would always fire on the + // main thread, but that is not how event handlers normally work in .NET, and our SDK does not have + // any such requirement. + // MainThread.MainThread.BeginInvokeOnMainThread(() => ConnectivityChangedInternal?.Invoke(null, e)); + ConnectivityChangedInternal?.Invoke(null, e); } } } diff --git a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs deleted file mode 100644 index 1162805d..00000000 --- a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.android.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using Android.OS; - -namespace LaunchDarkly.Xamarin.MainThread -{ - internal static partial class MainThread - { - static Handler handler; - - static bool PlatformIsMainThread - { - get - { - if (Platform.Platform.HasApiLevel(BuildVersionCodes.M)) - return Looper.MainLooper.IsCurrentThread; - - return Looper.MyLooper() == Looper.MainLooper; - } - } - - static void PlatformBeginInvokeOnMainThread(Action action) - { - if (handler?.Looper != Looper.MainLooper) - handler = new Handler(Looper.MainLooper); - - handler.Post(action); - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs deleted file mode 100644 index 3ae74c81..00000000 --- a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.ios.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using Foundation; - -namespace LaunchDarkly.Xamarin.MainThread -{ - internal static partial class MainThread - { - static bool PlatformIsMainThread => - NSThread.Current.IsMainThread; - - static void PlatformBeginInvokeOnMainThread(Action action) - { - NSRunLoop.Main.BeginInvokeOnMainThread(action.Invoke); - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs deleted file mode 100644 index 1a80cc56..00000000 --- a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.netstandard.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; - -namespace LaunchDarkly.Xamarin.MainThread -{ - internal static partial class MainThread - { - static void PlatformBeginInvokeOnMainThread(Action action) => - throw new NotImplementedException(); - - static bool PlatformIsMainThread => - throw new NotImplementedException(); - } -} diff --git a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs b/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs deleted file mode 100644 index 021164e2..00000000 --- a/src/LaunchDarkly.XamarinSdk/MainThread/MainThread.shared.cs +++ /dev/null @@ -1,100 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.MainThread -{ - internal static partial class MainThread - { - public static bool IsMainThread => - PlatformIsMainThread; - - public static void BeginInvokeOnMainThread(Action action) - { - if (IsMainThread) - { - action(); - } - else - { - PlatformBeginInvokeOnMainThread(action); - } - } - - internal static Task InvokeOnMainThread(Action action) - { - if (IsMainThread) - { - action(); -#if NETSTANDARD1_0 - return Task.FromResult(true); -#else - return Task.CompletedTask; -#endif - } - - var tcs = new TaskCompletionSource(); - - BeginInvokeOnMainThread(() => - { - try - { - action(); - tcs.TrySetResult(true); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }); - - return tcs.Task; - } - - internal static Task InvokeOnMainThread(Func action) - { - if (IsMainThread) - { - return Task.FromResult(action()); - } - - var tcs = new TaskCompletionSource(); - - BeginInvokeOnMainThread(() => - { - try - { - var result = action(); - tcs.TrySetResult(result); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }); - - return tcs.Task; - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs index c3c835c2..cf2bb06c 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs @@ -9,12 +9,7 @@ internal class MobileConnectionManager : IConnectionManager internal MobileConnectionManager() { UpdateConnectedStatus(); - try - { - LaunchDarkly.Xamarin.Connectivity.Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; - } - catch (NotImplementedException) - { } + LaunchDarkly.Xamarin.Connectivity.Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; } bool isConnected; @@ -35,15 +30,7 @@ void Connectivity_ConnectivityChanged(object sender, Connectivity.ConnectivityCh private void UpdateConnectedStatus() { - try - { - isConnected = LaunchDarkly.Xamarin.Connectivity.Connectivity.NetworkAccess == LaunchDarkly.Xamarin.Connectivity.NetworkAccess.Internet; - } - catch (NotImplementedException) - { - // .NET Standard has no way to detect network connectivity - isConnected = true; - } + isConnected = LaunchDarkly.Xamarin.Connectivity.Connectivity.NetworkAccess == LaunchDarkly.Xamarin.Connectivity.NetworkAccess.Internet; } } } diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs index 17499e08..ab7a9bc8 100644 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs +++ b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs @@ -32,15 +32,20 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Permissions { + // All commented-out code in this file came from Xamarin Essentials but was removed because it is not used by this SDK. + // Note that we are no longer using the shared Permissions abstraction at all; this Android code is being used directly + // by other Android-specific code. + internal static partial class Permissions { - static readonly object locker = new object(); - static int requestCode = 0; + //static readonly object locker = new object(); + //static int requestCode = 0; - static Dictionary tcs)> requests = - new Dictionary)>(); + //static Dictionary tcs)> requests = + //new Dictionary)>(); - static void PlatformEnsureDeclared(PermissionType permission) + //static void PlatformEnsureDeclared(PermissionType permission) + internal static void EnsureDeclared(PermissionType permission) { var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: false); @@ -61,101 +66,101 @@ static void PlatformEnsureDeclared(PermissionType permission) } } - static Task PlatformCheckStatusAsync(PermissionType permission) - { - EnsureDeclared(permission); - - // If there are no android permissions for the given permission type - // just return granted since we have none to ask for - var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true); - - if (androidPermissions == null || !androidPermissions.Any()) - return Task.FromResult(PermissionStatus.Granted); - - var context = Platform.Platform.AppContext; - var targetsMOrHigher = context.ApplicationInfo.TargetSdkVersion >= BuildVersionCodes.M; - - foreach (var ap in androidPermissions) - { - if (targetsMOrHigher) - { - if (ContextCompat.CheckSelfPermission(context, ap) != Permission.Granted) - return Task.FromResult(PermissionStatus.Denied); - } - else - { - if (PermissionChecker.CheckSelfPermission(context, ap) != PermissionChecker.PermissionGranted) - return Task.FromResult(PermissionStatus.Denied); - } - } - - return Task.FromResult(PermissionStatus.Granted); - } - - static async Task PlatformRequestAsync(PermissionType permission) - { - // Check status before requesting first - if (await PlatformCheckStatusAsync(permission) == PermissionStatus.Granted) - return PermissionStatus.Granted; - - TaskCompletionSource tcs; - var doRequest = true; - - lock (locker) - { - if (requests.ContainsKey(permission)) - { - tcs = requests[permission].tcs; - doRequest = false; - } - else - { - tcs = new TaskCompletionSource(); - - // Get new request code and wrap it around for next use if it's going to reach max - if (++requestCode >= int.MaxValue) - requestCode = 1; - - requests.Add(permission, (requestCode, tcs)); - } - } - - if (!doRequest) - return await tcs.Task; - - if (!MainThread.MainThread.IsMainThread) - throw new System.UnauthorizedAccessException("Permission request must be invoked on main thread."); - - var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true).ToArray(); - - ActivityCompat.RequestPermissions(Platform.Platform.GetCurrentActivity(true), androidPermissions, requestCode); - - return await tcs.Task; - } - - internal static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) - { - lock (locker) - { - // Check our pending requests for one with a matching request code - foreach (var kvp in requests) - { - if (kvp.Value.requestCode == requestCode) - { - var tcs = kvp.Value.tcs; - - // Look for any denied requests, and deny the whole request if so - // Remember, each PermissionType is tied to 1 or more android permissions - // so if any android permissions denied the whole PermissionType is considered denied - if (grantResults.Any(g => g == Permission.Denied)) - tcs.TrySetResult(PermissionStatus.Denied); - else - tcs.TrySetResult(PermissionStatus.Granted); - break; - } - } - } - } + //static Task PlatformCheckStatusAsync(PermissionType permission) + //{ + // EnsureDeclared(permission); + + // // If there are no android permissions for the given permission type + // // just return granted since we have none to ask for + // var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true); + + // if (androidPermissions == null || !androidPermissions.Any()) + // return Task.FromResult(PermissionStatus.Granted); + + // var context = Platform.Platform.AppContext; + // var targetsMOrHigher = context.ApplicationInfo.TargetSdkVersion >= BuildVersionCodes.M; + + // foreach (var ap in androidPermissions) + // { + // if (targetsMOrHigher) + // { + // if (ContextCompat.CheckSelfPermission(context, ap) != Permission.Granted) + // return Task.FromResult(PermissionStatus.Denied); + // } + // else + // { + // if (PermissionChecker.CheckSelfPermission(context, ap) != PermissionChecker.PermissionGranted) + // return Task.FromResult(PermissionStatus.Denied); + // } + // } + + // return Task.FromResult(PermissionStatus.Granted); + //} + + //static async Task PlatformRequestAsync(PermissionType permission) + //{ + // // Check status before requesting first + // if (await PlatformCheckStatusAsync(permission) == PermissionStatus.Granted) + // return PermissionStatus.Granted; + + // TaskCompletionSource tcs; + // var doRequest = true; + + // lock (locker) + // { + // if (requests.ContainsKey(permission)) + // { + // tcs = requests[permission].tcs; + // doRequest = false; + // } + // else + // { + // tcs = new TaskCompletionSource(); + + // // Get new request code and wrap it around for next use if it's going to reach max + // if (++requestCode >= int.MaxValue) + // requestCode = 1; + + // requests.Add(permission, (requestCode, tcs)); + // } + // } + + // if (!doRequest) + // return await tcs.Task; + + // if (!MainThread.MainThread.IsMainThread) + // throw new System.UnauthorizedAccessException("Permission request must be invoked on main thread."); + + // var androidPermissions = permission.ToAndroidPermissions(onlyRuntimePermissions: true).ToArray(); + + // ActivityCompat.RequestPermissions(Platform.Platform.GetCurrentActivity(true), androidPermissions, requestCode); + + // return await tcs.Task; + //} + + //internal static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + //{ + // lock (locker) + // { + // // Check our pending requests for one with a matching request code + // foreach (var kvp in requests) + // { + // if (kvp.Value.requestCode == requestCode) + // { + // var tcs = kvp.Value.tcs; + + // // Look for any denied requests, and deny the whole request if so + // // Remember, each PermissionType is tied to 1 or more android permissions + // // so if any android permissions denied the whole PermissionType is considered denied + // if (grantResults.Any(g => g == Permission.Denied)) + // tcs.TrySetResult(PermissionStatus.Denied); + // else + // tcs.TrySetResult(PermissionStatus.Granted); + // break; + // } + // } + // } + //} } internal static class PermissionTypeExtensions diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs deleted file mode 100644 index 1d2807b1..00000000 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.ios.cs +++ /dev/null @@ -1,124 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System.Threading.Tasks; -using System; -using CoreLocation; -using Foundation; - -namespace LaunchDarkly.Xamarin.Permissions -{ - internal static partial class Permissions - { - static void PlatformEnsureDeclared(PermissionType permission) - { - var info = NSBundle.MainBundle.InfoDictionary; - - if (permission == PermissionType.LocationWhenInUse) - { - if (!info.ContainsKey(new NSString("NSLocationWhenInUseUsageDescription"))) - throw new UnauthorizedAccessException("You must set `NSLocationWhenInUseUsageDescription` in your Info.plist file to enable Authorization Requests for Location updates."); - } - } - - static Task PlatformCheckStatusAsync(PermissionType permission) - { - EnsureDeclared(permission); - - switch (permission) - { - case PermissionType.LocationWhenInUse: - return Task.FromResult(GetLocationStatus()); - } - - return Task.FromResult(PermissionStatus.Granted); - } - - static async Task PlatformRequestAsync(PermissionType permission) - { - // Check status before requesting first and only request if Unknown - var status = await PlatformCheckStatusAsync(permission); - if (status != PermissionStatus.Unknown) - return status; - - EnsureDeclared(permission); - - switch (permission) - { - case PermissionType.LocationWhenInUse: - - if (!MainThread.MainThread.IsMainThread) - throw new UnauthorizedAccessException("Permission request must be invoked on main thread."); - - return await RequestLocationAsync(); - default: - return PermissionStatus.Granted; - } - } - - static PermissionStatus GetLocationStatus() - { - if (!CLLocationManager.LocationServicesEnabled) - return PermissionStatus.Disabled; - - var status = CLLocationManager.Status; - - switch (status) - { - case CLAuthorizationStatus.AuthorizedAlways: - case CLAuthorizationStatus.AuthorizedWhenInUse: - return PermissionStatus.Granted; - case CLAuthorizationStatus.Denied: - return PermissionStatus.Denied; - case CLAuthorizationStatus.Restricted: - return PermissionStatus.Restricted; - default: - return PermissionStatus.Unknown; - } - } - - static CLLocationManager locationManager; - - static Task RequestLocationAsync() - { - locationManager = new CLLocationManager(); - - var tcs = new TaskCompletionSource(locationManager); - - locationManager.AuthorizationChanged += LocationAuthCallback; - locationManager.RequestWhenInUseAuthorization(); - - return tcs.Task; - - void LocationAuthCallback(object sender, CLAuthorizationChangedEventArgs e) - { - if (e.Status == CLAuthorizationStatus.NotDetermined) - return; - - locationManager.AuthorizationChanged -= LocationAuthCallback; - tcs.TrySetResult(GetLocationStatus()); - locationManager.Dispose(); - locationManager = null; - } - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs deleted file mode 100644 index 2495b379..00000000 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.netstandard.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Permissions -{ - internal static partial class Permissions - { - static void PlatformEnsureDeclared(PermissionType permission) => - throw new NotImplementedException(); - - static Task PlatformCheckStatusAsync(PermissionType permission) => - throw new NotImplementedException(); - - static Task PlatformRequestAsync(PermissionType permission) => - throw new NotImplementedException(); - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs b/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs deleted file mode 100644 index f7d28c98..00000000 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Permissions -{ - internal static partial class Permissions - { - internal static void EnsureDeclared(PermissionType permission) => - PlatformEnsureDeclared(permission); - - internal static Task CheckStatusAsync(PermissionType permission) => - PlatformCheckStatusAsync(permission); - - internal static Task RequestAsync(PermissionType permission) => - PlatformRequestAsync(permission); - - internal static async Task RequireAsync(PermissionType permission) - { - if (await RequestAsync(permission) != PermissionStatus.Granted) - throw new System.UnauthorizedAccessException($"{permission} was not granted."); - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs b/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs index 435768ac..8ea21d35 100644 --- a/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs +++ b/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs @@ -32,146 +32,148 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using Android.Net.Wifi; using Android.OS; +// All commented-out code in this file came from Xamarin Essentials but was removed because it is not used by this SDK. + namespace LaunchDarkly.Xamarin.Platform { internal static partial class Platform { - static ActivityLifecycleContextListener lifecycleListener; + //static ActivityLifecycleContextListener lifecycleListener; internal static Context AppContext => Application.Context; - internal static Activity GetCurrentActivity(bool throwOnNull) - { - var activity = lifecycleListener?.Activity; - if (throwOnNull && activity == null) - throw new NullReferenceException("The current Activity can not be detected. Ensure that you have called Init in your Activity or Application class."); - - return activity; - } - - public static void Init(Application application) - { - lifecycleListener = new ActivityLifecycleContextListener(); - application.RegisterActivityLifecycleCallbacks(lifecycleListener); - } - - public static void Init(Activity activity, Bundle bundle) - { - Init(activity.Application); - lifecycleListener.Activity = activity; - } - - public static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) => - Permissions.Permissions.OnRequestPermissionsResult(requestCode, permissions, grantResults); - - internal static bool HasSystemFeature(string systemFeature) - { - var packageManager = AppContext.PackageManager; - foreach (var feature in packageManager.GetSystemAvailableFeatures()) - { - if (feature.Name.Equals(systemFeature, StringComparison.OrdinalIgnoreCase)) - return true; - } - return false; - } - - internal static bool IsIntentSupported(Intent intent) - { - var manager = AppContext.PackageManager; - var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly); - return activities.Any(); - } + //internal static Activity GetCurrentActivity(bool throwOnNull) + //{ + // var activity = lifecycleListener?.Activity; + // if (throwOnNull && activity == null) + // throw new NullReferenceException("The current Activity can not be detected. Ensure that you have called Init in your Activity or Application class."); + + // return activity; + //} + + //public static void Init(Application application) + //{ + // lifecycleListener = new ActivityLifecycleContextListener(); + // application.RegisterActivityLifecycleCallbacks(lifecycleListener); + //} + + //public static void Init(Activity activity, Bundle bundle) + //{ + // Init(activity.Application); + // lifecycleListener.Activity = activity; + //} + + //public static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) => + // Permissions.Permissions.OnRequestPermissionsResult(requestCode, permissions, grantResults); + + //internal static bool HasSystemFeature(string systemFeature) + //{ + // var packageManager = AppContext.PackageManager; + // foreach (var feature in packageManager.GetSystemAvailableFeatures()) + // { + // if (feature.Name.Equals(systemFeature, StringComparison.OrdinalIgnoreCase)) + // return true; + // } + // return false; + //} + + //internal static bool IsIntentSupported(Intent intent) + //{ + // var manager = AppContext.PackageManager; + // var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly); + // return activities.Any(); + //} internal static bool HasApiLevel(BuildVersionCodes versionCode) => (int)Build.VERSION.SdkInt >= (int)versionCode; - internal static CameraManager CameraManager => - AppContext.GetSystemService(Context.CameraService) as CameraManager; + //internal static CameraManager CameraManager => + // AppContext.GetSystemService(Context.CameraService) as CameraManager; internal static ConnectivityManager ConnectivityManager => AppContext.GetSystemService(Context.ConnectivityService) as ConnectivityManager; - internal static Vibrator Vibrator => - AppContext.GetSystemService(Context.VibratorService) as Vibrator; + //internal static Vibrator Vibrator => + // AppContext.GetSystemService(Context.VibratorService) as Vibrator; - internal static WifiManager WifiManager => - AppContext.GetSystemService(Context.WifiService) as WifiManager; + //internal static WifiManager WifiManager => + // AppContext.GetSystemService(Context.WifiService) as WifiManager; - internal static SensorManager SensorManager => - AppContext.GetSystemService(Context.SensorService) as SensorManager; + //internal static SensorManager SensorManager => + // AppContext.GetSystemService(Context.SensorService) as SensorManager; - internal static ClipboardManager ClipboardManager => - AppContext.GetSystemService(Context.ClipboardService) as ClipboardManager; + //internal static ClipboardManager ClipboardManager => + // AppContext.GetSystemService(Context.ClipboardService) as ClipboardManager; - internal static LocationManager LocationManager => - AppContext.GetSystemService(Context.LocationService) as LocationManager; + //internal static LocationManager LocationManager => + // AppContext.GetSystemService(Context.LocationService) as LocationManager; - internal static PowerManager PowerManager => - AppContext.GetSystemService(Context.PowerService) as PowerManager; + //internal static PowerManager PowerManager => + //AppContext.GetSystemService(Context.PowerService) as PowerManager; - internal static Java.Util.Locale GetLocale() - { - var resources = AppContext.Resources; - var config = resources.Configuration; - if (HasApiLevel(BuildVersionCodes.N)) - return config.Locales.Get(0); + //internal static Java.Util.Locale GetLocale() + //{ + // var resources = AppContext.Resources; + // var config = resources.Configuration; + // if (HasApiLevel(BuildVersionCodes.N)) + // return config.Locales.Get(0); - return config.Locale; - } + // return config.Locale; + //} - internal static void SetLocale(Java.Util.Locale locale) - { - Java.Util.Locale.Default = locale; - var resources = AppContext.Resources; - var config = resources.Configuration; - if (HasApiLevel(BuildVersionCodes.N)) - config.SetLocale(locale); - else - config.Locale = locale; +// internal static void SetLocale(Java.Util.Locale locale) +// { +// Java.Util.Locale.Default = locale; +// var resources = AppContext.Resources; +// var config = resources.Configuration; +// if (HasApiLevel(BuildVersionCodes.N)) +// config.SetLocale(locale); +// else +// config.Locale = locale; -#pragma warning disable CS0618 // Type or member is obsolete - resources.UpdateConfiguration(config, resources.DisplayMetrics); -#pragma warning restore CS0618 // Type or member is obsolete - } +//#pragma warning disable CS0618 // Type or member is obsolete +// resources.UpdateConfiguration(config, resources.DisplayMetrics); +//#pragma warning restore CS0618 // Type or member is obsolete + //} } - internal class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks - { - WeakReference currentActivity = new WeakReference(null); + //internal class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks + //{ + // WeakReference currentActivity = new WeakReference(null); - internal Context Context => - Activity ?? Application.Context; + // internal Context Context => + // Activity ?? Application.Context; - internal Activity Activity - { - get => currentActivity.TryGetTarget(out var a) ? a : null; - set => currentActivity.SetTarget(value); - } + // internal Activity Activity + // { + // get => currentActivity.TryGetTarget(out var a) ? a : null; + // set => currentActivity.SetTarget(value); + // } - void Application.IActivityLifecycleCallbacks.OnActivityCreated(Activity activity, Bundle savedInstanceState) => - Activity = activity; + // void Application.IActivityLifecycleCallbacks.OnActivityCreated(Activity activity, Bundle savedInstanceState) => + // Activity = activity; - void Application.IActivityLifecycleCallbacks.OnActivityDestroyed(Activity activity) - { - } + // void Application.IActivityLifecycleCallbacks.OnActivityDestroyed(Activity activity) + // { + // } - void Application.IActivityLifecycleCallbacks.OnActivityPaused(Activity activity) => - Activity = activity; + // void Application.IActivityLifecycleCallbacks.OnActivityPaused(Activity activity) => + // Activity = activity; - void Application.IActivityLifecycleCallbacks.OnActivityResumed(Activity activity) => - Activity = activity; + // void Application.IActivityLifecycleCallbacks.OnActivityResumed(Activity activity) => + // Activity = activity; - void Application.IActivityLifecycleCallbacks.OnActivitySaveInstanceState(Activity activity, Bundle outState) - { - } + // void Application.IActivityLifecycleCallbacks.OnActivitySaveInstanceState(Activity activity, Bundle outState) + // { + // } - void Application.IActivityLifecycleCallbacks.OnActivityStarted(Activity activity) - { - } + // void Application.IActivityLifecycleCallbacks.OnActivityStarted(Activity activity) + // { + // } - void Application.IActivityLifecycleCallbacks.OnActivityStopped(Activity activity) - { - } - } + // void Application.IActivityLifecycleCallbacks.OnActivityStopped(Activity activity) + // { + // } + //} } diff --git a/src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs b/src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs deleted file mode 100644 index b416338f..00000000 --- a/src/LaunchDarkly.XamarinSdk/Platform/Platform.ios.cs +++ /dev/null @@ -1,104 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Linq; -using System.Runtime.InteropServices; -using CoreMotion; -using Foundation; -using ObjCRuntime; -using UIKit; - -namespace LaunchDarkly.Xamarin.Platform -{ - internal static partial class Platform - { - [DllImport(ObjCRuntime.Constants.SystemLibrary, EntryPoint = "sysctlbyname")] - internal static extern int SysctlByName([MarshalAs(UnmanagedType.LPStr)] string property, IntPtr output, IntPtr oldLen, IntPtr newp, uint newlen); - - internal static string GetSystemLibraryProperty(string property) - { - var lengthPtr = Marshal.AllocHGlobal(sizeof(int)); - SysctlByName(property, IntPtr.Zero, lengthPtr, IntPtr.Zero, 0); - - var propertyLength = Marshal.ReadInt32(lengthPtr); - - if (propertyLength == 0) - { - Marshal.FreeHGlobal(lengthPtr); - throw new InvalidOperationException("Unable to read length of property."); - } - - var valuePtr = Marshal.AllocHGlobal(propertyLength); - SysctlByName(property, valuePtr, lengthPtr, IntPtr.Zero, 0); - - var returnValue = Marshal.PtrToStringAnsi(valuePtr); - - Marshal.FreeHGlobal(lengthPtr); - Marshal.FreeHGlobal(valuePtr); - - return returnValue; - } - - internal static bool HasOSVersion(int major, int minor) => - UIDevice.CurrentDevice.CheckSystemVersion(major, minor); - - internal static UIViewController GetCurrentViewController(bool throwIfNull = true) - { - UIViewController viewController = null; - - var window = UIApplication.SharedApplication.KeyWindow; - - if (window.WindowLevel == UIWindowLevel.Normal) - viewController = window.RootViewController; - - if (viewController == null) - { - window = UIApplication.SharedApplication - .Windows - .OrderByDescending(w => w.WindowLevel) - .FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal); - - if (window == null) - throw new InvalidOperationException("Could not find current view controller."); - else - viewController = window.RootViewController; - } - - while (viewController.PresentedViewController != null) - viewController = viewController.PresentedViewController; - - if (throwIfNull && viewController == null) - throw new InvalidOperationException("Could not find current view controller."); - - return viewController; - } - - static CMMotionManager motionManager; - - internal static CMMotionManager MotionManager => - motionManager ?? (motionManager = new CMMotionManager()); - - internal static NSOperationQueue GetCurrentQueue() => - NSOperationQueue.CurrentQueue ?? new NSOperationQueue(); - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs index 6aa16790..ae33089d 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs @@ -1,44 +1,20 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; +using System; namespace LaunchDarkly.Xamarin.Preferences { + // This code is not from Xamarin Essentials, though it implements the same Preferences abstraction. + // It is a stub with no underlying data store. + internal static partial class Preferences { - static bool PlatformContainsKey(string key, string sharedName) => - throw new NotImplementedException(); + static bool PlatformContainsKey(string key, string sharedName) => false; - static void PlatformRemove(string key, string sharedName) => - throw new NotImplementedException(); + static void PlatformRemove(string key, string sharedName) { } - static void PlatformClear(string sharedName) => - throw new NotImplementedException(); + static void PlatformClear(string sharedName) { } - static void PlatformSet(string key, T value, string sharedName) => - throw new NotImplementedException(); + static void PlatformSet(string key, T value, string sharedName) { } - static T PlatformGet(string key, T defaultValue, string sharedName) => - throw new NotImplementedException(); + static T PlatformGet(string key, T defaultValue, string sharedName) => defaultValue; } } diff --git a/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs index 5809d1b3..e373d732 100644 --- a/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs @@ -6,23 +6,12 @@ internal class SimpleMobileDevicePersistance : ISimplePersistance { public void Save(string key, string value) { - try - { - LaunchDarkly.Xamarin.Preferences.Preferences.Set(key, value); - } - catch (NotImplementedException) { } + LaunchDarkly.Xamarin.Preferences.Preferences.Set(key, value); } public string GetValue(string key) { - try - { - return LaunchDarkly.Xamarin.Preferences.Preferences.Get(key, null); - } - catch (NotImplementedException) - { - return null; - } + return LaunchDarkly.Xamarin.Preferences.Preferences.Get(key, null); } } } diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs index e27ac884..a992e4d0 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs +++ b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs @@ -4,14 +4,10 @@ namespace LaunchDarkly.Xamarin { internal static partial class UserMetadata { - private static string GetDevice() - { - return Build.Model + " " + Build.Product; - } + private static string PlatformDevice => + Build.Model + " " + Build.Product; - private static string GetOS() - { - return "Android " + Build.VERSION.SdkInt; - } + private static string PlatformOS => + "Android " + Build.VERSION.SdkInt; } } diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs index da7f53e3..6f66f57f 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs @@ -4,26 +4,27 @@ namespace LaunchDarkly.Xamarin { internal static partial class UserMetadata { - private static string GetDevice() + private static string PlatformDevice { - switch (UIDevice.CurrentDevice.UserInterfaceIdiom) + get { - case UIUserInterfaceIdiom.CarPlay: - return "CarPlay"; - case UIUserInterfaceIdiom.Pad: - return "iPad"; - case UIUserInterfaceIdiom.Phone: - return "iPhone"; - case UIUserInterfaceIdiom.TV: - return "Apple TV"; - default: - return "unknown"; + switch (UIDevice.CurrentDevice.UserInterfaceIdiom) + { + case UIUserInterfaceIdiom.CarPlay: + return "CarPlay"; + case UIUserInterfaceIdiom.Pad: + return "iPad"; + case UIUserInterfaceIdiom.Phone: + return "iPhone"; + case UIUserInterfaceIdiom.TV: + return "Apple TV"; + default: + return "unknown"; + } } } - private static string GetOS() - { - return "iOS " + UIDevice.CurrentDevice.SystemVersion; - } + private static string PlatformOS => + "iOS " + UIDevice.CurrentDevice.SystemVersion; } } diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs index 9ef808d8..013ebf0e 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs @@ -3,14 +3,8 @@ namespace LaunchDarkly.Xamarin { internal static partial class UserMetadata { - private static string GetDevice() - { - return null; - } - - private static string GetOS() - { - return null; - } + private static string PlatformDevice => null; + + private static string PlatformOS => null; } } diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs index ba4e7d4e..3c769c16 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs @@ -1,10 +1,14 @@  namespace LaunchDarkly.Xamarin { + // This class and the rest of its partial class implementations are not derived from Xamarin Essentials. + internal static partial class UserMetadata { - private static readonly string _os = GetOS(); - private static readonly string _device = GetDevice(); + // These values are obtained from the platform-specific code once and then stored in static fields, + // to avoid having to recompute them many times. + private static readonly string _os = PlatformOS; + private static readonly string _device = PlatformDevice; /// /// Returns the string that should be passed in the "device" property for all users. From b3fdb736cea4bcc5b469a1abc321332f3008f926 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 4 Jun 2019 14:09:23 -0700 Subject: [PATCH 080/254] .NET Standard 2.0 implementation of persistent storage --- .../Preferences/Preferences.android.cs | 77 ++--------- .../Preferences/Preferences.ios.cs | 64 ++------- .../Preferences/Preferences.netstandard.cs | 129 +++++++++++++++++- .../Preferences/Preferences.shared.cs | 104 +++++++------- 4 files changed, 195 insertions(+), 179 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs index 381029fb..1e07aec0 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs @@ -28,6 +28,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { + // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class + // to store them. Therefore, the overloads for non-string types have been removed, thereby + // reducing the amount of multi-platform implementation code that won't be used. + internal static partial class Preferences { static readonly object locker = new object(); @@ -67,7 +71,7 @@ static void PlatformClear(string sharedName) } } - static void PlatformSet(string key, T value, string sharedName) + static void PlatformSet(string key, string value, string sharedName) { lock (locker) { @@ -80,87 +84,22 @@ static void PlatformSet(string key, T value, string sharedName) } else { - switch (value) - { - case string s: - editor.PutString(key, s); - break; - case int i: - editor.PutInt(key, i); - break; - case bool b: - editor.PutBoolean(key, b); - break; - case long l: - editor.PutLong(key, l); - break; - case double d: - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - editor.PutString(key, valueString); - break; - case float f: - editor.PutFloat(key, f); - break; - } + editor.PutString(key, s); } editor.Apply(); } } } - static T PlatformGet(string key, T defaultValue, string sharedName) + static string PlatformGet(string key, string defaultValue, string sharedName) { lock (locker) { object value = null; using (var sharedPreferences = GetSharedPreferences(sharedName)) { - if (defaultValue == null) - { - value = sharedPreferences.GetString(key, null); - } - else - { - switch (defaultValue) - { - case int i: - value = sharedPreferences.GetInt(key, i); - break; - case bool b: - value = sharedPreferences.GetBoolean(key, b); - break; - case long l: - value = sharedPreferences.GetLong(key, l); - break; - case double d: - var savedDouble = sharedPreferences.GetString(key, null); - if (string.IsNullOrWhiteSpace(savedDouble)) - { - value = defaultValue; - } - else - { - if (!double.TryParse(savedDouble, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var outDouble)) - { - var maxString = Convert.ToString(double.MaxValue, CultureInfo.InvariantCulture); - outDouble = savedDouble.Equals(maxString) ? double.MaxValue : double.MinValue; - } - - value = outDouble; - } - break; - case float f: - value = sharedPreferences.GetFloat(key, f); - break; - case string s: - // the case when the string is not null - value = sharedPreferences.GetString(key, s); - break; - } - } + return sharedPreferences.GetString(key, defaultValue); } - - return (T)value; } } diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs index 9497cd09..616cd03b 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs @@ -26,6 +26,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { + // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class + // to store them. Therefore, the overloads for non-string types have been removed, thereby + // reducing the amount of multi-platform implementation code that won't be used. + internal static partial class Preferences { static readonly object locker = new object(); @@ -67,7 +71,7 @@ static void PlatformClear(string sharedName) } } - static void PlatformSet(string key, T value, string sharedName) + static void PlatformSet(string key, string value, string sharedName) { lock (locker) { @@ -80,36 +84,13 @@ static void PlatformSet(string key, T value, string sharedName) return; } - switch (value) - { - case string s: - userDefaults.SetString(s, key); - break; - case int i: - userDefaults.SetInt(i, key); - break; - case bool b: - userDefaults.SetBool(b, key); - break; - case long l: - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - userDefaults.SetString(valueString, key); - break; - case double d: - userDefaults.SetDouble(d, key); - break; - case float f: - userDefaults.SetFloat(f, key); - break; - } + userDefaults.SetString(value, key); } } } - static T PlatformGet(string key, T defaultValue, string sharedName) + static string PlatformGet(string key, string defaultValue, string sharedName) { - object value = null; - lock (locker) { using (var userDefaults = GetUserDefaults(sharedName)) @@ -117,38 +98,9 @@ static T PlatformGet(string key, T defaultValue, string sharedName) if (userDefaults[key] == null) return defaultValue; - switch (defaultValue) - { - case int i: - value = (int)(nint)userDefaults.IntForKey(key); - break; - case bool b: - value = userDefaults.BoolForKey(key); - break; - case long l: - var savedLong = userDefaults.StringForKey(key); - value = Convert.ToInt64(savedLong, CultureInfo.InvariantCulture); - break; - case double d: - value = userDefaults.DoubleForKey(key); - break; - case float f: - value = userDefaults.FloatForKey(key); - break; - case string s: - // the case when the string is not null - value = userDefaults.StringForKey(key); - break; - default: - // the case when the string is null - if (typeof(T) == typeof(string)) - value = userDefaults.StringForKey(key); - break; - } + return userDefaults.StringForKey(key); } } - - return (T)value; } static NSUserDefaults GetUserDefaults(string sharedName) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs index ae33089d..0023d194 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs @@ -1,20 +1,141 @@ -using System; +#if NETSTANDARD1_6 +#else +using System.IO; +using System.IO.IsolatedStorage; +using System.Linq; +using System.Text; +#endif namespace LaunchDarkly.Xamarin.Preferences { // This code is not from Xamarin Essentials, though it implements the same Preferences abstraction. - // It is a stub with no underlying data store. + // + // In .NET Standard 2.0, we use the IsolatedStorage API to store per-user data. The .NET Standard implementation + // of IsolatedStorage puts these files under ~/.local/share/IsolatedStorage followed by a subpath of obfuscated + // strings that are apparently based on the application and assembly name, so the data should be specific to both + // the OS user account and the current app. + // + // In .NET Standard 1.6, there is no data store. internal static partial class Preferences { +#if NETSTANDARD1_6 static bool PlatformContainsKey(string key, string sharedName) => false; static void PlatformRemove(string key, string sharedName) { } static void PlatformClear(string sharedName) { } - static void PlatformSet(string key, T value, string sharedName) { } + static void PlatformSet(string key, string value, string sharedName) { } - static T PlatformGet(string key, T defaultValue, string sharedName) => defaultValue; + static string PlatformGet(string key, string defaultValue, string sharedName) => defaultValue; +#else + private const string ConfigDirectoryName = "LaunchDarkly"; + + // GetUserStoreForDomain returns a storage object that is specific to the current application and OS user. + private static IsolatedStorageFile Store => IsolatedStorageFile.GetUserStoreForDomain(); + + static bool PlatformContainsKey(string key, string sharedName) + { + return Store.FileExists(MakeFilePath(key, sharedName)); + } + + static void PlatformRemove(string key, string sharedName) + { + try + { + Store.DeleteFile(MakeFilePath(key, sharedName)); + } + catch (IsolatedStorageException) {} // Preferences implementations shouldn't throw exceptions except for a code error like a null reference + } + + static void PlatformClear(string sharedName) + { + try + { + Store.DeleteDirectory(MakeDirectoryPath(sharedName)); + } + catch (IsolatedStorageException) {} + } + + static void PlatformSet(string key, string value, string sharedName) + { + try + { + var path = MakeDirectoryPath(sharedName); + if (!Store.DirectoryExists(path)) + { + Store.CreateDirectory(path); + } + } + catch (IsolatedStorageException) {} + using (var stream = Store.OpenFile(MakeFilePath(key, sharedName), FileMode.Create, FileAccess.Write)) + { + using (var sw = new StreamWriter(stream)) + { + sw.Write(value); + } + } + } + + static string PlatformGet(string key, string defaultValue, string sharedName) + { + try + { + using (var stream = Store.OpenFile(MakeFilePath(key, sharedName), FileMode.Open)) + { + using (var sr = new StreamReader(stream)) + { + return sr.ReadToEnd(); + } + } + } + catch (IsolatedStorageException) {} + catch (DirectoryNotFoundException) {} + catch (FileNotFoundException) {} + return null; + } + + static string MakeDirectoryPath(string sharedName) + { + if (string.IsNullOrEmpty(sharedName)) + { + return ConfigDirectoryName; + } + return ConfigDirectoryName + "." + EscapeFilenameComponent(sharedName); + } + + static string MakeFilePath(string key, string sharedName) + { + return MakeDirectoryPath(sharedName) + "/" + EscapeFilenameComponent(key); + } + + static string EscapeFilenameComponent(string name) + { + // In actual usage for LaunchDarkly this should not be an issue, because keys are really feature flag keys + // which have a very limited character set, and we don't actually use sharedName. It's just good practice. + StringBuilder buf = null; + var badChars = Path.GetInvalidFileNameChars(); + const char escapeChar = '%'; + for (var i = 0; i < name.Length; i++) + { + var ch = name[i]; + if (badChars.Contains(ch) || ch == escapeChar) + { + if (buf == null) // create StringBuilder lazily since most names will be valid + { + buf = new StringBuilder(name.Length); + buf.Append(name.Substring(0, i)); + } + buf.Append(escapeChar).Append(((int)ch).ToString("X")); // hex value + } + else + { + buf?.Append(ch); + } + } + return buf == null ? name : buf.ToString(); + } +#endif } } diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs index 62997c12..ef221963 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs @@ -24,6 +24,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace LaunchDarkly.Xamarin.Preferences { + // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class + // to store them. Therefore, the overloads for non-string types have been removed, thereby + // reducing the amount of multi-platform implementation code that won't be used. + internal static partial class Preferences { internal static string GetPrivatePreferencesSharedName(string feature) => @@ -43,38 +47,38 @@ public static void Clear() => public static string Get(string key, string defaultValue) => Get(key, defaultValue, null); - public static bool Get(string key, bool defaultValue) => - Get(key, defaultValue, null); + //public static bool Get(string key, bool defaultValue) => + // Get(key, defaultValue, null); - public static int Get(string key, int defaultValue) => - Get(key, defaultValue, null); + //public static int Get(string key, int defaultValue) => + // Get(key, defaultValue, null); - public static double Get(string key, double defaultValue) => - Get(key, defaultValue, null); + //public static double Get(string key, double defaultValue) => + // Get(key, defaultValue, null); - public static float Get(string key, float defaultValue) => - Get(key, defaultValue, null); + //public static float Get(string key, float defaultValue) => + // Get(key, defaultValue, null); - public static long Get(string key, long defaultValue) => - Get(key, defaultValue, null); + //public static long Get(string key, long defaultValue) => + // Get(key, defaultValue, null); public static void Set(string key, string value) => Set(key, value, null); - public static void Set(string key, bool value) => - Set(key, value, null); + //public static void Set(string key, bool value) => + // Set(key, value, null); - public static void Set(string key, int value) => - Set(key, value, null); + //public static void Set(string key, int value) => + // Set(key, value, null); - public static void Set(string key, double value) => - Set(key, value, null); + //public static void Set(string key, double value) => + // Set(key, value, null); - public static void Set(string key, float value) => - Set(key, value, null); + //public static void Set(string key, float value) => + // Set(key, value, null); - public static void Set(string key, long value) => - Set(key, value, null); + //public static void Set(string key, long value) => + // Set(key, value, null); // shared -> platform @@ -88,53 +92,53 @@ public static void Clear(string sharedName) => PlatformClear(sharedName); public static string Get(string key, string defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + PlatformGet(key, defaultValue, sharedName); - public static bool Get(string key, bool defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + //public static bool Get(string key, bool defaultValue, string sharedName) => + // PlatformGet(key, defaultValue, sharedName); - public static int Get(string key, int defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + //public static int Get(string key, int defaultValue, string sharedName) => + // PlatformGet(key, defaultValue, sharedName); - public static double Get(string key, double defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + //public static double Get(string key, double defaultValue, string sharedName) => + // PlatformGet(key, defaultValue, sharedName); - public static float Get(string key, float defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + //public static float Get(string key, float defaultValue, string sharedName) => + // PlatformGet(key, defaultValue, sharedName); - public static long Get(string key, long defaultValue, string sharedName) => - PlatformGet(key, defaultValue, sharedName); + //public static long Get(string key, long defaultValue, string sharedName) => + // PlatformGet(key, defaultValue, sharedName); public static void Set(string key, string value, string sharedName) => - PlatformSet(key, value, sharedName); + PlatformSet(key, value, sharedName); - public static void Set(string key, bool value, string sharedName) => - PlatformSet(key, value, sharedName); + //public static void Set(string key, bool value, string sharedName) => + // PlatformSet(key, value, sharedName); - public static void Set(string key, int value, string sharedName) => - PlatformSet(key, value, sharedName); + //public static void Set(string key, int value, string sharedName) => + // PlatformSet(key, value, sharedName); - public static void Set(string key, double value, string sharedName) => - PlatformSet(key, value, sharedName); + //public static void Set(string key, double value, string sharedName) => + // PlatformSet(key, value, sharedName); - public static void Set(string key, float value, string sharedName) => - PlatformSet(key, value, sharedName); + //public static void Set(string key, float value, string sharedName) => + // PlatformSet(key, value, sharedName); - public static void Set(string key, long value, string sharedName) => - PlatformSet(key, value, sharedName); + //public static void Set(string key, long value, string sharedName) => + // PlatformSet(key, value, sharedName); // DateTime - public static DateTime Get(string key, DateTime defaultValue) => - Get(key, defaultValue, null); + //public static DateTime Get(string key, DateTime defaultValue) => + // Get(key, defaultValue, null); - public static void Set(string key, DateTime value) => - Set(key, value, null); + //public static void Set(string key, DateTime value) => + // Set(key, value, null); - public static DateTime Get(string key, DateTime defaultValue, string sharedName) => - DateTime.FromBinary(PlatformGet(key, defaultValue.ToBinary(), sharedName)); + //public static DateTime Get(string key, DateTime defaultValue, string sharedName) => + // DateTime.FromBinary(PlatformGet(key, defaultValue.ToBinary(), sharedName)); - public static void Set(string key, DateTime value, string sharedName) => - PlatformSet(key, value.ToBinary(), sharedName); + //public static void Set(string key, DateTime value, string sharedName) => + // PlatformSet(key, value.ToBinary(), sharedName); } } From eb01cde35db93d0dbe62313c79dd8671597ed93e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 4 Jun 2019 15:27:49 -0700 Subject: [PATCH 081/254] better error handling for persistent storage --- .../Preferences/Preferences.netstandard.cs | 128 +++++++++++++----- 1 file changed, 95 insertions(+), 33 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs index 0023d194..b46621c8 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs @@ -1,9 +1,12 @@ #if NETSTANDARD1_6 #else +using System; using System.IO; using System.IO.IsolatedStorage; using System.Linq; using System.Text; +using Common.Logging; +using LaunchDarkly.Common; #endif namespace LaunchDarkly.Xamarin.Preferences @@ -30,73 +33,132 @@ static void PlatformSet(string key, string value, string sharedName) { } static string PlatformGet(string key, string defaultValue, string sharedName) => defaultValue; #else - private const string ConfigDirectoryName = "LaunchDarkly"; + private static readonly ILog Log = LogManager.GetLogger(typeof(Preferences)); + + private static AtomicBoolean _loggedOSError = new AtomicBoolean(false); // AtomicBoolean is defined in LaunchDarkly.CommonSdk - // GetUserStoreForDomain returns a storage object that is specific to the current application and OS user. - private static IsolatedStorageFile Store => IsolatedStorageFile.GetUserStoreForDomain(); + private const string ConfigDirectoryName = "LaunchDarkly"; static bool PlatformContainsKey(string key, string sharedName) { - return Store.FileExists(MakeFilePath(key, sharedName)); + return WithStore(store => store.FileExists(MakeFilePath(key, sharedName))); } static void PlatformRemove(string key, string sharedName) { - try + WithStore(store => { - Store.DeleteFile(MakeFilePath(key, sharedName)); - } - catch (IsolatedStorageException) {} // Preferences implementations shouldn't throw exceptions except for a code error like a null reference + try + { + store.DeleteFile(MakeFilePath(key, sharedName)); + } + catch (IsolatedStorageException) { } // file didn't exist - that's OK + }); } static void PlatformClear(string sharedName) { - try + WithStore(store => { - Store.DeleteDirectory(MakeDirectoryPath(sharedName)); - } - catch (IsolatedStorageException) {} + try + { + store.DeleteDirectory(MakeDirectoryPath(sharedName)); + // The directory will be recreated next time PlatformSet is called with the same sharedName. + } + catch (IsolatedStorageException) { } // directory didn't exist - that's OK + }); } static void PlatformSet(string key, string value, string sharedName) { - try + WithStore(store => { var path = MakeDirectoryPath(sharedName); - if (!Store.DirectoryExists(path)) + store.CreateDirectory(path); // has no effect if directory already exists + using (var stream = store.OpenFile(MakeFilePath(key, sharedName), FileMode.Create, FileAccess.Write)) { - Store.CreateDirectory(path); + using (var sw = new StreamWriter(stream)) + { + sw.Write(value); + } } - } - catch (IsolatedStorageException) {} - using (var stream = Store.OpenFile(MakeFilePath(key, sharedName), FileMode.Create, FileAccess.Write)) + }); + } + + static string PlatformGet(string key, string defaultValue, string sharedName) + { + return WithStore(store => { - using (var sw = new StreamWriter(stream)) + try { - sw.Write(value); + using (var stream = store.OpenFile(MakeFilePath(key, sharedName), FileMode.Open)) + { + using (var sr = new StreamReader(stream)) + { + return sr.ReadToEnd(); + } + } } - } + catch (DirectoryNotFoundException) { } // just return null if no preferences have ever been set + catch (FileNotFoundException) { } // just return null if this preference was never set + return null; + }); } - static string PlatformGet(string key, string defaultValue, string sharedName) + private static T WithStore(Func callback) { + IsolatedStorageFile store; try { - using (var stream = Store.OpenFile(MakeFilePath(key, sharedName), FileMode.Open)) + // GetUserStoreForDomain returns a storage object that is specific to the current application and OS user. + store = IsolatedStorageFile.GetUserStoreForDomain(); + } + catch (Exception e) + { + HandleStoreException(e); + return default; + } + return callback(store); + } + + private static void HandleStoreException(Exception e) + { + if (e is IsolatedStorageException || + e is InvalidOperationException) + { + // These exceptions are ones that IsolatedStorageFile methods may throw under conditions that are + // unrelated to our code, e.g. filesystem permissions don't allow the store to be used. Since such a + // condition is unlikely to change during the application's lifetime, we only want to log it once. + // We won't log a stacktrace since it'll just point to somewhere in the standard library. + + // Note that we specifically catch IsolatedStorageException in a couple places above, when it would + // indicate a particular error condition that we want to handle differently. In all other cases it + // is unexpected and should be considered a platform/configuration issue. + + if (!_loggedOSError.GetAndSet(true)) { - using (var sr = new StreamReader(stream)) - { - return sr.ReadToEnd(); - } + Log.WarnFormat("Persistent storage is unavailable and has been disabled ({0}: {1})", e.GetType(), e.Message); } } - catch (IsolatedStorageException) {} - catch (DirectoryNotFoundException) {} - catch (FileNotFoundException) {} - return null; + else + { + // All other errors probably indicate an error in our own code. We don't want to throw these up + // into the SDK; the Preferences API is expected to either work or silently fail. + Log.ErrorFormat("Error in accessing persistent storage: {0}: {1}", e.GetType(), e.Message); + Log.Debug(e.StackTrace); + } + } + + private static void WithStore(Action callback) + { + WithStore(store => + { + callback(store); + return true; + }); } - static string MakeDirectoryPath(string sharedName) + private static string MakeDirectoryPath(string sharedName) { if (string.IsNullOrEmpty(sharedName)) { @@ -105,7 +167,7 @@ static string MakeDirectoryPath(string sharedName) return ConfigDirectoryName + "." + EscapeFilenameComponent(sharedName); } - static string MakeFilePath(string key, string sharedName) + private static string MakeFilePath(string key, string sharedName) { return MakeDirectoryPath(sharedName) + "/" + EscapeFilenameComponent(key); } From 2f8cdfb21311fdc46507ab3a93f4205b0ec3392a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 4 Jun 2019 15:41:08 -0700 Subject: [PATCH 082/254] fix try block --- .../Preferences/Preferences.netstandard.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs index b46621c8..5891f2d6 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs @@ -107,18 +107,17 @@ static string PlatformGet(string key, string defaultValue, string sharedName) private static T WithStore(Func callback) { - IsolatedStorageFile store; try { // GetUserStoreForDomain returns a storage object that is specific to the current application and OS user. - store = IsolatedStorageFile.GetUserStoreForDomain(); + var store = IsolatedStorageFile.GetUserStoreForDomain(); + return callback(store); } catch (Exception e) { HandleStoreException(e); return default; } - return callback(store); } private static void HandleStoreException(Exception e) @@ -172,7 +171,7 @@ private static string MakeFilePath(string key, string sharedName) return MakeDirectoryPath(sharedName) + "/" + EscapeFilenameComponent(key); } - static string EscapeFilenameComponent(string name) + private static string EscapeFilenameComponent(string name) { // In actual usage for LaunchDarkly this should not be an issue, because keys are really feature flag keys // which have a very limited character set, and we don't actually use sharedName. It's just good practice. From 2a49acb723e522b37fa8f40dbd33844a5b4d61b3 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 4 Jun 2019 16:01:03 -0700 Subject: [PATCH 083/254] comment --- .../Preferences/Preferences.netstandard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs index 5891f2d6..71fc87f0 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs @@ -18,6 +18,9 @@ namespace LaunchDarkly.Xamarin.Preferences // strings that are apparently based on the application and assembly name, so the data should be specific to both // the OS user account and the current app. // + // This is based on the Plugin.Settings plugin (which is what Xamarin Essentials uses for preferences), but greatly + // simplified since we only need one data type. See: https://github.com/jamesmontemagno/SettingsPlugin/blob/master/src/Plugin.Settings/Settings.dotnet.cs + // // In .NET Standard 1.6, there is no data store. internal static partial class Preferences From 37844f13c9ef3c2970f844349f534d2e0857b3d4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 14:55:57 -0700 Subject: [PATCH 084/254] simplify backgrounding implementation, cleanup async usage --- .../AsyncUtils.shared.cs | 73 +++++++++++ .../BackgroundAdapter.ios.cs | 59 --------- .../BackgroundAdapter.netstandard.cs | 18 --- .../BackgroundDetection.android.cs} | 47 ++------ .../BackgroundDetection.ios.cs | 37 ++++++ .../BackgroundDetection.netstandard.cs | 15 +++ .../BackgroundDetection.shared.cs | 58 +++++++++ .../IBackgroundingState.shared.cs | 28 ----- .../IPlatformAdapter.shared.cs | 37 ------ .../LdClient.shared.cs | 113 ++++-------------- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 2 +- 11 files changed, 221 insertions(+), 266 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs rename src/LaunchDarkly.XamarinSdk/{BackgroundAdapter/BackgroundAdapter.android.cs => BackgroundDetection/BackgroundDetection.android.cs} (50%) create mode 100644 src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs create mode 100644 src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs create mode 100644 src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs b/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs new file mode 100644 index 00000000..d76b72e7 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin +{ + internal static class AsyncUtils + { + private static readonly TaskFactory _taskFactory = new TaskFactory(CancellationToken.None, + TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); + + // This procedure for blocking on a Task without using Task.Wait is derived from the MIT-licensed ASP.NET + // code here: https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs + // In general, mixing sync and async code is not recommended, and if done in other ways can result in + // deadlocks. See: https://stackoverflow.com/questions/9343594/how-to-call-asynchronous-method-from-synchronous-method-in-c + // Task.Wait would only be safe if we could guarantee that every intermediate Task within the async + // code had been modified with ConfigureAwait(false), but that is very error-prone and we can't depend + // on feature store implementors doing so. + + internal static void WaitSafely(Func taskFn) + { + try + { + _taskFactory.StartNew(taskFn) + .Unwrap() + .GetAwaiter() + .GetResult(); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } + } + + internal static bool WaitSafely(Func taskFn, TimeSpan timeout) + { + try + { + return _taskFactory.StartNew(taskFn) + .Unwrap() + .Wait(timeout); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } + } + + internal static T WaitSafely(Func> taskFn) + { + try + { + return _taskFactory.StartNew(taskFn) + .Unwrap() + .GetAwaiter() + .GetResult(); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } + } + + private static Exception UnwrapAggregateException(AggregateException e) + { + if (e.InnerExceptions.Count == 1) + { + return e.InnerExceptions[0]; + } + return e; + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs deleted file mode 100644 index 457ae6f7..00000000 --- a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.ios.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using LaunchDarkly.Xamarin; -using UIKit; -using Common.Logging; -using Foundation; - -namespace LaunchDarkly.Xamarin.BackgroundAdapter -{ - internal class BackgroundAdapter : IPlatformAdapter - { - private IBackgroundingState _backgroundingState; - private NSObject _foregroundHandle; - private NSObject _backgroundHandle; - private static readonly ILog Log = LogManager.GetLogger(typeof(BackgroundAdapter)); - - public void EnableBackgrounding(IBackgroundingState backgroundingState) - { - _foregroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleWillEnterForeground); - _backgroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, HandleWillEnterBackground); - _backgroundingState = backgroundingState; - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected void _Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects). - } - - _backgroundingState = null; - _foregroundHandle = null; - - disposedValue = true; - } - } - - public void Dispose() - { - _Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - - private void HandleWillEnterForeground(NSNotification notification) - { - _backgroundingState.ExitBackgroundAsync(); - } - - private void HandleWillEnterBackground(NSNotification notification) - { - _backgroundingState.EnterBackgroundAsync(); - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs b/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs deleted file mode 100644 index 047c801a..00000000 --- a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.netstandard.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using LaunchDarkly.Xamarin; - -namespace LaunchDarkly.Xamarin.BackgroundAdapter -{ - // This is a stub implementation for .NET Standard where there's no such thing as backgrounding. - - internal class BackgroundAdapter : IPlatformAdapter - { - public void Dispose() - { - } - - public void EnableBackgrounding(IBackgroundingState backgroundingState) - { - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.android.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs similarity index 50% rename from src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.android.cs rename to src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs index ab7853f0..096e0ed4 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundAdapter/BackgroundAdapter.android.cs +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs @@ -3,48 +3,25 @@ using Android.App; using Android.OS; -namespace LaunchDarkly.Xamarin.BackgroundAdapter +namespace LaunchDarkly.Xamarin.BackgroundDetection { - internal class BackgroundAdapter : IPlatformAdapter + internal static partial class BackgroundDetection { private static ActivityLifecycleCallbacks _callbacks; - private Application application; + private static Application _application; - public void EnableBackgrounding(IBackgroundingState backgroundingState) + private static void StartListening() { - if (_callbacks == null) - { - _callbacks = new ActivityLifecycleCallbacks(backgroundingState); - application = (Application)Application.Context; - application.RegisterActivityLifecycleCallbacks(_callbacks); - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void _Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects). - } - - application = null; - _callbacks = null; - - disposedValue = true; - } + _callbacks = new ActivityLifecycleCallbacks(); + _application = (Application)Application.Context; + _application.RegisterActivityLifecycleCallbacks(_callbacks); } - public void Dispose() + private static void StopListening() { - _Dispose(true); - GC.SuppressFinalize(this); + _callbacks = null; + _application = null; } - #endregion private class ActivityLifecycleCallbacks : Java.Lang.Object, Application.IActivityLifecycleCallbacks { @@ -65,12 +42,12 @@ public void OnActivityDestroyed(Activity activity) public void OnActivityPaused(Activity activity) { - _backgroundingState.EnterBackgroundAsync(); + BackgroundDetection.UpdateBackgroundMode(true); } public void OnActivityResumed(Activity activity) { - _backgroundingState.ExitBackgroundAsync(); + BackgroundDetection.UpdateBackgroundMode(false); } public void OnActivitySaveInstanceState(Activity activity, Bundle outState) diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs new file mode 100644 index 00000000..59943320 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs @@ -0,0 +1,37 @@ +using System; +using LaunchDarkly.Xamarin; +using UIKit; +using Common.Logging; +using Foundation; + +namespace LaunchDarkly.Xamarin.BackgroundDetection +{ + internal static partial class BackgroundDetection + { + private static NSObject _foregroundHandle; + private static NSObject _backgroundHandle; + private static readonly ILog Log = LogManager.GetLogger(typeof(BackgroundAdapter)); + + private static void StartListening() + { + _foregroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleWillEnterForeground); + _backgroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, HandleWillEnterBackground); + } + + private static void StopListening() + { + _foregroundHandle = null; + _backgroundHandle = null; + } + + private static void HandleWillEnterForeground(NSNotification notification) + { + UpdateBackgroundMode(false); + } + + private static void HandleWillEnterBackground(NSNotification notification) + { + UpdateBackgroundMode(true); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs new file mode 100644 index 00000000..5779eb39 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs @@ -0,0 +1,15 @@ +using System; + +namespace LaunchDarkly.Xamarin.BackgroundDetection +{ + internal static partial class BackgroundDetection + { + private static void PlatformStartListening() + { + } + + private static void PlatformStopListening() + { + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs new file mode 100644 index 00000000..80dfc9c6 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs @@ -0,0 +1,58 @@ +using System; + +namespace LaunchDarkly.Xamarin.BackgroundDetection +{ + internal static partial class BackgroundDetection + { + private static event EventHandler _backgroundModeChanged; + + private static object _backgroundModeChangedHandlersLock = new object(); + + private static bool HasHandlers => _backgroundModeChanged != null && _backgroundModeChanged.GetInvocationList().Length != 0; + + public static event EventHandler BackgroundModeChanged + { + add + { + lock (_backgroundModeChangedHandlersLock) + { + var hadHandlers = HasHandlers; + _backgroundModeChanged += value; + if (!hadHandlers) + { + PlatformStartListening(); + } + } + } + remove + { + lock (_backgroundModeChangedHandlersLock) + { + var hadHandlers = HasHandlers; + _backgroundModeChanged -= value; + if (hadHandlers && !HasHandlers) + { + PlatformStopListening(); + } + } + } + } + + internal static void UpdateBackgroundMode(bool isInBackground) + { + var args = new BackgroundModeChangedEventArgs(isInBackground); + var handlers = _backgroundModeChanged; + handlers?.Invoke(null, args); + } + } + + internal class BackgroundModeChangedEventArgs + { + public bool IsInBackground { get; private set; } + + public BackgroundModeChangedEventArgs(bool isInBackground) + { + IsInBackground = isInBackground; + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs b/src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs deleted file mode 100644 index 88708ddb..00000000 --- a/src/LaunchDarkly.XamarinSdk/IBackgroundingState.shared.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin -{ - /// - /// An interface that is used internally by implementations of - /// to update the state of the LaunchDarkly client in background mode. Application code does not need - /// to interact with this interface. - /// - public interface IBackgroundingState - { - /// - /// Tells the LaunchDarkly client that the application is entering background mode. The client will - /// suspend the regular streaming or polling process, except when - /// is called. - /// - Task EnterBackgroundAsync(); - - /// - /// Tells the LaunchDarkly client that the application is exiting background mode. The client will - /// resume the regular streaming or polling process. - /// - Task ExitBackgroundAsync(); - } -} diff --git a/src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs b/src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs deleted file mode 100644 index ad49c2c1..00000000 --- a/src/LaunchDarkly.XamarinSdk/IPlatformAdapter.shared.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace LaunchDarkly.Xamarin -{ - /// - /// Interface for a component that helps LdClient interact with a specific mobile platform. - /// Currently this is necessary in order to handle features that are not part of the portable - /// Xamarin.Essentials API; in the future it may be handled automatically when you - /// create an LdClient. - /// - /// To obtain an instance of this interface, use the implementation of `PlatformComponents.CreatePlatformAdapter` - /// that is provided by the add-on library for your specific platform (e.g. LaunchDarkly.Xamarin.Android). - /// Then pass the object to - /// when you are building your client configuration. - /// - /// Application code should not call any methods of this interface directly; they are used internally - /// by LdClient. - /// - public interface IPlatformAdapter : IDisposable - { - /// - /// Tells the IPlatformAdapter to start monitoring the foreground/background state of - /// the application, and provides a callback object for it to use when the state changes. - /// - /// An implementation of IBackgroundingState provided by the client - void EnableBackgrounding(IBackgroundingState backgroundingState); - } - - internal class NullPlatformAdapter : IPlatformAdapter - { - public void EnableBackgrounding(IBackgroundingState backgroundingState) { } - - public void Dispose() { } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs index ca33cde8..e5f1cdcc 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs @@ -51,7 +51,6 @@ public sealed class LdClient : ILdMobileClient readonly EventFactory eventFactoryDefault = EventFactory.Default; readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; IFeatureFlagListenerManager flagListenerManager; - IPlatformAdapter platformAdapter; SemaphoreSlim connectionLock; @@ -77,7 +76,6 @@ public sealed class LdClient : ILdMobileClient persister = Factory.CreatePersister(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - platformAdapter = new LaunchDarkly.Xamarin.BackgroundAdapter.BackgroundAdapter(); User = DecorateUser(user); @@ -89,6 +87,7 @@ public sealed class LdClient : ILdMobileClient eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); SetupConnectionManager(); + BackgroundDetection.BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; } /// @@ -216,34 +215,12 @@ static void CreateInstance(Configuration configuration, User user) Instance = new LdClient(configuration, user); Log.InfoFormat("Initialized LaunchDarkly Client {0}", - Instance.Version); - - TimeSpan? bgPollInterval = null; - if (configuration.EnableBackgroundUpdating) - { - bgPollInterval = configuration.BackgroundPollingInterval; - } - try - { - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); - } - catch - { - Log.Info("Foreground/Background is only available on iOS and Android"); - } + Instance.Version); } bool StartUpdateProcessor(TimeSpan maxWaitTime) { - var initTask = updateProcessor.Start(); - try - { - return initTask.Wait(maxWaitTime); - } - catch (AggregateException e) - { - throw UnwrapAggregateException(e); - } + return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); } Task StartUpdateProcessorAsync() @@ -463,17 +440,7 @@ public void Flush() /// public void Identify(User user) { - try - { - // Note that we must use Task.Run here, rather than just doing IdentifyAsync(user).Wait(), - // to avoid a deadlock if we are on the main thread. See: - // https://olitee.com/2015/01/c-async-await-common-deadlock-scenario/ - Task.Run(() => IdentifyAsync(user)).Wait(); - } - catch (AggregateException e) - { - throw UnwrapAggregateException(e); - } + AsyncUtils.WaitSafely(() => IdentifyAsync(user)); } /// @@ -553,14 +520,7 @@ void Dispose(bool disposing) { Log.InfoFormat("Shutting down the LaunchDarkly client"); - try - { - platformAdapter.Dispose(); - } - catch(Exception error) - { - Log.Error(error); - } + BackgroundDetection.BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; updateProcessor.Dispose(); eventProcessor.Dispose(); } @@ -585,23 +545,30 @@ public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener lis public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) { flagListenerManager.UnregisterListener(listener, flagKey); - } - - internal async Task EnterBackgroundAsync() + } + + internal void OnBackgroundModeChanged(object sender, BackgroundDetection.BackgroundModeChangedEventArgs args) { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - if (Config.EnableBackgroundUpdating) - { - await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); - } - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); } - internal async Task EnterForegroundAsync() + internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundDetection.BackgroundModeChangedEventArgs args) { - ResetProcessorForForeground(); - await RestartUpdateProcessorAsync(Config.PollingInterval); + if (args.IsInBackground) + { + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + if (Config.EnableBackgroundUpdating) + { + await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); + } + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + } + else + { + ResetProcessorForForeground(); + await RestartUpdateProcessorAsync(Config.PollingInterval); + } } void ResetProcessorForForeground() @@ -614,35 +581,5 @@ void ResetProcessorForForeground() Config.IsStreamingEnabled = true; } } - - private Exception UnwrapAggregateException(AggregateException e) - { - if (e.InnerExceptions.Count == 1) - { - return e.InnerExceptions[0]; - } - return e; - } - } - - // Implementation of IBackgroundingState - this allows us to keep these methods out of the public LdClient API - internal class LdClientBackgroundingState : IBackgroundingState - { - private readonly LdClient _client; - - internal LdClientBackgroundingState(LdClient client) - { - _client = client; - } - - public async Task EnterBackgroundAsync() - { - await _client.EnterBackgroundAsync(); - } - - public async Task ExitBackgroundAsync() - { - await _client.EnterForegroundAsync(); - } } } \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index ae3cbe2a..107db885 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -19,7 +19,7 @@ public static LdClient CreateClient(Configuration config, User user) { lock (ClientInstanceLock) { - LdClient client = LdClient.Init(config, user, TimeSpan.Zero); + LdClient client = LdClient.Init(config, user, TimeSpan.FromSeconds(1)); LdClient.Instance = null; return client; } From 1e91ef2883d6d591489f5859238ca1d34f8a6658 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 15:20:58 -0700 Subject: [PATCH 085/254] misc fixes --- .../BackgroundDetection.android.cs | 11 ++--------- .../BackgroundDetection/BackgroundDetection.ios.cs | 6 ++---- .../Preferences/Preferences.android.cs | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs index 096e0ed4..ad27eb29 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs @@ -10,14 +10,14 @@ internal static partial class BackgroundDetection private static ActivityLifecycleCallbacks _callbacks; private static Application _application; - private static void StartListening() + private static void PlatformStartListening() { _callbacks = new ActivityLifecycleCallbacks(); _application = (Application)Application.Context; _application.RegisterActivityLifecycleCallbacks(_callbacks); } - private static void StopListening() + private static void PlatformStopListening() { _callbacks = null; _application = null; @@ -25,13 +25,6 @@ private static void StopListening() private class ActivityLifecycleCallbacks : Java.Lang.Object, Application.IActivityLifecycleCallbacks { - private IBackgroundingState _backgroundingState; - - public ActivityLifecycleCallbacks(IBackgroundingState backgroundingState) - { - _backgroundingState = backgroundingState; - } - public void OnActivityCreated(Activity activity, Bundle savedInstanceState) { } diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs index 59943320..fcd1a1ba 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs @@ -1,7 +1,6 @@ using System; using LaunchDarkly.Xamarin; using UIKit; -using Common.Logging; using Foundation; namespace LaunchDarkly.Xamarin.BackgroundDetection @@ -10,15 +9,14 @@ internal static partial class BackgroundDetection { private static NSObject _foregroundHandle; private static NSObject _backgroundHandle; - private static readonly ILog Log = LogManager.GetLogger(typeof(BackgroundAdapter)); - private static void StartListening() + private static void PlatformStartListening() { _foregroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleWillEnterForeground); _backgroundHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, HandleWillEnterBackground); } - private static void StopListening() + private static void PlatformStopListening() { _foregroundHandle = null; _backgroundHandle = null; diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs index 1e07aec0..1e6f8a7c 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs @@ -84,7 +84,7 @@ static void PlatformSet(string key, string value, string sharedName) } else { - editor.PutString(key, s); + editor.PutString(key, value); } editor.Apply(); } From 3e0b5b811c94b62bb6bbec85baaa1c87f11fe4e6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 15:36:50 -0700 Subject: [PATCH 086/254] basic components of Xamarin build on Mac --- .circleci/config.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index da05f2db..36d24c06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,11 +1,14 @@ version: 2 + workflows: version: 2 test: jobs: - - test-2.0 + - test-netstandard2.0 + - test-android + jobs: - test-2.0: + test-netstandard2.0: docker: - image: microsoft/dotnet:2.0-sdk-jessie steps: @@ -13,3 +16,20 @@ jobs: - run: dotnet restore - run: dotnet build src/LaunchDarkly.XamarinSdk -f netstandard2.0 - run: dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 + + test-android: + macos: + xcode: '10.1.0' + + steps: + - run: + name: Set up package source for Xamarin tools + command: brew install caskroom/cask/brew-cask + + - run: + name: Install Xamarin tools + command: brew cask install xamarin xamarin-jdk xamarin-ios xamarin-studio + + - run: + name: Build SDK + command: msbuild From c701fd884f8013e2859e3ff1a253a0aa6d658707 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 15:45:03 -0700 Subject: [PATCH 087/254] misc fixes --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 36d24c06..d4db86e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ jobs: test-android: macos: - xcode: '10.1.0' + xcode: "10.2.1" steps: - run: @@ -28,7 +28,7 @@ jobs: - run: name: Install Xamarin tools - command: brew cask install xamarin xamarin-jdk xamarin-ios xamarin-studio + command: brew cask install xamarin xamarin-android xamarin-ios - run: name: Build SDK From 531633d092fb60f713e355705fd516d5b4ccebe5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 16:07:34 -0700 Subject: [PATCH 088/254] misc fixes --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d4db86e5..cbd925b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,10 @@ jobs: - run: name: Install Xamarin tools - command: brew cask install xamarin xamarin-android xamarin-ios + command: brew cask install xamarin xamarin-android xamarin-ios && brew install mono + # Note, "mono" provides the msbuild CLI tool + + - checkout - run: name: Build SDK From c763ea98a05fdead1340ad2cf36b1bc37c5330aa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 16:14:06 -0700 Subject: [PATCH 089/254] fix var name --- src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs index 1e07aec0..1e6f8a7c 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs +++ b/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs @@ -84,7 +84,7 @@ static void PlatformSet(string key, string value, string sharedName) } else { - editor.PutString(key, s); + editor.PutString(key, value); } editor.Apply(); } From 71f9979398269e2036c8190e6fc6848ef3948f6f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 7 Jun 2019 16:14:53 -0700 Subject: [PATCH 090/254] restore packages --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cbd925b3..17174325 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,4 +35,4 @@ jobs: - run: name: Build SDK - command: msbuild + command: msbuild /restore From 42758670c65c793029d020136f5413f195f92453 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Jun 2019 13:35:06 -0700 Subject: [PATCH 091/254] install Android SDK components --- .circleci/config.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 17174325..59d2d96d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,9 @@ jobs: macos: xcode: "10.2.1" + environment: + ANDROID_SDK_ROOT: "/usr/local/share/android-sdk" + steps: - run: name: Set up package source for Xamarin tools @@ -28,9 +31,13 @@ jobs: - run: name: Install Xamarin tools - command: brew cask install xamarin xamarin-android xamarin-ios && brew install mono + command: brew cask install xamarin xamarin-android xamarin-ios android-sdk && brew install mono # Note, "mono" provides the msbuild CLI tool + - run: + name: Install Android SDK components + command: yes | $ANDROID_SDK_ROOT/tools/bin/sdkmanager "system-images;android-24;default;armeabi-v7a" + - checkout - run: From 0177b39fa693027086a38967bd0ce6ee840f6ac0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Jun 2019 13:40:55 -0700 Subject: [PATCH 092/254] clarifying comment --- .../BackgroundDetection/BackgroundDetection.netstandard.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs index 5779eb39..0aeef6bf 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs @@ -2,6 +2,10 @@ namespace LaunchDarkly.Xamarin.BackgroundDetection { + // This code is not from Xamarin Essentials, though it implements the same abstraction. It is a stub + // that does nothing, since in .NET Standard there is no notion of an application being in the + // background or the foreground. + internal static partial class BackgroundDetection { private static void PlatformStartListening() From 75c4b8fdea82d8bff39eff55a795aa7408318381 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Jun 2019 13:42:28 -0700 Subject: [PATCH 093/254] remove unnecessary checks for AggregateException --- .../AsyncUtils.shared.cs | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs b/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs index d76b72e7..cf4ca1a6 100644 --- a/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs @@ -19,17 +19,11 @@ internal static class AsyncUtils internal static void WaitSafely(Func taskFn) { - try - { - _taskFactory.StartNew(taskFn) - .Unwrap() - .GetAwaiter() - .GetResult(); - } - catch (AggregateException e) - { - throw UnwrapAggregateException(e); - } + _taskFactory.StartNew(taskFn) + .Unwrap() + .GetAwaiter() + .GetResult(); + // Note, GetResult does not throw AggregateException so we don't need to post-process exceptions } internal static bool WaitSafely(Func taskFn, TimeSpan timeout) @@ -48,17 +42,10 @@ internal static bool WaitSafely(Func taskFn, TimeSpan timeout) internal static T WaitSafely(Func> taskFn) { - try - { - return _taskFactory.StartNew(taskFn) - .Unwrap() - .GetAwaiter() - .GetResult(); - } - catch (AggregateException e) - { - throw UnwrapAggregateException(e); - } + return _taskFactory.StartNew(taskFn) + .Unwrap() + .GetAwaiter() + .GetResult(); } private static Exception UnwrapAggregateException(AggregateException e) From 490889aceb4437feefa15f15ee0369b219e34e4c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Jun 2019 18:17:53 -0700 Subject: [PATCH 094/254] add config option for whether to persist flags --- .../Configuration.shared.cs | 97 ++++++++----------- ....cs => DefaultPersistentStorage.shared.cs} | 2 +- src/LaunchDarkly.XamarinSdk/Factory.shared.cs | 56 +++++------ .../FlagCacheManager.shared.cs | 5 +- .../IDeviceInfo.shared.cs | 2 +- .../IMobileConfiguration.shared.cs | 16 ++- .../IPersistentStorage.shared.cs | 14 +++ .../ISimplePersistance.shared.cs | 8 -- .../IUserFlagCache.shared.cs | 6 ++ .../LdClient.shared.cs | 6 +- .../SimpleInMemoryPersistance.shared.cs | 21 ---- .../UserFlagDeviceCache.shared.cs | 4 +- .../LdClientTests.cs | 59 ++++++++++- .../MobileStreamingProcessorTests.cs | 2 +- .../MockComponents.cs | 35 ++++++- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 4 +- 16 files changed, 208 insertions(+), 129 deletions(-) rename src/LaunchDarkly.XamarinSdk/{SimpleMobileDevicePersistance.shared.cs => DefaultPersistentStorage.shared.cs} (83%) create mode 100644 src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs b/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs index 04c128c2..f65d032e 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs @@ -125,12 +125,14 @@ public class Configuration : IMobileConfiguration public bool EnableBackgroundUpdating { get; internal set; } /// public bool UseReport { get; internal set; } + /// + public bool PersistFlagValues { get; internal set; } internal IFlagCacheManager FlagCacheManager { get; set; } internal IConnectionManager ConnectionManager { get; set; } internal IEventProcessor EventProcessor { get; set; } - internal IMobileUpdateProcessor MobileUpdateProcessor { get; set; } - internal ISimplePersistance Persister { get; set; } + internal Func UpdateProcessorFactory { get; set; } + internal IPersistentStorage PersistentStorage { get; set; } internal IDeviceInfo DeviceInfo { get; set; } internal IFeatureFlagListenerManager FeatureFlagListenerManager { get; set; } @@ -229,7 +231,8 @@ public static Configuration Default(string mobileKey) UserKeysFlushInterval = DefaultUserKeysFlushInterval, InlineUsersInEvents = false, EnableBackgroundUpdating = true, - UseReport = true + UseReport = true, + PersistFlagValues = true }; return defaultConfiguration; @@ -541,18 +544,6 @@ public static Configuration WithInlineUsersInEvents(this Configuration configura return configuration; } - /// - /// Sets the IFlagCacheManager instance, used internally for stubbing mock instances. - /// - /// Configuration. - /// FlagCacheManager. - /// the same Configuration instance - internal static Configuration WithFlagCacheManager(this Configuration configuration, IFlagCacheManager flagCacheManager) - { - configuration.FlagCacheManager = flagCacheManager; - return configuration; - } - /// /// Sets the IConnectionManager instance, used internally for stubbing mock instances. /// @@ -606,79 +597,77 @@ public static Configuration WithEvaluationReasons(this Configuration configurati } /// - /// Sets the IMobileUpdateProcessor instance, used internally for stubbing mock instances. + /// Sets whether to enable background polling. /// /// Configuration. - /// Mobile update processor. + /// If set to true enable background updating. /// the same Configuration instance - internal static Configuration WithUpdateProcessor(this Configuration configuration, IMobileUpdateProcessor mobileUpdateProcessor) + public static Configuration WithEnableBackgroundUpdating(this Configuration configuration, bool enableBackgroundUpdating) { - configuration.MobileUpdateProcessor = mobileUpdateProcessor; + configuration.EnableBackgroundUpdating = enableBackgroundUpdating; return configuration; } /// - /// Sets the ISimplePersistance instance, used internally for stubbing mock instances. + /// Sets the interval for background polling. /// /// Configuration. - /// Persister. + /// Background polling internal. /// the same Configuration instance - public static Configuration WithPersister(this Configuration configuration, ISimplePersistance persister) + public static Configuration WithBackgroundPollingInterval(this Configuration configuration, TimeSpan backgroundPollingInternal) { - configuration.Persister = persister; + if (backgroundPollingInternal.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) + { + Log.WarnFormat("BackgroundPollingInterval cannot be less than the default of {0}.", Configuration.MinimumBackgroundPollingInterval); + backgroundPollingInternal = Configuration.MinimumBackgroundPollingInterval; + } + configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } /// - /// Sets the IDeviceInfo instance, used internally for stubbing mock instances. + /// Sets whether the SDK should save flag values for each user in persistent storage, so they will be + /// immediately available the next time the SDK is started for the same user. The default is . /// - /// Configuration. - /// Device info. + /// the configuration + /// true or false /// the same Configuration instance - public static Configuration WithDeviceInfo(this Configuration configuration, IDeviceInfo deviceInfo) + /// + public static Configuration WithPersistFlagValues(this Configuration configuration, bool persistFlagValues) + { + configuration.PersistFlagValues = persistFlagValues; + return configuration; + } + + // The following properties can only be set internally. They are used for providing stub implementations in unit tests. + + internal static Configuration WithDeviceInfo(this Configuration configuration, IDeviceInfo deviceInfo) { configuration.DeviceInfo = deviceInfo; return configuration; } - /// - /// Sets the IFeatureFlagListenerManager instance, used internally for stubbing mock instances. - /// - /// Configuration. - /// Feature flag listener manager. - /// the same Configuration instance internal static Configuration WithFeatureFlagListenerManager(this Configuration configuration, IFeatureFlagListenerManager featureFlagListenerManager) { configuration.FeatureFlagListenerManager = featureFlagListenerManager; return configuration; } - /// - /// Sets whether to enable background polling. - /// - /// Configuration. - /// If set to true enable background updating. - /// the same Configuration instance - public static Configuration WithEnableBackgroundUpdating(this Configuration configuration, bool enableBackgroundUpdating) + internal static Configuration WithFlagCacheManager(this Configuration configuration, IFlagCacheManager flagCacheManager) { - configuration.EnableBackgroundUpdating = enableBackgroundUpdating; + configuration.FlagCacheManager = flagCacheManager; return configuration; } - /// - /// Sets the interval for background polling. - /// - /// Configuration. - /// Background polling internal. - /// the same Configuration instance - public static Configuration WithBackgroundPollingInterval(this Configuration configuration, TimeSpan backgroundPollingInternal) + internal static Configuration WithPersistentStorage(this Configuration configuration, IPersistentStorage persistentStorage) { - if (backgroundPollingInternal.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) - { - Log.WarnFormat("BackgroundPollingInterval cannot be less than the default of {0}.", Configuration.MinimumBackgroundPollingInterval); - backgroundPollingInternal = Configuration.MinimumBackgroundPollingInterval; - } - configuration.BackgroundPollingInterval = backgroundPollingInternal; + configuration.PersistentStorage = persistentStorage; + return configuration; + } + + internal static Configuration WithUpdateProcessorFactory(this Configuration configuration, Func factory) + { + configuration.UpdateProcessorFactory = factory; return configuration; } } diff --git a/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs similarity index 83% rename from src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs rename to src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs index e373d732..10a69193 100644 --- a/src/LaunchDarkly.XamarinSdk/SimpleMobileDevicePersistance.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs @@ -2,7 +2,7 @@ namespace LaunchDarkly.Xamarin { - internal class SimpleMobileDevicePersistance : ISimplePersistance + internal class DefaultPersistentStorage : IPersistentStorage { public void Save(string key, string value) { diff --git a/src/LaunchDarkly.XamarinSdk/Factory.shared.cs b/src/LaunchDarkly.XamarinSdk/Factory.shared.cs index 3dac0518..8815fe43 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.shared.cs @@ -11,7 +11,7 @@ internal static class Factory private static readonly ILog Log = LogManager.GetLogger(typeof(Factory)); internal static IFlagCacheManager CreateFlagCacheManager(Configuration configuration, - ISimplePersistance persister, + IPersistentStorage persister, IFlagListenerUpdater updater, User user) { @@ -22,7 +22,7 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura else { var inMemoryCache = new UserFlagInMemoryCache(); - var deviceCache = new UserFlagDeviceCache(persister); + var deviceCache = configuration.PersistFlagValues ? new UserFlagDeviceCache(persister) as IUserFlagCache : new NullUserFlagCache(); return new FlagCacheManager(inMemoryCache, deviceCache, updater, user); } } @@ -32,38 +32,32 @@ internal static IConnectionManager CreateConnectionManager(Configuration configu return configuration.ConnectionManager ?? new MobileConnectionManager(); } - internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, - User user, - IFlagCacheManager flagCacheManager, - TimeSpan pollingInterval, - StreamManager.EventSourceCreator source = null) - { - if (configuration.MobileUpdateProcessor != null) + internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, User user, IFlagCacheManager flagCacheManager, TimeSpan? overridePollingInterval) + { + if (configuration.Offline) + { + Log.InfoFormat("Starting LaunchDarkly client in offline mode"); + return new NullUpdateProcessor(); + } + + if (configuration.UpdateProcessorFactory != null) { - return configuration.MobileUpdateProcessor; + return configuration.UpdateProcessorFactory(configuration, flagCacheManager, user); } - if (configuration.Offline) - { - Log.InfoFormat("Starting LaunchDarkly client in offline mode"); - return new NullUpdateProcessor(); + if (configuration.IsStreamingEnabled) + { + return new MobileStreamingProcessor(configuration, flagCacheManager, user, null); } - - if (configuration.IsStreamingEnabled) - { - return new MobileStreamingProcessor(configuration, - flagCacheManager, - user, source); - } - else - { - var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); - return new MobilePollingProcessor(featureFlagRequestor, - flagCacheManager, - user, - pollingInterval); + else + { + var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); + return new MobilePollingProcessor(featureFlagRequestor, + flagCacheManager, + user, + overridePollingInterval ?? configuration.PollingInterval); } - } + } internal static IEventProcessor CreateEventProcessor(Configuration configuration) { @@ -80,9 +74,9 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration return new DefaultEventProcessor(configuration, null, httpClient, Constants.EVENTS_PATH); } - internal static ISimplePersistance CreatePersister(Configuration configuration) + internal static IPersistentStorage CreatePersistentStorage(Configuration configuration) { - return configuration.Persister ?? new SimpleMobileDevicePersistance(); + return configuration.PersistentStorage ?? new DefaultPersistentStorage(); } internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) diff --git a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs index 92e0acda..d12680f9 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs @@ -22,7 +22,10 @@ public FlagCacheManager(IUserFlagCache inMemoryCache, this.flagListenerUpdater = flagListenerUpdater; var flagsFromDevice = deviceCache.RetrieveFlags(user); - inMemoryCache.CacheFlagsForUser(flagsFromDevice, user); + if (flagsFromDevice != null) + { + inMemoryCache.CacheFlagsForUser(flagsFromDevice, user); + } } public IDictionary FlagsForUser(User user) diff --git a/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs index 5b07db18..53bbad27 100644 --- a/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs @@ -1,6 +1,6 @@ namespace LaunchDarkly.Xamarin { - public interface IDeviceInfo + internal interface IDeviceInfo { string UniqueDeviceId(); } diff --git a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs index 8a98fde0..8fb923a8 100644 --- a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs @@ -50,6 +50,20 @@ public interface IMobileConfiguration : IBaseConfiguration /// to true. /// /// true if evaluation reasons are desired. - bool EvaluationReasons { get; } + bool EvaluationReasons { get; } + + /// + /// True if the SDK should save flag values for each user in persistent storage, so they will be + /// immediately available the next time the SDK is started for the same user. This is true by + /// default; set it to false to disable this behavior. + /// + /// + /// The implementation of persistent storage depends on the target platform. In Android and iOS, it + /// uses the standard user preferences mechanism. In .NET Standard, it uses the IsolatedStorageFile + /// API, which stores file data under the current account's home directory at + /// ~/.local/share/IsolateStorage/. + /// + /// true if flag values should be stored locally (the default). + bool PersistFlagValues { get; } } } diff --git a/src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs b/src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs new file mode 100644 index 00000000..11a2b8d8 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs @@ -0,0 +1,14 @@ +namespace LaunchDarkly.Xamarin +{ + internal interface IPersistentStorage + { + string GetValue(string key); + void Save(string key, string value); + } + + internal class NullPersistentStorage : IPersistentStorage + { + public string GetValue(string key) => null; + public void Save(string key, string value) { } + } +} \ No newline at end of file diff --git a/src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs deleted file mode 100644 index 6681c06e..00000000 --- a/src/LaunchDarkly.XamarinSdk/ISimplePersistance.shared.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LaunchDarkly.Xamarin -{ - public interface ISimplePersistance - { - string GetValue(string key); - void Save(string key, string value); - } -} \ No newline at end of file diff --git a/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs index 8994715b..99211ba4 100644 --- a/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs @@ -8,4 +8,10 @@ internal interface IUserFlagCache void CacheFlagsForUser(IDictionary flags, User user); IDictionary RetrieveFlags(User user); } + + internal class NullUserFlagCache : IUserFlagCache + { + public void CacheFlagsForUser(IDictionary flags, User user) { } + public IDictionary RetrieveFlags(User user) => null; + } } diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs index e5f1cdcc..022d7a1f 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs @@ -46,7 +46,7 @@ public sealed class LdClient : ILdMobileClient IConnectionManager connectionManager; IMobileUpdateProcessor updateProcessor; IEventProcessor eventProcessor; - ISimplePersistance persister; + IPersistentStorage persister; IDeviceInfo deviceInfo; readonly EventFactory eventFactoryDefault = EventFactory.Default; readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; @@ -73,7 +73,7 @@ public sealed class LdClient : ILdMobileClient connectionLock = new SemaphoreSlim(1, 1); - persister = Factory.CreatePersister(configuration); + persister = Factory.CreatePersistentStorage(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); @@ -81,7 +81,7 @@ public sealed class LdClient : ILdMobileClient flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, configuration.PollingInterval); + updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, null); eventProcessor = Factory.CreateEventProcessor(configuration); eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); diff --git a/src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs b/src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs deleted file mode 100644 index 5f6d81c7..00000000 --- a/src/LaunchDarkly.XamarinSdk/SimpleInMemoryPersistance.shared.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace LaunchDarkly.Xamarin -{ - public class SimpleInMemoryPersistance : ISimplePersistance - { - IDictionary map = new Dictionary(); - - public string GetValue(string key) - { - string value = null; - map.TryGetValue(key, out value); - return value; - } - - public void Save(string key, string value) - { - map[key] = value; - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs index b3fc8ff3..29e616e6 100644 --- a/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs @@ -10,9 +10,9 @@ namespace LaunchDarkly.Xamarin internal class UserFlagDeviceCache : IUserFlagCache { private static readonly ILog Log = LogManager.GetLogger(typeof(UserFlagDeviceCache)); - private readonly ISimplePersistance persister; + private readonly IPersistentStorage persister; - public UserFlagDeviceCache(ISimplePersistance persister) + public UserFlagDeviceCache(IPersistentStorage persister) { this.persister = persister; } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 46cdc271..d172cf6a 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -114,12 +117,14 @@ public void ConnectionManagerShouldKnowIfOnlineOrNot() [Fact] public void ConnectionChangeShouldStopUpdateProcessor() { - using (var client = Client()) + var mockUpdateProc = new MockPollingProcessor(null); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithUpdateProcessorFactory(mockUpdateProc.AsFactory()); + using (var client = TestUtil.CreateClient(config, simpleUser)) { var connMgr = client.Config.ConnectionManager as MockConnectionManager; connMgr.ConnectionChanged += (bool obj) => client.Online = obj; connMgr.Connect(false); - var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; Assert.False(mockUpdateProc.IsRunning); } } @@ -245,5 +250,55 @@ public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerMan Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); } } + + [Fact] + public void FlagsAreLoadedFromPersistentStorageByDefault() + { + var storage = new MockPersistentStorage(); + var flagsJson = "{\"flag\": {\"value\": 100}}"; + storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithOffline(true) + .WithPersistentStorage(storage) + .WithFlagCacheManager(null); // use actual cache logic, not mock component (even though persistence layer is a mock) + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.Equal(100, client.IntVariation("flag", 99)); + } + } + + [Fact] + public void FlagsAreNotLoadedFromPersistentStorageIfPersistFlagValuesIsFalse() + { + var storage = new MockPersistentStorage(); + var flagsJson = "{\"flag\": {\"value\": 100}}"; + storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithOffline(true) + .WithPersistFlagValues(false) + .WithPersistentStorage(storage) + .WithFlagCacheManager(null); // use actual cache logic, not mock component (even though persistence layer is a mock) + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.Equal(99, client.IntVariation("flag", 99)); // returns default value + } + } + + [Fact] + public void FlagsAreSavedToPersistentStorageByDefault() + { + var storage = new MockPersistentStorage(); + var flagsJson = "{\"flag\": {\"value\": 100}}"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, flagsJson) + .WithPersistentStorage(storage) + .WithFlagCacheManager(null) + .WithUpdateProcessorFactory(MockPollingProcessor.Factory(flagsJson)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + var storedJson = storage.GetValue(Constants.FLAGS_KEY_PREFIX + simpleUser.Key); + var flags = JsonConvert.DeserializeObject>(storedJson); + Assert.Equal(new JValue(100), flags["flag"].value); + } + } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index c61fc10e..03d4db6d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -41,7 +41,7 @@ public MobileStreamingProcessorTests() private IMobileUpdateProcessor MobileStreamingProcessorStarted() { - var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, TimeSpan.FromMinutes(5), eventSourceFactory.Create()); + IMobileUpdateProcessor processor = new MobileStreamingProcessor(config, mockFlagCacheMgr, user, eventSourceFactory.Create()); processor.Start(); return processor; } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index 83645b1f..69f96cd0 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LaunchDarkly.Client; +using Newtonsoft.Json; namespace LaunchDarkly.Xamarin.Tests { @@ -133,7 +134,7 @@ public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user } } - internal class MockPersister : ISimplePersistance + internal class MockPersistentStorage : IPersistentStorage { private IDictionary map = new Dictionary(); @@ -153,6 +154,34 @@ public void Save(string key, string value) internal class MockPollingProcessor : IMobileUpdateProcessor { + private IFlagCacheManager _cacheManager; + private User _user; + private string _flagsJson; + + public MockPollingProcessor(string flagsJson) : this(null, null, flagsJson) { } + + private MockPollingProcessor(IFlagCacheManager cacheManager, User user, string flagsJson) + { + _cacheManager = cacheManager; + _user = user; + _flagsJson = flagsJson; + } + + public static Func Factory(string flagsJson) + { + return (config, manager, user) => new MockPollingProcessor(manager, user, flagsJson); + } + + public Func AsFactory() + { + return (config, manager, user) => + { + _cacheManager = manager; + _user = user; + return this; + }; + } + public bool IsRunning { get; @@ -172,6 +201,10 @@ public bool Initialized() public Task Start() { IsRunning = true; + if (_cacheManager != null && _flagsJson != null) + { + _cacheManager.CacheFlagsFromService(JsonConvert.DeserializeObject>(_flagsJson), _user); + } return Task.FromResult(true); } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 107db885..71d8cb13 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -70,8 +70,8 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) .WithConnectionManager(new MockConnectionManager(true)) .WithEventProcessor(new MockEventProcessor()) - .WithUpdateProcessor(new MockPollingProcessor()) - .WithPersister(new MockPersister()) + .WithUpdateProcessorFactory(MockPollingProcessor.Factory(null)) + .WithPersistentStorage(new MockPersistentStorage()) .WithDeviceInfo(new MockDeviceInfo("")) .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); return configuration; From 7ef51d9dd37c281a4b458ccead39ad3d447a1e5d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 13 Jun 2019 13:22:31 -0700 Subject: [PATCH 095/254] suppress spurious sdkmanager error --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 59d2d96d..848a4c81 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,7 +36,8 @@ jobs: - run: name: Install Android SDK components - command: yes | $ANDROID_SDK_ROOT/tools/bin/sdkmanager "system-images;android-24;default;armeabi-v7a" + command: yes | $ANDROID_SDK_ROOT/tools/bin/sdkmanager "system-images;android-24;default;armeabi-v7a" || true + # Note, "|| true" is because sdkmanager is known to return a non-zero exit code (141) when there's no error - checkout From 6c80e5a7610c3777c0fe3fbc5068bdeca6e6298f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 14:57:47 -0700 Subject: [PATCH 096/254] split out Android job --- .circleci/config.yml | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 848a4c81..ceed00f9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,12 +18,39 @@ jobs: - run: dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 test-android: + docker: + - image: circleci/android:api-28 + + steps: + - checkout + + - run: sdkmanager "system-images;android-24;default;armeabi-v7a" || true + - run: sdkmanager --licenses + + - run: sudo apt install apt-transport-https dirmngr gnupg ca-certificates && sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update + - run: echo y | sudo apt install mono-devel nuget libzip4 + + - run: nuget restore + + - restore_cache: + key: xamarin-android-cache-v9-2-99-172 + - run: chmod +x scripts/check_xamarin_android_cache.sh && ./scripts/check_xamarin_android_cache.sh + - save_cache: + key: xamarin-android-cache-v9-2-99-172 + paths: + - ~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release + + - run: sudo mkdir "/usr/lib/xamarin.android" && sudo mkdir "/usr/lib/mono/xbuild/Xamarin/" + - run: cd ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release && sudo cp -a "bin/Debug/lib/xamarin.android/." "/usr/lib/xamarin.android/" + - run: rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" + - run: sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" && sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" + + - run: msbuild /restorer /p:TargetFramework=MonoAndroid81 + + test-ios: macos: xcode: "10.2.1" - environment: - ANDROID_SDK_ROOT: "/usr/local/share/android-sdk" - steps: - run: name: Set up package source for Xamarin tools @@ -31,16 +58,11 @@ jobs: - run: name: Install Xamarin tools - command: brew cask install xamarin xamarin-android xamarin-ios android-sdk && brew install mono + command: brew cask install xamarin xamarin-ios && brew install mono # Note, "mono" provides the msbuild CLI tool - - run: - name: Install Android SDK components - command: yes | $ANDROID_SDK_ROOT/tools/bin/sdkmanager "system-images;android-24;default;armeabi-v7a" || true - # Note, "|| true" is because sdkmanager is known to return a non-zero exit code (141) when there's no error - - checkout - run: name: Build SDK - command: msbuild /restore + command: msbuild /restore /p:TargetFramework=Xamarin.iOS10 From 6cc44a96ded25ca101249382face1e394df7d428 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 15:28:02 -0700 Subject: [PATCH 097/254] add missing script --- .circleci/config.yml | 1 + scripts/check_xamarin_android_cache.sh | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 scripts/check_xamarin_android_cache.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index ceed00f9..60513c5d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,7 @@ workflows: jobs: - test-netstandard2.0 - test-android + - test-ios jobs: test-netstandard2.0: diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh new file mode 100644 index 00000000..6aa443f6 --- /dev/null +++ b/scripts/check_xamarin_android_cache.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# used only for CI build + +if [ -f "~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then + echo "Xamarin Android cache exists" +else + wget https://jenkins.mono-project.com/view/Xamarin.Android/job/xamarin-android-linux/lastSuccessfulBuild/Azure/processDownloadRequest/xamarin-android/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 + tar xjf ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 + echo "Downloaded Xamarin Android from Mono Jenkins" +fi From 75e5bfba4e1cfe0d79f0a5a45d7e5eabd9bce203 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 15:54:54 -0700 Subject: [PATCH 098/254] build only main project --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60513c5d..c150129d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,4 +66,4 @@ jobs: - run: name: Build SDK - command: msbuild /restore /p:TargetFramework=Xamarin.iOS10 + command: msbuild /restore /p:TargetFramework=Xamarin.iOS10 src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj From 69dce011c565033ec69f75a399120a1e336500b4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 15:55:38 -0700 Subject: [PATCH 099/254] fix Android build --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c150129d..9e73ccac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,9 @@ jobs: - run: rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - run: sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" && sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - - run: msbuild /restorer /p:TargetFramework=MonoAndroid81 + - run: + name: Build SDK + command: msbuild /restore /p:TargetFramework=MonoAndroid81 src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj test-ios: macos: From 1f1f1c99d04f89104f50a44846b87f84b3ec42c8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 16:09:39 -0700 Subject: [PATCH 100/254] make script executable --- scripts/check_xamarin_android_cache.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/check_xamarin_android_cache.sh diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh old mode 100644 new mode 100755 From 9591da7fe1e951d2978c93fc4859e9f4111f60ff Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 16:10:01 -0700 Subject: [PATCH 101/254] make script executable --- .circleci/config.yml | 2 +- scripts/check_xamarin_android_cache.sh | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 scripts/check_xamarin_android_cache.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 9e73ccac..875b472e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ jobs: - restore_cache: key: xamarin-android-cache-v9-2-99-172 - - run: chmod +x scripts/check_xamarin_android_cache.sh && ./scripts/check_xamarin_android_cache.sh + - run: ./scripts/check_xamarin_android_cache.sh - save_cache: key: xamarin-android-cache-v9-2-99-172 paths: diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh old mode 100755 new mode 100644 From 2cfb3783444def6ee0321b55e8c79f64561c87ac Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 16:49:29 -0700 Subject: [PATCH 102/254] try Android 27 image --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 875b472e..e49cf9e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,7 @@ jobs: test-android: docker: - - image: circleci/android:api-28 + - image: circleci/android:api-27 steps: - checkout From 1215254d6d0a763172d1fdd09f3274c15f714ff9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 18 Jun 2019 18:02:05 -0700 Subject: [PATCH 103/254] fix script permission --- scripts/check_xamarin_android_cache.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/check_xamarin_android_cache.sh diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh old mode 100644 new mode 100755 From 9b8719ec63701c0ccb6b19e04977f7031cf66a54 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 19 Jun 2019 11:52:20 -0700 Subject: [PATCH 104/254] cached data is a directory, not a file --- scripts/check_xamarin_android_cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh index 6aa443f6..e25f02a4 100755 --- a/scripts/check_xamarin_android_cache.sh +++ b/scripts/check_xamarin_android_cache.sh @@ -2,7 +2,7 @@ # used only for CI build -if [ -f "~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then +if [ -e "~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then echo "Xamarin Android cache exists" else wget https://jenkins.mono-project.com/view/Xamarin.Android/job/xamarin-android-linux/lastSuccessfulBuild/Azure/processDownloadRequest/xamarin-android/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 From 75422e2226311c398b8c5c80942bcbdba63fac80 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 19 Jun 2019 13:08:17 -0700 Subject: [PATCH 105/254] fix path in if test --- scripts/check_xamarin_android_cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh index e25f02a4..41eae00a 100755 --- a/scripts/check_xamarin_android_cache.sh +++ b/scripts/check_xamarin_android_cache.sh @@ -2,7 +2,7 @@ # used only for CI build -if [ -e "~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then +if [ -e "xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then echo "Xamarin Android cache exists" else wget https://jenkins.mono-project.com/view/Xamarin.Android/job/xamarin-android-linux/lastSuccessfulBuild/Azure/processDownloadRequest/xamarin-android/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 From 6c05c2ef702b6c433ecfac47225ed236a8d0748e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sun, 23 Jun 2019 15:47:13 -0700 Subject: [PATCH 106/254] tests of LdClient with actual HTTP, + misc test cleanup --- .circleci/config.yml | 2 + .../FeatureFlagRequestor.shared.cs | 2 - .../LdClient.shared.cs | 19 +- .../MobilePollingProcessor.shared.cs | 3 +- .../LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 49 +++ .../ConfigurationTest.cs | 3 +- .../FeatureFlagListenerTests.cs | 5 +- .../FeatureFlagRequestorTests.cs | 127 +++++++ .../FeatureFlagTests.cs | 5 +- .../FlagCacheManagerTests.cs | 2 +- .../LDClientEndToEndTests.cs | 341 ++++++++++++++++++ .../LaunchDarkly.XamarinSdk.Tests.csproj | 1 + .../LdClientEvaluationTests.cs | 8 +- .../LdClientEventTests.cs | 2 +- .../LdClientTests.cs | 24 +- .../LaunchDarkly.XamarinSdk.Tests/LogSink.cs | 73 ++++ .../MobilePollingProcessorTests.cs | 2 +- .../MobileStreamingProcessorTests.cs | 4 +- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 24 +- .../UserFlagCacheTests.cs | 2 +- .../WireMockExtensions.cs | 43 +++ 21 files changed, 685 insertions(+), 56 deletions(-) create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/WireMockExtensions.cs diff --git a/.circleci/config.yml b/.circleci/config.yml index e49cf9e7..4de92fef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,8 @@ jobs: test-netstandard2.0: docker: - image: microsoft/dotnet:2.0-sdk-jessie + env: + - ASPNETCORE_SUPPRESSSTATUSMESSAGES: "true" # suppresses annoying debug output from embedded HTTP servers in tests steps: - checkout - run: dotnet restore diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs index 10174600..832b90b2 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -9,7 +8,6 @@ using Common.Logging; using LaunchDarkly.Client; using LaunchDarkly.Common; -using Newtonsoft.Json; namespace LaunchDarkly.Xamarin { diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs index 022d7a1f..9dedb090 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs @@ -193,29 +193,30 @@ public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTim /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static Task InitAsync(Configuration config, User user) { - CreateInstance(config, user); + var c = CreateInstance(config, user); - if (Instance.Online) + if (c.Online) { - Task t = Instance.StartUpdateProcessorAsync(); - return t.ContinueWith((result) => Instance); + Task t = c.StartUpdateProcessorAsync(); + return t.ContinueWith((result) => c); } else { - return Task.FromResult(Instance); + return Task.FromResult(c); } } - static void CreateInstance(Configuration configuration, User user) + static LdClient CreateInstance(Configuration configuration, User user) { if (Instance != null) { throw new Exception("LdClient instance already exists."); } - Instance = new LdClient(configuration, user); - Log.InfoFormat("Initialized LaunchDarkly Client {0}", - Instance.Version); + var c = new LdClient(configuration, user); + Instance = c; + Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); + return c; } bool StartUpdateProcessor(TimeSpan maxWaitTime) diff --git a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs index 9119e367..44668ae5 100644 --- a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs @@ -42,8 +42,7 @@ Task IMobileUpdateProcessor.Start() if (pollingInterval.Equals(TimeSpan.Zero)) throw new Exception("Timespan for polling can't be zero"); - Log.InfoFormat("Starting LaunchDarkly PollingProcessor with interval: {0} milliseconds", - pollingInterval); + Log.InfoFormat("Starting LaunchDarkly PollingProcessor with interval: {0}", pollingInterval); Task.Run(() => UpdateTaskLoopAsync()); return _startTask.Task; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs new file mode 100644 index 00000000..09b1acc5 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Common.Logging; +using WireMock.Server; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + [Collection("serialize all tests")] + public class BaseTest + { + public BaseTest() + { + LogManager.Adapter = new LogSinkFactoryAdapter(); + TestUtil.ClearClient(); + } + + ~BaseTest() + { + TestUtil.ClearClient(); + } + + protected void WithServer(Action a) + { + var s = FluentMockServer.Start(); + try + { + a(s); + } + finally + { + s.Stop(); + } + } + + protected async Task WithServerAsync(Func a) + { + var s = FluentMockServer.Start(); + try + { + await a(s); + } + finally + { + s.Stop(); + } + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs index 8f5b4f8d..641a3c22 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs @@ -1,11 +1,10 @@ using System; using System.Net.Http; -using LaunchDarkly.Client; using Xunit; namespace LaunchDarkly.Xamarin.Tests { - public class ConfigurationTest + public class ConfigurationTest : BaseTest { [Fact] public void CanOverrideConfiguration() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs index cf0d5b26..d20f2d9b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using Xunit; namespace LaunchDarkly.Xamarin.Tests { - public class FeatureFlagListenerTests + public class FeatureFlagListenerTests : BaseTest { private const string INT_FLAG = "int-flag"; private const string DOUBLE_FLAG = "double-flag"; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs new file mode 100644 index 00000000..fd07d09e --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -0,0 +1,127 @@ +using System.Threading.Tasks; +using LaunchDarkly.Client; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + // End-to-end tests of this component against an embedded HTTP server. + public class FeatureFlagRequestorTests : BaseTest + { + private const string _mobileKey = "FAKE_KEY"; + + private static readonly User _user = User.WithKey("foo"); + private const string _encodedUser = "eyJrZXkiOiJmb28iLCJjdXN0b20iOnt9fQ=="; + // Note that in a real use case, the user encoding may vary depending on the target platform, because the SDK adds custom + // user attributes like "os". But the lower-level FeatureFlagRequestor component does not do that. + + private const string _allDataJson = "{}"; // Note that in this implementation, unlike the .NET SDK, FeatureFlagRequestor does not unmarshal the response + + [Fact] + public async Task GetFlagsUsesCorrectUriAndMethodInGetModeAsync() + { + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) + .WithUseReport(false); + + using (var requestor = new FeatureFlagRequestor(config, _user)) + { + var resp = await requestor.FeatureFlagsAsync(); + Assert.Equal(200, resp.statusCode); + Assert.Equal(_allDataJson, resp.jsonResponse); + + var req = server.GetLastRequest(); + Assert.Equal("GET", req.Method); + Assert.Equal($"/msdk/evalx/users/{_encodedUser}", req.Path); + Assert.Equal("", req.RawQuery); + Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + Assert.Null(req.Body); + } + }); + } + + [Fact] + public async Task GetFlagsUsesCorrectUriAndMethodInGetModeWithReasonsAsync() + { + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) + .WithUseReport(false).WithEvaluationReasons(true); + + using (var requestor = new FeatureFlagRequestor(config, _user)) + { + var resp = await requestor.FeatureFlagsAsync(); + Assert.Equal(200, resp.statusCode); + Assert.Equal(_allDataJson, resp.jsonResponse); + + var req = server.GetLastRequest(); + Assert.Equal("GET", req.Method); + Assert.Equal($"/msdk/evalx/users/{_encodedUser}", req.Path); + Assert.Equal("?withReasons=true", req.RawQuery); + Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + Assert.Null(req.Body); + } + }); + } + + [Fact] + public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync() + { + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) + .WithUseReport(true); + + using (var requestor = new FeatureFlagRequestor(config, _user)) + { + var resp = await requestor.FeatureFlagsAsync(); + Assert.Equal(200, resp.statusCode); + Assert.Equal(_allDataJson, resp.jsonResponse); + + var req = server.GetLastRequest(); + Assert.Equal("REPORT", req.Method); + Assert.Equal($"/msdk/evalx/user", req.Path); + Assert.Equal("", req.RawQuery); + Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + + //Assert.Equal("{\"key\":\"foo\"}", req.Body); + // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, for unknown + // reasons the current version of WireMock.Net does not seem to be capturing that information; Content-Type and + // Content-Length are correct, and directing the request at a different listener (e.g. "nc -l") shows that the body + // is definitely being sent, but req.Body and req.BodyAsJson are both null no matter what. + } + }); + } + + [Fact] + public async Task GetFlagsUsesCorrectUriAndMethodInReportModeWithReasonsAsync() + { + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) + .WithUseReport(true).WithEvaluationReasons(true); + + using (var requestor = new FeatureFlagRequestor(config, _user)) + { + var resp = await requestor.FeatureFlagsAsync(); + Assert.Equal(200, resp.statusCode); + Assert.Equal(_allDataJson, resp.jsonResponse); + + var req = server.GetLastRequest(); + Assert.Equal("REPORT", req.Method); + Assert.Equal($"/msdk/evalx/user", req.Path); + Assert.Equal("?withReasons=true", req.RawQuery); + Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + } + }); + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs index 5b804a88..92a5b569 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagTests.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; using Xunit; namespace LaunchDarkly.Xamarin.Tests { - public class FeatureFlagEventTests + public class FeatureFlagEventTests : BaseTest { [Fact] public void ReturnsFlagVersionAsVersion() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs index 574cbfd0..fdf00d8d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs @@ -4,7 +4,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class FlagCacheManagerTests + public class FlagCacheManagerTests : BaseTest { private const string initialFlagsJson = "{" + "\"int-flag\":{\"value\":15}," + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs new file mode 100644 index 00000000..4034c769 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Common.Logging; +using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Server; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + // Tests of an LDClient instance doing actual HTTP against an embedded server. These aren't intended to cover + // every possible type of interaction, since the lower-level component tests like FeatureFlagRequestorTests + // (and the DefaultEventProcessor and StreamManager tests in LaunchDarkly.CommonSdk) cover those more thoroughly. + // These are more of a smoke test to ensure that the SDK is initializing and using those components in the + // expected ways. + public class LdClientEndToEndTests : BaseTest + { + private const string _mobileKey = "FAKE_KEY"; + + private static readonly User _user = User.WithKey("foo"); + private static readonly User _otherUser = User.WithKey("bar"); + + private static readonly IDictionary _flagData1 = new Dictionary + { + { "flag1", "value1" } + }; + + private static readonly IDictionary _flagData2 = new Dictionary + { + { "flag1", "value2" } + }; + + [Fact] + public void InitGetsFlagsInPollingModeSync() + { + InitGetsFlagsSync(UpdateMode.Polling); + } + + [Fact] + public void InitGetsFlagsInStreamingModeSync() + { + InitGetsFlagsSync(UpdateMode.Streaming); + } + + private void InitGetsFlagsSync(UpdateMode mode) + { + WithServer(server => + { + SetupResponse(server, _flagData1, mode); + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + using (var client = TestUtil.CreateClient(config, _user)) + { + VerifyRequest(server, mode); + VerifyFlagValues(client, _flagData1); + } + }); + } + + [Fact] + public async Task InitGetsFlagsInPollingModeAsync() + { + await InitGetsFlagsAsync(UpdateMode.Polling); + } + + [Fact] + public async Task InitGetsFlagsInStreamingModeAsync() + { + // Note: since WireMock.Net doesn't seem to support streaming responses, the response will close after the end of the + // data, the SDK will enter retry mode and we may get another identical streaming request. For the purposes of these tests, + // that doesn't matter. The correct processing of a chunked stream is tested in the LaunchDarkly.EventSource tests, and + // the retry logic is tested in LaunchDarkly.CommonSdk. + await InitGetsFlagsAsync(UpdateMode.Streaming); + } + + private async Task InitGetsFlagsAsync(UpdateMode mode) + { + await WithServerAsync(async server => + { + SetupResponse(server, _flagData1, mode); + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + VerifyRequest(server, mode); + } + }); + } + + [Fact] + public void InitCanTimeOutSync() + { + WithServer(server => + { + server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); + + using (var log = new LogSinkScope()) + { + var config = BaseConfig(server).WithIsStreamingEnabled(false); + using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) + { + Assert.False(client.Initialized()); + Assert.Null(client.StringVariation(_flagData1.First().Key, null)); + Assert.Contains(log.Messages, m => m.Level == LogLevel.Warn && + m.Text == "Client did not successfully initialize within 200 milliseconds."); + } + } + }); + } + + [Fact] + public void InitFailsOn401InPollingModeSync() + { + InitFailsOn401Sync(UpdateMode.Polling); + } + + [Fact] + public void InitFailsOn401InStreamingModeSync() + { + InitFailsOn401Sync(UpdateMode.Streaming); + } + + private void InitFailsOn401Sync(UpdateMode mode) + { + WithServer(server => + { + server.ForAllRequests(r => r.WithStatusCode(401)); + + using (var log = new LogSinkScope()) + { + try + { + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + using (var client = TestUtil.CreateClient(config, _user)) { } + } + catch (Exception e) + { + // Currently the exact class of this exception is undefined: the polling processor throws + // LaunchDarkly.Client.UnsuccessfulResponseException, while the streaming processor throws + // a lower-level exception that is defined by LaunchDarkly.EventSource. + Assert.Contains("401", e.Message); + return; + } + throw new Exception("Expected exception from LdClient.Init"); + } + }); + } + + [Fact] + public async Task InitFailsOn401InPollingModeAsync() + { + await InitFailsOn401Async(UpdateMode.Polling); + } + + [Fact] + public async Task InitFailsOn401InStreamingModeAsync() + { + await InitFailsOn401Async(UpdateMode.Streaming); + } + + private async Task InitFailsOn401Async(UpdateMode mode) + { + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithStatusCode(401)); + + using (var log = new LogSinkScope()) + { + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + + // Currently the behavior of LdClient.InitAsync is somewhat inconsistent with LdClient.Init if there is + // an unrecoverable error: LdClient.Init throws an exception, but LdClient.InitAsync returns a task that + // will complete successfully with an uninitialized client. + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + Assert.False(client.Initialized()); + } + } + }); + } + + [Fact] + public void IdentifySwitchesUserAndGetsFlagsInPollingModeSync() + { + IdentifySwitchesUserAndGetsFlagsSync(UpdateMode.Polling); + } + + [Fact] + public void IdentifySwitchesUserAndGetsFlagsInStreamingModeSync() + { + IdentifySwitchesUserAndGetsFlagsSync(UpdateMode.Streaming); + } + + private void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) + { + WithServer(server => + { + SetupResponse(server, _flagData1, mode); + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + using (var client = TestUtil.CreateClient(config, _user)) + { + VerifyRequest(server, mode); + VerifyFlagValues(client, _flagData1); + var user1RequestPath = server.GetLastRequest().Path; + + server.Reset(); + SetupResponse(server, _flagData2, mode); + + client.Identify(_otherUser); + Assert.Equal(_otherUser, client.User); + + VerifyRequest(server, mode); + Assert.NotEqual(user1RequestPath, server.GetLastRequest().Path); + VerifyFlagValues(client, _flagData2); + } + }); + } + + [Fact] + public async Task IdentifySwitchesUserAndGetsFlagsInPollingModeAsync() + { + await IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode.Polling); + } + + [Fact] + public async Task IdentifySwitchesUserAndGetsFlagsInStreamingModeAsync() + { + await IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode.Streaming); + } + + private async Task IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode mode) + { + await WithServerAsync(async server => + { + SetupResponse(server, _flagData1, mode); + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + VerifyRequest(server, mode); + VerifyFlagValues(client, _flagData1); + var user1RequestPath = server.GetLastRequest().Path; + + server.Reset(); + SetupResponse(server, _flagData2, mode); + + await client.IdentifyAsync(_otherUser); + Assert.Equal(_otherUser, client.User); + + VerifyRequest(server, mode); + Assert.NotEqual(user1RequestPath, server.GetLastRequest().Path); + VerifyFlagValues(client, _flagData2); + } + }); + } + + private Configuration BaseConfig(FluentMockServer server) + { + return Configuration.Default(_mobileKey) + .WithBaseUri(server.GetUrl()) + .WithStreamUri(server.GetUrl()) + .WithEventProcessor(new NullEventProcessor()); + } + + private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) + { + server.ForAllRequests(r => + mode.IsStreaming ? r.WithEventsBody(StreamingData(data)) : r.WithJsonBody(PollingData(data))); + } + + private void VerifyRequest(FluentMockServer server, UpdateMode mode) + { + var req = server.GetLastRequest(); + Assert.Equal("GET", req.Method); + + // Note, we don't check for an exact match of the encoded user string in Req.Path because it is not determinate - the + // SDK may add custom attributes to the user ("os" etc.) and since we don't canonicalize the JSON representation, + // properties could be serialized in any order causing the encoding to vary. Also, we don't test REPORT mode here + // because it is already covered in FeatureFlagRequestorTest. + Assert.Matches(mode.FlagsPathRegex, req.Path); + + Assert.Equal("", req.RawQuery); + Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + Assert.Null(req.Body); + } + + private void VerifyFlagValues(ILdMobileClient client, IDictionary flags) + { + Assert.True(client.Initialized()); + foreach (var e in flags) + { + Assert.Equal(e.Value, client.StringVariation(e.Key, null)); + } + } + + private JToken FlagJson(string key, string value) + { + var o = new JObject(); + o.Add("key", key); + o.Add("value", value); + return o; + } + + private string PollingData(IDictionary flags) + { + var o = new JObject(); + foreach (var e in flags) + { + o.Add(e.Key, FlagJson(e.Key, e.Value)); + } + return JsonConvert.SerializeObject(o); + } + + private string StreamingData(IDictionary flags) + { + return "event: put\ndata: " + PollingData(flags) + "\n\n"; + } + } + + class UpdateMode + { + public bool IsStreaming { get; private set; } + public string FlagsPathRegex { get; private set; } + + public static readonly UpdateMode Streaming = new UpdateMode + { + IsStreaming = true, + FlagsPathRegex = "^/meval/[^/?]+" + }; + + public static readonly UpdateMode Polling = new UpdateMode + { + IsStreaming = false, + FlagsPathRegex = "^/msdk/evalx/users/[^/?]+" + }; + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 1b212463..0f8ebb7f 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index 5c47ce32..9d324328 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; -using LaunchDarkly.Client; -using Newtonsoft.Json; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; using Xunit; namespace LaunchDarkly.Xamarin.Tests { - public class LdClientEvaluationTests + public class LdClientEvaluationTests : BaseTest { static readonly string appKey = "some app key"; static readonly string nonexistentFlagKey = "some flag key"; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index 89973126..240e9efc 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -4,7 +4,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class LdClientEventTests + public class LdClientEventTests : BaseTest { private static readonly User user = User.WithKey("userkey"); private MockEventProcessor eventProcessor = new MockEventProcessor(); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index d172cf6a..ae66e813 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -7,21 +7,11 @@ namespace LaunchDarkly.Xamarin.Tests { - public class DefaultLdClientTests + public class DefaultLdClientTests : BaseTest { static readonly string appKey = "some app key"; static readonly User simpleUser = User.WithKey("user-key"); - public DefaultLdClientTests() - { - TestUtil.ClearClient(); - } - - ~DefaultLdClientTests() - { - TestUtil.ClearClient(); - } - LdClient Client() { var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); @@ -252,19 +242,19 @@ public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerMan } [Fact] - public void FlagsAreLoadedFromPersistentStorageByDefault() + public void FlagsAreLoadedFromPersistentStorageByDefault() { var storage = new MockPersistentStorage(); var flagsJson = "{\"flag\": {\"value\": 100}}"; - storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); + storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") .WithOffline(true) .WithPersistentStorage(storage) .WithFlagCacheManager(null); // use actual cache logic, not mock component (even though persistence layer is a mock) - using (var client = TestUtil.CreateClient(config, simpleUser)) - { - Assert.Equal(100, client.IntVariation("flag", 99)); - } + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.Equal(100, client.IntVariation("flag", 99)); + } } [Fact] diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs new file mode 100644 index 00000000..b74229db --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Common.Logging; +using Common.Logging.Simple; + +namespace LaunchDarkly.Xamarin.Tests +{ + // This mechanism allows unit tests to capture and inspect log output. In order for it to work properly, all + // tests must derive from BaseTest so that the global Common.Logging configuration is modified before any + // test code runs (i.e. before any SDK code has a change to create a logger instance). + // + // For debugging purposes, this also allows you to mirror all log output to the console (which Common.Logging + // does not do by default) by setting the environment variable LD_TEST_LOGS to any value. + + public class LogSink : AbstractSimpleLogger + { + public static LogSink Instance = new LogSink(); + + private static readonly bool _showLogs = Environment.GetEnvironmentVariable("LD_TEST_LOGS") != null; + + public LogSink() : base("", LogLevel.All, false, false, false, "") {} + + protected override void WriteInternal(LogLevel level, object message, Exception exception) + { + if (_showLogs) + { + Console.WriteLine("*** LOG: [" + level + "] " + message); + } + LogSinkScope.WithCurrent(s => s.Messages.Add(new LogItem { Level = level, Text = message.ToString() })); + } + } + + public struct LogItem + { + public LogLevel Level { get; set; } + public string Text { get; set; } + } + + public class LogSinkFactoryAdapter : AbstractSimpleLoggerFactoryAdapter + { + public LogSinkFactoryAdapter() : base(null) {} + + protected override ILog CreateLogger(string name, LogLevel level, bool showLevel, bool showDateTime, bool showLogName, string dateTimeFormat) + { + return LogSink.Instance; + } + } + + public class LogSinkScope : IDisposable + { + private static Stack _scopes = new Stack(); + + public List Messages = new List(); + + public LogSinkScope() + { + _scopes.Push(this); + } + + public void Dispose() + { + _scopes.Pop(); + } + + public static void WithCurrent(Action a) + { + if (_scopes.TryPeek(out var s)) + { + a(s); + } + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs index 1728fa34..1aeca942 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs @@ -4,7 +4,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class MobilePollingProcessorTests + public class MobilePollingProcessorTests : BaseTest { private const string flagsJson = "{" + "\"int-flag\":{\"value\":15}," + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index 03d4db6d..a7f8b1d0 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -1,17 +1,15 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using LaunchDarkly.Client; using LaunchDarkly.Common; using LaunchDarkly.EventSource; -using Newtonsoft.Json; using Xunit; namespace LaunchDarkly.Xamarin.Tests { - public class MobileStreamingProcessorTests + public class MobileStreamingProcessorTests : BaseTest { private const string initialFlagsJson = "{" + "\"int-flag\":{\"value\":15,\"version\":100}," + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 71d8cb13..be5d187b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -1,30 +1,46 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using LaunchDarkly.Client; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin.Tests { - public class TestUtil + public static class TestUtil { // Any tests that are going to access the static LdClient.Instance must hold this lock, // to avoid interfering with tests that use CreateClient. public static readonly object ClientInstanceLock = new object(); - + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can // instantiate their own independent clients. Application code cannot do this because // the LdClient.Instance setter has internal scope. - public static LdClient CreateClient(Configuration config, User user) + public static LdClient CreateClient(Configuration config, User user, TimeSpan? timeout = null) { + ClearClient(); lock (ClientInstanceLock) { - LdClient client = LdClient.Init(config, user, TimeSpan.FromSeconds(1)); + LdClient client = LdClient.Init(config, user, timeout ?? TimeSpan.FromSeconds(1)); LdClient.Instance = null; return client; } } + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can + // instantiate their own independent clients. Application code cannot do this because + // the LdClient.Instance setter has internal scope. + public static async Task CreateClientAsync(Configuration config, User user) + { + ClearClient(); + LdClient client = await LdClient.InitAsync(config, user); + lock (ClientInstanceLock) + { + LdClient.Instance = null; + } + return client; + } + public static void ClearClient() { lock (ClientInstanceLock) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs index 4bfffcaa..b8b8e0f6 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/UserFlagCacheTests.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class UserFlagCacheTests + public class UserFlagCacheTests : BaseTest { IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); User user1 = User.WithKey("user1Key"); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/WireMockExtensions.cs b/tests/LaunchDarkly.XamarinSdk.Tests/WireMockExtensions.cs new file mode 100644 index 00000000..7e91eb7d --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/WireMockExtensions.cs @@ -0,0 +1,43 @@ +using System; +using WireMock; +using WireMock.Logging; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace LaunchDarkly.Xamarin.Tests +{ + // Convenience methods to streamline the test code's usage of WireMock. + public static class WireMockExtensions + { + public static string GetUrl(this FluentMockServer server) + { + return server.Urls[0]; + } + + public static FluentMockServer ForAllRequests(this FluentMockServer server, Func builderFn) + { + server.Given(Request.Create()).RespondWith(builderFn(Response.Create().WithStatusCode(200))); + return server; + } + + public static IResponseBuilder WithJsonBody(this IResponseBuilder resp, string body) + { + return resp.WithBody(body).WithHeader("Content-Type", "application/json"); + } + + public static IResponseBuilder WithEventsBody(this IResponseBuilder resp, string body) + { + return resp.WithBody(body).WithHeader("Content-Type", "text/event-stream"); + } + + public static RequestMessage GetLastRequest(this FluentMockServer server) + { + foreach (LogEntry le in server.LogEntries) + { + return le.RequestMessage; + } + throw new InvalidOperationException("Did not receive a request"); + } + } +} From eb781c35ec24d07368d8908ff459255c0c77c028 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sun, 23 Jun 2019 16:39:45 -0700 Subject: [PATCH 107/254] fix malformed log message --- src/LaunchDarkly.XamarinSdk/Configuration.shared.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs b/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs index f65d032e..1d31887e 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs @@ -388,7 +388,7 @@ public static Configuration WithPollingInterval(this Configuration configuration { if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) { - Log.WarnFormat("PollingInterval cannot be less than the default of {0}."); + Log.WarnFormat("PollingInterval cannot be less than the default of {0}.", Configuration.MinimumPollingInterval); pollingInterval = Configuration.MinimumPollingInterval; } configuration.PollingInterval = pollingInterval; From fd93cfe35af5c7f233d3ce21204f4b0d27f7286c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sun, 23 Jun 2019 16:40:43 -0700 Subject: [PATCH 108/254] add tests for cached flags in offline mode --- .../LDClientEndToEndTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 4034c769..610f9f31 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -258,6 +258,58 @@ await WithServerAsync(async server => }); } + [Fact] + public void OfflineClientUsesCachedFlagsSync() + { + WithServer(server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + using (var client = TestUtil.CreateClient(config, _user)) + { + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + VerifyFlagValues(client, _flagData1); + } + }); + } + + [Fact] + public async Task OfflineClientUsesCachedFlagsAsync() + { + await WithServerAsync(async server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + VerifyFlagValues(client, _flagData1); + } + }); + } + private Configuration BaseConfig(FluentMockServer server) { return Configuration.Default(_mobileKey) From feed22d2898e018b12f92b259553ab8c9d753def Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sun, 23 Jun 2019 16:41:13 -0700 Subject: [PATCH 109/254] tiny fix to avoid doing ToString twice --- tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs index b74229db..5cee4c8d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LogSink.cs @@ -22,11 +22,12 @@ public LogSink() : base("", LogLevel.All, false, false, false, "") {} protected override void WriteInternal(LogLevel level, object message, Exception exception) { + var str = message?.ToString(); if (_showLogs) { - Console.WriteLine("*** LOG: [" + level + "] " + message); + Console.WriteLine("*** LOG: [" + level + "] " + str); } - LogSinkScope.WithCurrent(s => s.Messages.Add(new LogItem { Level = level, Text = message.ToString() })); + LogSinkScope.WithCurrent(s => s.Messages.Add(new LogItem { Level = level, Text = str })); } } From 1a417f3774411b4b167ad0813be2d5b65dd66e44 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sun, 23 Jun 2019 19:45:54 -0700 Subject: [PATCH 110/254] syntax error --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4de92fef..2dd03e7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,8 +12,8 @@ jobs: test-netstandard2.0: docker: - image: microsoft/dotnet:2.0-sdk-jessie - env: - - ASPNETCORE_SUPPRESSSTATUSMESSAGES: "true" # suppresses annoying debug output from embedded HTTP servers in tests + environment: + ASPNETCORE_SUPPRESSSTATUSMESSAGES: "true" # suppresses annoying debug output from embedded HTTP servers in tests steps: - checkout - run: dotnet restore From bdcc5f1735d084f74f52727243594fcb098d9690 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 10:07:15 -0700 Subject: [PATCH 111/254] comment --- .../FeatureFlagRequestorTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index fd07d09e..5c0dd584 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -91,10 +91,8 @@ await WithServerAsync(async server => Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); //Assert.Equal("{\"key\":\"foo\"}", req.Body); - // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, for unknown - // reasons the current version of WireMock.Net does not seem to be capturing that information; Content-Type and - // Content-Length are correct, and directing the request at a different listener (e.g. "nc -l") shows that the body - // is definitely being sent, but req.Body and req.BodyAsJson are both null no matter what. + // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, WireMock.Net + // is not currently able to detect the body for REPORT requests: https://github.com/WireMock-Net/WireMock.Net/issues/290 } }); } From 4b276c344f3fe65f8da30e7b6eb0f66d6e55259c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 10:16:53 -0700 Subject: [PATCH 112/254] simplify test methods by parameterizing --- .../LDClientEndToEndTests.cs | 108 +++++------------- 1 file changed, 27 insertions(+), 81 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 610f9f31..8fb985a2 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -33,19 +33,15 @@ public class LdClientEndToEndTests : BaseTest { "flag1", "value2" } }; - [Fact] - public void InitGetsFlagsInPollingModeSync() - { - InitGetsFlagsSync(UpdateMode.Polling); - } - - [Fact] - public void InitGetsFlagsInStreamingModeSync() + public static readonly IEnumerable PollingAndStreaming = new List { - InitGetsFlagsSync(UpdateMode.Streaming); - } + { new object[] { UpdateMode.Polling } }, + { new object[] { UpdateMode.Streaming } } + }; - private void InitGetsFlagsSync(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public void InitGetsFlagsSync(UpdateMode mode) { WithServer(server => { @@ -60,23 +56,9 @@ private void InitGetsFlagsSync(UpdateMode mode) }); } - [Fact] - public async Task InitGetsFlagsInPollingModeAsync() - { - await InitGetsFlagsAsync(UpdateMode.Polling); - } - - [Fact] - public async Task InitGetsFlagsInStreamingModeAsync() - { - // Note: since WireMock.Net doesn't seem to support streaming responses, the response will close after the end of the - // data, the SDK will enter retry mode and we may get another identical streaming request. For the purposes of these tests, - // that doesn't matter. The correct processing of a chunked stream is tested in the LaunchDarkly.EventSource tests, and - // the retry logic is tested in LaunchDarkly.CommonSdk. - await InitGetsFlagsAsync(UpdateMode.Streaming); - } - - private async Task InitGetsFlagsAsync(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public async Task InitGetsFlagsAsync(UpdateMode mode) { await WithServerAsync(async server => { @@ -111,19 +93,9 @@ public void InitCanTimeOutSync() }); } - [Fact] - public void InitFailsOn401InPollingModeSync() - { - InitFailsOn401Sync(UpdateMode.Polling); - } - - [Fact] - public void InitFailsOn401InStreamingModeSync() - { - InitFailsOn401Sync(UpdateMode.Streaming); - } - - private void InitFailsOn401Sync(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public void InitFailsOn401Sync(UpdateMode mode) { WithServer(server => { @@ -149,19 +121,9 @@ private void InitFailsOn401Sync(UpdateMode mode) }); } - [Fact] - public async Task InitFailsOn401InPollingModeAsync() - { - await InitFailsOn401Async(UpdateMode.Polling); - } - - [Fact] - public async Task InitFailsOn401InStreamingModeAsync() - { - await InitFailsOn401Async(UpdateMode.Streaming); - } - - private async Task InitFailsOn401Async(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public async Task InitFailsOn401Async(UpdateMode mode) { await WithServerAsync(async server => { @@ -182,19 +144,9 @@ await WithServerAsync(async server => }); } - [Fact] - public void IdentifySwitchesUserAndGetsFlagsInPollingModeSync() - { - IdentifySwitchesUserAndGetsFlagsSync(UpdateMode.Polling); - } - - [Fact] - public void IdentifySwitchesUserAndGetsFlagsInStreamingModeSync() - { - IdentifySwitchesUserAndGetsFlagsSync(UpdateMode.Streaming); - } - - private void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { WithServer(server => { @@ -220,19 +172,9 @@ private void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) }); } - [Fact] - public async Task IdentifySwitchesUserAndGetsFlagsInPollingModeAsync() - { - await IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode.Polling); - } - - [Fact] - public async Task IdentifySwitchesUserAndGetsFlagsInStreamingModeAsync() - { - await IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode.Streaming); - } - - private async Task IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode mode) + [Theory] + [MemberData(nameof(PollingAndStreaming))] + public async Task IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode mode) { await WithServerAsync(async server => { @@ -322,6 +264,10 @@ private void SetupResponse(FluentMockServer server, IDictionary { server.ForAllRequests(r => mode.IsStreaming ? r.WithEventsBody(StreamingData(data)) : r.WithJsonBody(PollingData(data))); + // Note: in streaming mode, since WireMock.Net doesn't seem to support streaming responses, the fake response will close + // after the end of the data-- so the SDK will enter retry mode and we may get another identical streaming request. For + // the purposes of these tests, that doesn't matter. The correct processing of a chunked stream is tested in the + // LaunchDarkly.EventSource tests, and the retry logic is tested in LaunchDarkly.CommonSdk. } private void VerifyRequest(FluentMockServer server, UpdateMode mode) @@ -373,7 +319,7 @@ private string StreamingData(IDictionary flags) } } - class UpdateMode + public class UpdateMode { public bool IsStreaming { get; private set; } public string FlagsPathRegex { get; private set; } From 1205e52a9bf2afe1b6b1bf5084e98b933f56ac3b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 14:04:40 -0700 Subject: [PATCH 113/254] fix test cleanup concurrency problem --- tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 4 ++-- .../LdClientEvaluationTests.cs | 10 ---------- .../LdClientEventTests.cs | 10 ---------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs index 09b1acc5..36ee2c06 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -7,7 +7,7 @@ namespace LaunchDarkly.Xamarin.Tests { [Collection("serialize all tests")] - public class BaseTest + public class BaseTest : IDisposable { public BaseTest() { @@ -15,7 +15,7 @@ public BaseTest() TestUtil.ClearClient(); } - ~BaseTest() + public void Dispose() { TestUtil.ClearClient(); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index 9d324328..25a3cb10 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -9,16 +9,6 @@ public class LdClientEvaluationTests : BaseTest static readonly string appKey = "some app key"; static readonly string nonexistentFlagKey = "some flag key"; static readonly User user = User.WithKey("userkey"); - - public LdClientEvaluationTests() - { - TestUtil.ClearClient(); - } - - ~LdClientEvaluationTests() - { - TestUtil.ClearClient(); - } private static LdClient ClientWithFlagsJson(string flagsJson) { diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index 240e9efc..daa4cc6f 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -8,16 +8,6 @@ public class LdClientEventTests : BaseTest { private static readonly User user = User.WithKey("userkey"); private MockEventProcessor eventProcessor = new MockEventProcessor(); - - public LdClientEventTests() - { - TestUtil.ClearClient(); - } - - ~LdClientEventTests() - { - TestUtil.ClearClient(); - } public LdClient MakeClient(User user, string flagsJson) { From 810179e28690419e23f1e685ff5595e26b66f483 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 15:15:07 -0700 Subject: [PATCH 114/254] make standard test suite run in iOS test project --- .../AppDelegate.cs | 51 +++ .../LDiOSTests.cs | 61 ---- .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 300 +++++++++++++++++- LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs | 4 +- .../UnitTestAppDelegate.cs | 45 --- .../packages.config | 84 ++++- .../LDClientEndToEndTests.cs | 6 +- .../LdClientTests.cs | 2 +- 8 files changed, 437 insertions(+), 116 deletions(-) create mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs new file mode 100644 index 00000000..3cb59b4a --- /dev/null +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Foundation; +using UIKit; + +using Xunit.Runner; +using Xunit.Sdk; + + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + // The UIApplicationDelegate for the application. This class is responsible for launching the + // User Interface of the application, as well as listening (and optionally responding) to + // application events from iOS. + [Register("AppDelegate")] + public partial class AppDelegate : RunnerAppDelegate + { + + // + // This method is invoked when the application has loaded and is ready to run. In this + // method you should instantiate the window, load the UI into it and then make the window + // visible. + // + // You have 17 seconds to return from this method, or iOS will terminate your application. + // + public override bool FinishedLaunching(UIApplication app, NSDictionary options) + { + // We need this to ensure the execution assembly is part of the app bundle + AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); + + + // tests can be inside the main assembly + AddTestAssembly(Assembly.GetExecutingAssembly()); + // otherwise you need to ensure that the test assemblies will + // become part of the app bundle + //AddTestAssembly(typeof(PortableTests).Assembly); + +#if false + // you can use the default or set your own custom writer (e.g. save to web site and tweet it ;-) + Writer = new TcpTextWriter ("10.0.1.2", 16384); + // start running the test suites as soon as the application is loaded + AutoStart = true; + // crash the application (to ensure it's ended) and return to springboard + TerminateAfterExecution = true; +#endif + return base.FinishedLaunching(app, options); + } + } +} \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs deleted file mode 100644 index 8deef83b..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LDiOSTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using NUnit.Framework; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin.iOS.Tests -{ - [TestFixture] - public class LDiOSTests - { - private ILdMobileClient client; - - [SetUp] - public void Setup() { - var user = LaunchDarkly.Client.User.WithKey("test-user"); - var timeSpan = TimeSpan.FromSeconds(10); - client = LdClient.Init("MOBILE_KEY", user, timeSpan); - } - - [TearDown] - public void Tear() { LdClient.Instance = null;} - - [Test] - public void BooleanFeatureFlag() - { - Console.WriteLine("Test Boolean Variation"); - Assert.True(client.BoolVariation("boolean-feature-flag")); - } - - [Test] - public void IntFeatureFlag() - { - Console.WriteLine("Test Integer Variation"); - Assert.True(client.IntVariation("int-feature-flag") == 2); - } - - [Test] - public void StringFeatureFlag() - { - Console.WriteLine("Test String Variation"); - Assert.True(client.StringVariation("string-feature-flag", "false").Equals("bravo")); - } - - [Test] - public void JsonFeatureFlag() - { - string json = @"{ - ""test2"": ""testing2"" - }"; - Console.WriteLine("Test JSON Variation"); - JToken jsonToken = JToken.FromObject(JObject.Parse(json)); - Assert.True(JToken.DeepEquals(jsonToken, client.JsonVariation("json-feature-flag", "false"))); - } - - [Test] - public void FloatFeatureFlag() - { - Console.WriteLine("Test Float Variation"); - Assert.True(client.FloatVariation("float-feature-flag") == 1.5); - } - } -} diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index 04398e77..d8f5c899 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -1,5 +1,8 @@ + + + Debug iPhoneSimulator @@ -94,7 +97,7 @@ ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll - ..\packages\Newtonsoft.Json.9.0.1\lib\netstandard1.0\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.11.0.2\lib\netstandard2.0\Newtonsoft.Json.dll @@ -110,6 +113,243 @@ ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\xamarinios10\Plugin.DeviceInfo.dll + + ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Core.dll + + + ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Platform.dll + + + ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Platform.iOS.dll + + + ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Xaml.dll + + + ..\packages\xunit.runner.devices.2.5.25\lib\xamarinios10\xunit.runner.devices.dll + + + ..\packages\xunit.runner.devices.2.5.25\lib\xamarinios10\xunit.runner.utility.netstandard15.dll + + + ..\packages\xunit.abstractions.2.0.3\lib\netstandard2.0\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.4.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.4.1\lib\netstandard1.1\xunit.execution.dotnet.dll + + + ..\packages\Microsoft.AspNetCore.Diagnostics.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Diagnostics.Abstractions.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.FileSystemGlobbing.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileSystemGlobbing.dll + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\packages\Microsoft.Extensions.ObjectPool.2.1.1\lib\netstandard2.0\Microsoft.Extensions.ObjectPool.dll + + + ..\packages\MimeKitLite.2.0.7\lib\xamarinios\MimeKitLite.dll + + + ..\packages\Fare.2.1.1\lib\netstandard1.1\Fare.dll + + + ..\packages\JmesPath.Net.1.0.125\lib\netstandard2.0\JmesPath.Net.dll + + + ..\packages\NLipsum.1.1.0\lib\netstandard1.0\NLipsum.Core.dll + + + ..\packages\RandomDataGenerator.Net.1.0.8\lib\netstandard2.0\RandomDataGenerator.dll + + + ..\packages\SimMetrics.Net.1.0.5\lib\netstandard2.0\SimMetrics.Net.dll + + + ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll + + + ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll + + + ..\packages\System.Diagnostics.DiagnosticSource.4.5.0\lib\netstandard1.3\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\RestEase.1.4.7\lib\netstandard2.0\RestEase.dll + + + ..\packages\System.Linq.Dynamic.Core.1.0.12\lib\netstandard2.0\System.Linq.Dynamic.Core.dll + + + ..\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll + + + ..\packages\Handlebars.Net.1.9.5\lib\netstandard2.0\Handlebars.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll + + + ..\packages\Microsoft.Extensions.Primitives.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + + + ..\packages\Microsoft.AspNetCore.Http.Features.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Features.dll + + + ..\packages\Microsoft.Extensions.Configuration.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Hosting.Server.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Server.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Configuration.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll + + + ..\packages\Microsoft.Extensions.Configuration.Binder.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll + + + ..\packages\Microsoft.Extensions.Configuration.CommandLine.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.CommandLine.dll + + + ..\packages\Microsoft.Extensions.Configuration.EnvironmentVariables.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.EnvironmentVariables.dll + + + ..\packages\Microsoft.Extensions.FileProviders.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Abstractions.dll + + + ..\packages\Microsoft.Extensions.FileProviders.Physical.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Physical.dll + + + ..\packages\Microsoft.Extensions.Configuration.FileExtensions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.FileExtensions.dll + + + ..\packages\Microsoft.Extensions.Configuration.Json.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Json.dll + + + ..\packages\Microsoft.Extensions.Configuration.UserSecrets.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.UserSecrets.dll + + + ..\packages\Microsoft.Extensions.Hosting.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Hosting.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Options.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Logging.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.dll + + + ..\packages\Microsoft.Extensions.Logging.Debug.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Debug.dll + + + ..\packages\Microsoft.Extensions.Options.ConfigurationExtensions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.ConfigurationExtensions.dll + + + ..\packages\Microsoft.Extensions.Logging.Configuration.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Configuration.dll + + + ..\packages\Microsoft.Extensions.Logging.Console.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Console.dll + + + ..\packages\Microsoft.Net.Http.Headers.2.1.1\lib\netstandard2.0\Microsoft.Net.Http.Headers.dll + + + ..\packages\System.IO.Pipelines.4.5.2\lib\netstandard2.0\System.IO.Pipelines.dll + + + ..\packages\Microsoft.AspNetCore.Connections.Abstractions.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Connections.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.dll + + + ..\packages\System.Security.Principal.Windows.4.5.1\lib\netstandard2.0\System.Security.Principal.Windows.dll + + + ..\packages\System.Text.Encodings.Web.4.5.0\lib\netstandard2.0\System.Text.Encodings.Web.dll + + + ..\packages\Microsoft.AspNetCore.Http.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Authentication.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Hosting.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Http.Extensions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Extensions.dll + + + ..\packages\Microsoft.AspNetCore.HttpOverrides.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.HttpOverrides.dll + + + ..\packages\Microsoft.AspNetCore.Routing.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Routing.Abstractions.dll + + + ..\packages\Microsoft.AspNetCore.Routing.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Routing.dll + + + ..\packages\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll + + + ..\packages\Microsoft.AspNetCore.WebUtilities.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.WebUtilities.dll + + + ..\packages\Microsoft.AspNetCore.Diagnostics.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Diagnostics.dll + + + ..\packages\Microsoft.AspNetCore.Http.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.dll + + + ..\packages\Microsoft.AspNetCore.Authentication.Core.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Core.dll + + + ..\packages\Microsoft.AspNetCore.HostFiltering.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.HostFiltering.dll + + + ..\packages\Microsoft.AspNetCore.Hosting.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.dll + + + ..\packages\Microsoft.AspNetCore.Server.IISIntegration.2.1.2\lib\netstandard2.0\Microsoft.AspNetCore.Server.IISIntegration.dll + + + ..\packages\Microsoft.AspNetCore.Server.Kestrel.Core.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Core.dll + + + ..\packages\Microsoft.AspNetCore.Server.Kestrel.Https.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Https.dll + + + ..\packages\Microsoft.AspNetCore.Server.Kestrel.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.dll + + + ..\packages\Microsoft.AspNetCore.2.1.4\lib\netstandard2.0\Microsoft.AspNetCore.dll + + + ..\packages\XPath2.1.0.6.1\lib\netstandard2.0\XPath2.dll + + + ..\packages\XPath2.Extensions.1.0.6.1\lib\netstandard2.0\XPath2.Extensions.dll + + + ..\packages\WireMock.Net.1.0.20\lib\netstandard2.0\WireMock.Net.dll + @@ -121,9 +361,63 @@ - - + + + SharedTestCode\BaseTest.cs + + + SharedTestCode\ConfigurationTest.cs + + + SharedTestCode\FeatureFlagListenerTests.cs + + + SharedTestCode\FeatureFlagRequestorTests.cs + + + SharedTestCode\FeatureFlagTests.cs + + + SharedTestCode\FlagCacheManagerTests.cs + + + SharedTestCode\LdClientEndToEndTests.cs + + + SharedTestCode\LdClientEvaluationTests.cs + + + SharedTestCode\LdClientEventTests.cs + + + SharedTestCode\LdClientTests.cs + + + SharedTestCode\LogSink.cs + + + SharedTestCode\MobilePollingProcessorTests.cs + + + SharedTestCode\MockComponents.cs + + + SharedTestCode\TestUtil.cs + + + SharedTestCode\UserFlagCacheTests.cs + + + SharedTestCode\WireMockExtensions.cs + + + + + + + + \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs index c040831c..96b290eb 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs @@ -12,9 +12,9 @@ public class Application // This is the main entry point of the application. static void Main(string[] args) { - // if you want to use a different Application Delegate class from "UnitTestAppDelegate" + // if you want to use a different Application Delegate class from "AppDelegate" // you can specify it here. - UIApplication.Main(args, null, "UnitTestAppDelegate"); + UIApplication.Main(args, null, "AppDelegate"); } } } diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs deleted file mode 100644 index d5e666aa..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/UnitTestAppDelegate.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; - -using Foundation; -using UIKit; -using MonoTouch.NUnit.UI; - -namespace LaunchDarkly.Xamarin.iOS.Tests -{ - // The UIApplicationDelegate for the application. This class is responsible for launching the - // User Interface of the application, as well as listening (and optionally responding) to - // application events from iOS. - [Register("UnitTestAppDelegate")] - public partial class UnitTestAppDelegate : UIApplicationDelegate - { - // class-level declarations - UIWindow window; - TouchRunner runner; - - // - // This method is invoked when the application has loaded and is ready to run. In this - // method you should instantiate the window, load the UI into it and then make the window - // visible. - // - // You have 17 seconds to return from this method, or iOS will terminate your application. - // - public override bool FinishedLaunching(UIApplication app, NSDictionary options) - { - // create a new window instance based on the screen size - window = new UIWindow(UIScreen.MainScreen.Bounds); - runner = new TouchRunner(window); - - // register every tests included in the main application/assembly - runner.Add(System.Reflection.Assembly.GetExecutingAssembly()); - - window.RootViewController = new UINavigationController(runner.GetViewController()); - - // make the window visible - window.MakeKeyAndVisible(); - - return true; - } - } -} diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config b/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config index 6aedd5ad..8a78858e 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config @@ -2,18 +2,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + @@ -23,34 +82,57 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 8fb985a2..37dd9052 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -163,7 +163,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) SetupResponse(server, _flagData2, mode); client.Identify(_otherUser); - Assert.Equal(_otherUser, client.User); + Assert.Equal(_otherUser.Key, client.User.Key); // don't compare entire user, because SDK may have added device/os attributes VerifyRequest(server, mode); Assert.NotEqual(user1RequestPath, server.GetLastRequest().Path); @@ -191,7 +191,7 @@ await WithServerAsync(async server => SetupResponse(server, _flagData2, mode); await client.IdentifyAsync(_otherUser); - Assert.Equal(_otherUser, client.User); + Assert.Equal(_otherUser.Key, client.User.Key); // don't compare entire user, because SDK may have added device/os attributes VerifyRequest(server, mode); Assert.NotEqual(user1RequestPath, server.GetLastRequest().Path); @@ -257,7 +257,7 @@ private Configuration BaseConfig(FluentMockServer server) return Configuration.Default(_mobileKey) .WithBaseUri(server.GetUrl()) .WithStreamUri(server.GetUrl()) - .WithEventProcessor(new NullEventProcessor()); + .WithEventProcessor(new MockEventProcessor()); } private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index ae66e813..45afe92e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -53,7 +53,7 @@ public void IdentifyUpdatesTheUser() { var updatedUser = User.WithKey("some new key"); client.Identify(updatedUser); - Assert.Equal(client.User, updatedUser); + Assert.Equal(client.User.Key, updatedUser.Key); // don't compare entire user, because SDK may have added device/os attributes } } From d55c90a09f29e4c086581512d0e553cf43166aa0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 16:39:00 -0700 Subject: [PATCH 115/254] run iOS tests in CI --- .circleci/config.yml | 46 +++++++++++++++++-- .../AppDelegate.cs | 8 +--- .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 5 ++ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2dd03e7c..b4cce793 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,17 +57,55 @@ jobs: xcode: "10.2.1" steps: - - run: - name: Set up package source for Xamarin tools - command: brew install caskroom/cask/brew-cask + - restore_cache: + key: homebrew - run: name: Install Xamarin tools - command: brew cask install xamarin xamarin-ios && brew install mono + command: | + brew install caskroom/cask/brew-cask + brew cask install xamarin xamarin-ios && brew install mono # Note, "mono" provides the msbuild CLI tool + - save_cache: + key: homebrew + paths: + - /usr/local/Homebrew + - checkout - run: name: Build SDK command: msbuild /restore /p:TargetFramework=Xamarin.iOS10 src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj + + - run: + name: Build test project + command: msbuild LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj + + - run: + name: Start simulator + command: | + xcrun simctl create xm-ios com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-2 + xcrun simctl boot xm-ios + + - run: + name: Load test app into simulator + command: xcrun simctl install "xm-ios" LaunchDarkly.XamarinSdk.iOS.Tests/bin/iPhoneSimulator/Debug/LaunchDarkly.XamarinSdk.iOS.Tests.app + + - run: + name: Start capturing log output + command: xcrun simctl spawn booted log stream --predicate 'senderImagePath contains "LaunchDarkly.XamarinSdk.iOS.Tests"' >test-run.log + background: true + + - run: + name: Launch test app in simulator + command: xcrun simctl launch "xm-ios" com.launchdarkly.LaunchDarkly-Xamarin-iOS-Tests + + - run: + name: Wait for tests to finish running + # https://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found + command: "( tail -f -n0 test-run.log & ) | grep -q 'Tests run:'" + + - run: + name: Show test output + command: cat test-run.log diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs index 3cb59b4a..aa7f8ce1 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs @@ -8,7 +8,6 @@ using Xunit.Runner; using Xunit.Sdk; - namespace LaunchDarkly.Xamarin.iOS.Tests { // The UIApplicationDelegate for the application. This class is responsible for launching the @@ -17,7 +16,6 @@ namespace LaunchDarkly.Xamarin.iOS.Tests [Register("AppDelegate")] public partial class AppDelegate : RunnerAppDelegate { - // // This method is invoked when the application has loaded and is ready to run. In this // method you should instantiate the window, load the UI into it and then make the window @@ -29,7 +27,6 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary options) { // We need this to ensure the execution assembly is part of the app bundle AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); - // tests can be inside the main assembly AddTestAssembly(Assembly.GetExecutingAssembly()); @@ -37,14 +34,13 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary options) // become part of the app bundle //AddTestAssembly(typeof(PortableTests).Assembly); -#if false // you can use the default or set your own custom writer (e.g. save to web site and tweet it ;-) - Writer = new TcpTextWriter ("10.0.1.2", 16384); + // Writer = new TcpTextWriter ("10.0.1.2", 16384); // start running the test suites as soon as the application is loaded AutoStart = true; // crash the application (to ensure it's ended) and return to springboard TerminateAfterExecution = true; -#endif + return base.FinishedLaunching(app, options); } } diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index d8f5c899..cc14f2f4 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -83,6 +83,11 @@ NSUrlSessionHandler + + + + + ..\src\LaunchDarkly.XamarinSdk\bin\Debug\xamarin.ios10\LaunchDarkly.XamarinSdk.dll From 732f31f43ef3115c77499febefe8badecda20faf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 16:40:41 -0700 Subject: [PATCH 116/254] syntax error --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b4cce793..8e35c19e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,7 +58,7 @@ jobs: steps: - restore_cache: - key: homebrew + key: homebrew - run: name: Install Xamarin tools @@ -104,7 +104,7 @@ jobs: - run: name: Wait for tests to finish running # https://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found - command: "( tail -f -n0 test-run.log & ) | grep -q 'Tests run:'" + command: "( tail -f -c+0 test-run.log & ) | grep -q 'Tests run:'" - run: name: Show test output From 3bd1f8f21c11b2e97b8eb972661a0e336a88a6a4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 16:49:27 -0700 Subject: [PATCH 117/254] misc build fixes --- .circleci/config.yml | 2 +- .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e35c19e..1441af98 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,7 +80,7 @@ jobs: - run: name: Build test project - command: msbuild LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj + command: msbuild /restore LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj - run: name: Start simulator diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index cc14f2f4..ec344391 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -89,9 +89,6 @@ - - ..\src\LaunchDarkly.XamarinSdk\bin\Debug\xamarin.ios10\LaunchDarkly.XamarinSdk.dll - @@ -419,6 +416,12 @@ + + + {7717A2B2-9905-40A7-989F-790139D69543} + LaunchDarkly.XamarinSdk + + From 39d124bf116125ac630608524778348294d98cfa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Jun 2019 16:50:09 -0700 Subject: [PATCH 118/254] cleanup boilerplate code --- .../AppDelegate.cs | 32 ++++--------------- LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs | 10 +----- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs index aa7f8ce1..4beefc8f 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Foundation; using UIKit; @@ -10,35 +7,20 @@ namespace LaunchDarkly.Xamarin.iOS.Tests { - // The UIApplicationDelegate for the application. This class is responsible for launching the - // User Interface of the application, as well as listening (and optionally responding) to - // application events from iOS. + // This is based on code that was generated automatically by the xunit.runner.devices package. + // It configures the test-runner app that is implemented by that package, telling it where to + // find the tests, and also configuring it to run them immediately and then quit rather than + // waiting for user input. Output from the test run goes to the system log. + [Register("AppDelegate")] public partial class AppDelegate : RunnerAppDelegate { - // - // This method is invoked when the application has loaded and is ready to run. In this - // method you should instantiate the window, load the UI into it and then make the window - // visible. - // - // You have 17 seconds to return from this method, or iOS will terminate your application. - // public override bool FinishedLaunching(UIApplication app, NSDictionary options) { - // We need this to ensure the execution assembly is part of the app bundle AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); - - // tests can be inside the main assembly AddTestAssembly(Assembly.GetExecutingAssembly()); - // otherwise you need to ensure that the test assemblies will - // become part of the app bundle - //AddTestAssembly(typeof(PortableTests).Assembly); - - // you can use the default or set your own custom writer (e.g. save to web site and tweet it ;-) - // Writer = new TcpTextWriter ("10.0.1.2", 16384); - // start running the test suites as soon as the application is loaded - AutoStart = true; - // crash the application (to ensure it's ended) and return to springboard + + AutoStart = true; TerminateAfterExecution = true; return base.FinishedLaunching(app, options); diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs index 96b290eb..fa24461b 100644 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs +++ b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs @@ -1,19 +1,11 @@ -using System; -using System.Linq; -using System.Collections.Generic; - -using Foundation; -using UIKit; +using UIKit; namespace LaunchDarkly.Xamarin.iOS.Tests { public class Application { - // This is the main entry point of the application. static void Main(string[] args) { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. UIApplication.Main(args, null, "AppDelegate"); } } From e166d731bd6c066238d0b838e77c153f1627c6e2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 25 Jun 2019 13:38:10 -0700 Subject: [PATCH 119/254] add a new iOS test project made from scratch --- .circleci/config.yml | 6 +- LaunchDarkly.XamarinSdk.sln | 28 +-- .../AppDelegate.cs | 29 +++ .../AppIcon.appiconset/Contents.json | 117 ++++++++++ .../AppIcon.appiconset/Icon1024.png | Bin 0 -> 70429 bytes .../AppIcon.appiconset/Icon120.png | Bin 0 -> 3773 bytes .../AppIcon.appiconset/Icon152.png | Bin 0 -> 4750 bytes .../AppIcon.appiconset/Icon167.png | Bin 0 -> 4692 bytes .../AppIcon.appiconset/Icon180.png | Bin 0 -> 5192 bytes .../AppIcon.appiconset/Icon20.png | Bin 0 -> 1313 bytes .../AppIcon.appiconset/Icon29.png | Bin 0 -> 845 bytes .../AppIcon.appiconset/Icon40.png | Bin 0 -> 1101 bytes .../AppIcon.appiconset/Icon58.png | Bin 0 -> 1761 bytes .../AppIcon.appiconset/Icon60.png | Bin 0 -> 2537 bytes .../AppIcon.appiconset/Icon76.png | Bin 0 -> 2332 bytes .../AppIcon.appiconset/Icon80.png | Bin 0 -> 2454 bytes .../AppIcon.appiconset/Icon87.png | Bin 0 -> 2758 bytes .../Entitlements.plist | 6 + .../Info.plist | 48 +++++ .../LaunchDarkly.XamarinSdk.iOS.Toasts.csproj | 204 ++++++++++++++++++ .../LaunchScreen.storyboard | 27 +++ .../LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs | 12 ++ .../Main.storyboard | 27 +++ .../Properties/AssemblyInfo.cs | 36 ++++ .../Resources/LaunchScreen.xib | 43 ++++ 25 files changed, 566 insertions(+), 17 deletions(-) create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon1024.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon120.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon152.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon167.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon180.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon20.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon29.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon40.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon58.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon60.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon76.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon80.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon87.png create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.storyboard create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/Resources/LaunchScreen.xib diff --git a/.circleci/config.yml b/.circleci/config.yml index 1441af98..7d47f923 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,7 +80,7 @@ jobs: - run: name: Build test project - command: msbuild /restore LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj + command: msbuild /restore tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj - run: name: Start simulator @@ -90,7 +90,7 @@ jobs: - run: name: Load test app into simulator - command: xcrun simctl install "xm-ios" LaunchDarkly.XamarinSdk.iOS.Tests/bin/iPhoneSimulator/Debug/LaunchDarkly.XamarinSdk.iOS.Tests.app + command: xcrun simctl install "xm-ios" tests/LaunchDarkly.XamarinSdk.iOS.Tests/bin/iPhoneSimulator/Debug/LaunchDarkly.XamarinSdk.iOS.Tests.app - run: name: Start capturing log output @@ -99,7 +99,7 @@ jobs: - run: name: Launch test app in simulator - command: xcrun simctl launch "xm-ios" com.launchdarkly.LaunchDarkly-Xamarin-iOS-Tests + command: xcrun simctl launch "xm-ios" com.launchdarkly.XamarinSdkTests - run: name: Wait for tests to finish running diff --git a/LaunchDarkly.XamarinSdk.sln b/LaunchDarkly.XamarinSdk.sln index 348c10fb..9611317e 100644 --- a/LaunchDarkly.XamarinSdk.sln +++ b/LaunchDarkly.XamarinSdk.sln @@ -6,10 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Tests", "tests\LaunchDarkly.XamarinSdk.Tests\LaunchDarkly.XamarinSdk.Tests.csproj", "{F6B71DFE-314C-4F27-A219-A14569C8CF48}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.iOS.Tests", "LaunchDarkly.XamarinSdk.iOS.Tests\LaunchDarkly.XamarinSdk.iOS.Tests.csproj", "{066AA0F9-449A-48F5-9492-D698F0EFD923}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Android.Tests", "LaunchDarkly.XamarinSdk.Android.Tests\LaunchDarkly.XamarinSdk.Android.Tests.csproj", "{0B18C336-C770-42C1-B77A-E4A49F789677}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.iOS.Tests", "tests\LaunchDarkly.XamarinSdk.iOS.Tests\LaunchDarkly.XamarinSdk.iOS.Tests.csproj", "{5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,18 +44,6 @@ Global {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.ActiveCfg = Debug|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.Build.0 = Debug|Any CPU - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|Any CPU.ActiveCfg = Release|iPhone - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|Any CPU.Build.0 = Release|iPhone - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhone.ActiveCfg = Release|iPhone - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhone.Build.0 = Release|iPhone - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhone.ActiveCfg = Debug|iPhone - {066AA0F9-449A-48F5-9492-D698F0EFD923}.Debug|iPhone.Build.0 = Debug|iPhone {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.Build.0 = Debug|Any CPU {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -68,6 +56,18 @@ Global {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.ActiveCfg = Debug|Any CPU {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.Build.0 = Debug|Any CPU + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|Any CPU.ActiveCfg = Release|iPhone + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|Any CPU.Build.0 = Release|iPhone + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|iPhone.ActiveCfg = Release|iPhone + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|iPhone.Build.0 = Release|iPhone + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhone.ActiveCfg = Debug|iPhone + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhone.Build.0 = Debug|iPhone EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs new file mode 100644 index 00000000..fa931387 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Foundation; +using UIKit; + +using Xunit.Runner; +using Xunit.Sdk; + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + // This is based on code that was generated automatically by the xunit.runner.devices package. + // It configures the test-runner app that is implemented by that package, telling it where to + // find the tests, and also configuring it to run them immediately and then quit rather than + // waiting for user input. Output from the test run goes to the system log. + + [Register("AppDelegate")] + public partial class AppDelegate : RunnerAppDelegate + { + public override bool FinishedLaunching(UIApplication app, NSDictionary options) + { + AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); + AddTestAssembly(Assembly.GetExecutingAssembly()); + + AutoStart = true; + TerminateAfterExecution = true; + + return base.FinishedLaunching(app, options); + } + } +} \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Contents.json b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..98f4d035 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,117 @@ +{ + "images": [ + { + "scale": "2x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon40.png" + }, + { + "scale": "3x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon60.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon58.png" + }, + { + "scale": "3x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon87.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon80.png" + }, + { + "scale": "3x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "2x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "3x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon180.png" + }, + { + "scale": "1x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon20.png" + }, + { + "scale": "2x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "1x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon29.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon58.png" + }, + { + "scale": "1x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon80.png" + }, + { + "scale": "1x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon76.png" + }, + { + "scale": "2x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon152.png" + }, + { + "scale": "2x", + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon167.png" + }, + { + "scale": "1x", + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon1024.png" + } + ], + "properties": {}, + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon1024.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon1024.png new file mode 100644 index 0000000000000000000000000000000000000000..9174c989a9c8b8a5ca133228f4ed7c173fffd2ee GIT binary patch literal 70429 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 z zCn)b8&=oMu6ygwwJfdasJX|@L6?m1Dv0X9t>JAWO^UIj0#&(3UrHx;vP^3g= zL{(XT!?`D*pP8)WoGHYEZZc$!odTzb8n)q0|88*>6P z`?6&CSv_W7r2yF0beQ2*?V^_%pKktVAo`)T^26X@NpK_*-ni{D7{Sp{C0A<|16l(; zOL*xGW|*sKsiwHvE!h3QXe@^a#6W3}8!DQu-h?A_4gkeRYkt4NC~GR5P8eyp;9kVQ8$QG$5ad7Fo23Z~ak1jY~RXG{v?3G$RarFe`XePu3X{R+=mBOw&X zks)|Sc$RcG-jhn!`~-x|vg!&DA&@}QH^RNdyy9nq56yrU$^qAaS+F_NOaeFb)CVaH z?!UvPajgrK&zqdAs>&Def#wkcG_UhmYOVw^M`VZz@+4IWAVzK%`+za9rm2SD9={u@ zlx5D6UDL;lc7#9`+%vnlP3PescU=N`DHQPt_N55GNBMkVCRMR4?fvp zAFsvcHN4c9rb>J@{*IH>RTr9de%9i4Gd(cbFa9SP4anhoP;TA0!oZyB8?lNMDHPHK zCaOaFU9?x2A!o>p>mCF9r+hKs9Czu_P1l$LWU%}q#)=T3p`ZnYyeHmsewqw`}L^4LuHqfo+CG6<2n7#l^3;H^^!1 zsaieYFnN)Kc7Mv}^xE)4kXUw8<9I+jMB@QV9T9I8haLDt1Ne#exWUfGYG$4uMoEu& zo81#2up18Y40h%tIsOZglp(ltVsE*j1~$lVd|;rN)&${~o~-%KZnJp&3|OFR{^8E9 zJ;fCu53Ysw%}@VYWE*z7r)&4P=^B-SF%a@>*9g84<4aFUZT7x)qdsS+#2tu5NbpU@ zg;EwV)l-#sK>#r9>(0Figx{9lKm>KvRj;y<8 zc8SxMW4<11(s@QMV_}n9MRzA*62->vzxmHh1)GVASEJY7LVtRw`Rv{v`(Fuc00(&o z%m>gS2aJekmdNQ4p<{pD3HqZ-%4hdU1__xYhLi9mTJXD|E zE`t6SX)}l_DY5vO0Xrs#O6_DKtPKn0f+e~SprDYmJL_`<053iA5P`zn z4<5etc%aF58sHFr#M;U-9|=;l)J#Q2vS!Q9(d(EX6fubL%uA_lqa2%!cpNIv78QZ}Ayo(>C(ZpsRtKhzD--fpuoCch87cX-Bna9_{z%$b*dHM0?+T&Hk!+^UM`r|vq z2Id$??bX^|tfYaE+h#Nik(ZcN+wt)28q^gWe!y8jDCXrD<2qV#49x@5$8&Zrd5NTs zNYcix;9fe#PQQ;T?!6hG>9K{K+RCPqiGc9z%t{=`QaX>7O{l(+#7mJ1>Rae^J?82e z6cLqLypskTCyu>uc~$0-XZ^1Qvhwr+pKQ#CKImhGu*MGM*ZrROuAHWuT*yM$ieEy8*KLFMMdLZL|D+yDmy@3_PELTEVMI6nwfcYA3ZQ9wwKdtkT z;`;z7fU{U6>CS7kr3=A-()_G*G(Mjf2wXKe

Fpy)y!S(AQHSG#udd_8#b4sQu!R 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 PgFh2POM|ilu9yD@)d*=D literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon120.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon120.png new file mode 100644 index 0000000000000000000000000000000000000000..9c60a1761dbf62cc2a45ff98b9fdb63ade16e4d9 GIT binary patch literal 3773 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 Dh-f%Y literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon152.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon152.png new file mode 100644 index 0000000000000000000000000000000000000000..448d6efb577d07e227a5c62545173ddf6bd86b55 GIT binary patch literal 4750 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon167.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon167.png new file mode 100644 index 0000000000000000000000000000000000000000..8524768f8d764da7e9c452a444208708f2d18ff1 GIT binary patch literal 4692 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon180.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon180.png new file mode 100644 index 0000000000000000000000000000000000000000..60a64703c0f11d08705cd607f7751338707f5919 GIT binary patch literal 5192 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 nB>N_)Q7iwV%v7MwAoJ}E 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 T3-0p800000NkvXXu0mjfSlal| literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon58.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon58.png new file mode 100644 index 0000000000000000000000000000000000000000..1ad04f004b638bf781012290d78e4138f97bbe5e GIT binary patch literal 1761 zcmV<71|Io|P)4P%ubY|S^%$zf~ zmwTOa@12BA$oV-Y9!V&U%c=j==#_}M2ylE}1m>yyDoGsZ#Yy zNX}RO*f(MzmKS&u`qiajIyW{Y_LC%m2NqT@Ic|QpvYqwNgBK7n5X%c(3k^?2>EOA` zqGaXjE7H9BiJ55fh0iJRW}@=&(@R^E1hLB>kE%PS6eP@VZVdtn(fh;5DPKg!j;fJZ%)wH{Wn#~V&#n(o1URS zsyS`0Tu2m;-H}z9O^h`!UZAFr@?0a7Z;pYOi0uZhgzh=rOEDi`FIkKtVu*gEcSM!h zmb#_XR$akjlg$JI75MXgWkG7IUnYJ+X=1J!qJ%jXVj{L1I2QU%?=?DgV^U?)92DZm zV?>``xT1#kZdgUt!2n?|0>*6ae4tikA9FAlJ}kjmMQm_z3LB5sZYHBKdbex9_Hv@K z%Y?q@9-)b7vJ6X$3h0B4tH__=#*`9^efY@IQfghn*=E2Nb8sR8lrQlu`Ca_Rmm6>Z z7bkEe^w8M>x;hoNUvWu_GZJPVpI;bMTsBpf(@U$Ch(-gk0T#WpsaB1{7ISQ~Y48mW z;Nk?@LjM_?q{BV-D=veoOmJoncVDC1GwGGz(O5@o7ZkGCIJMPO(7K9b6M_wF?Xqd< zo4J6KF0_U2<1=T3x0qc6G6#g+^=N{QTpChC!GZuCY*|eU{Rw)LMN7a2wwbaCdn_dE zzy+Lip(XU4-+SoTFyEcnH3?HRV^%-;Ylx;|>8v&^Dy459ZJ_1zio`68!6s8SO(6sq zaN75WUKiF9+8MruR3=w5)hzA^Z1clVBuXc)+8@e);xX7bfygR&FsIIt-gQ+==(c;S z#J*CO1qZBF&M&6TRmskZXaMOU6&?jn_(BqY5 z>Y|^?uOh;yp6w0QR1`>tiEz_-{Zu!N#(nhRndJV$7;LOgPyZQ*J2yCin+~*u!qKj# zItsG2IDi{ZH+E6j=D|ht=qWxKNxaA6E>3Wna>=z1gy?*#>|g_1^BGspGro&OWRc(k zPP)(*y0WZ7Z-kICr3#g7($P*LCjE>7S`Xh~s!b~bPTo0XSkRm2T(M`QZbv-`Imx*Ulqk*aI* zaqa*=>61?nFb8w3wuiA&zyVyGz>!cm-pY3xEsMEiY)Th4FVrtqWp%V~gxW82)>4^N z*H{>GR?m$^s6BN^M=4^iEjT7(gM(8z7K#9Sn(M_`)oSH2zHDCla&cmK)bb|4nAMIO ze4S3gI9L8AD+T9c#C8K8JF)I68NBWK`5pE`q^OR#h~-u!e7P2i$UDq7^*uZr< zbp8sR<$nYxb8EKH|BltapZ^j3+PZt_u^PR*lT6;TCNxu^yFH$j(!JXvbmHfQ>a0>O z+2k{tOWD%ln$M`tD&>+*KBKvmEgi1;jOwgXF4_DG_&r&PcxYTT00000NkvXXu0mjf DptxI> literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon60.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon60.png new file mode 100644 index 0000000000000000000000000000000000000000..2dd52620a8f15577e56ec7fe8e671988dd17ab0f GIT binary patch literal 2537 zcmVPx;qe(e5T3KvW#~J?bH8u`*(}F|NhUAh32zJ7f2pkEptATfox2hG|f7uRZ{7dCNS$k!NW<#`m*kmICFk!tEERe?wf;US8WE@{jE&0m>|Jvej|>M> z;}l{M410%2UXA^??LK1KUtXD`AK%hILYdpqOYm}jd|d2*vUflbr7=@gMVU;7I#%CF z@SuWG2sQ%&918h74YaTD*aGv;+AQTqN5oz<01TzPIk(tG2RHC)Oto8borfrs^}7gN zF!0O!ZL|rUwN^S4hA}b>1W0*CHMt$_V-H7zAj?vl8)k`5Wh7)hSE9{k;3KXpjEST? zyAtCpxAT4RJG`f#!jYeN;}3`dhi!QGDD__Pms*o=2;Q3&*n7JY@CXS z1A}DayC2el%Okb`@$^RzFQ-}6RlfRwWDuf1?F;?B_%D4vLcI8h@zH?@Uk5%sKz?jY zE--lQqcc*cHy<%RN&rTe4vc{fD|s|{!}Nvzb4n*qL#$F!+k1Ib8g;tM7MVh;&Hw0^ zHrxzxmL_Im9g4l@zZOJ&$II`Q=A;fcLws^Wvl+h~tL~6_G*g_7@l^rfhsCq&rHq?z zgsu7OVLCnP%`?)-YN}MIeEi{MR8wW-O-KgvzMt{D%M+A#lQNJVV5v5tv@!C8v0O9G zpX2SFy=XH~&CdRGgMSu5qfc#vow6`tKuQ7|ts==bqf*NiXVw#sL$c>+A*Ux#X=9QeoXNk1y=(v1+_xsNnr=_n4JJDcnH= z1vdTjbD3RRZ=OS#X%R`-0GgV@IGt#3wyUKa>T0xH9UY^_KlhO?61JOjZ}d=R#tiWa zgl%J?tv{Ge`@g(Ij~@6;>LIito2SE%ctM~mIa079B8*evT9@>M(56{cw5M%ZBx_BCarzS`uN)?I57hG zdX&TI-G_*(ytz59ld*GOJ-e2+ue~P@P1+J&4WSv1D6o%_1)kU2s3+$1{g;L%TuPE0 zEBNix=Tli~3xQJW|9;G_3N6P9e*C~EVqGX@M5RO^+%26Puf;*6U~CWJVla|b2U|yM zC7qQD>$KFPtr!S^X3P5nadM-Bz2}df^$|ADxlU3kh@UWs08prz2NO~(l4dC`oe+$W z2LWRggj$SDoF<|`2u3{@hYXMA*)v5b6zD9DU<7+^-sh#`|1mUfAyn||Cocs07EHk$ zfIzRnE`|aMJr{?4G-@>>)-VVN#^zgh_%?xO^{}a0$wD<18D=dIL9_GBWkX{Z0)o50 z8noN}WoCp>7Vw*;lt-K|t`EYnwvjD~Y+r#|WV;U{m*T32jmCXjv3V zlP&l|Uf=@)f{|^QN%;UH2!;RvGQPy0+G8vn(88fDu~MR()Oa@xzV3BPt(u8qKrosP z{&czdWbm%miU59xK=dExZ&8BlT&qFzoos<_t*-@(0E7yjQ(H|p@bY0>u)XyzA?|{; z#RUVxAL~9L^`cbqJ4OYp?fJQvK^Fw)78!GmjOS^=?!ywy+X^VXSPTJ{Ftni_b+>W` zAL*PZ2(=i<$no4=?`=oH%)OLhSUs$b6AIc$!Dz%51WZZ+SbM)Uu|(0v3I=T$7`I>0G94Y?ZF+6cDa1(dN?r|khZUI(Dll( zGxVoZ=V{>T2#q*lSXw@cSHqE3uC9iDHNSzLXq=a7c~{!F=cLTiPjwxmz2|t-Q%qDq zAi}>&K!YrKvNPLms;57;Hdew?Xe%}tKL#Ac-qbR-Vyzqo57ILRim4DbFnw(s6p|go@E(~?bHK%`eB7(`HNSZz)L2!NEuxKG zADi?5>T&ee!3JrLLJh?eb!Y>Q0#Xa$0bVYM!`KOMICOzdr9kQ){$g;59(e004HtN0 z?s(l6sK$7PEb@{uMFbckNg7UH2#B%KIQD3;WuUA*Ju_3F_a0gjnO||~QW<>g;vlVi zr=RlH4`D7N`#sTU^d3V8=WsN6gm>E^amE4{pmMVLaoY1>6E#}@;&>Rrdn$u*#y!jl zlDM9AS*tSA(`yz|OECusJR~A9Slzl!`|zE6ryVdj4Va$hG+@|~xXUBeH{3dx|6(d9 za$*|%)MXn61%BUunqK0|1|&s+Tdo|@(PkJ?PG#_`KWw7*dEb@P5j>g%>UAW}HHWP< z@|y++D!qJZqFvj7E7^VyGE&Ro86LVp$25@2U@+RcY7zbV_BqDrD20-Yl@kLjPkfvVNgv$SlI14Xv{YYdN94Fvf zYfHTjUu%k&tIxE-<$CU$LO0#R-;|yzSI_?e;Lg?$;O{=K00000NkvXXu0mjfz>L20 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon76.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon76.png new file mode 100644 index 0000000000000000000000000000000000000000..b058cae2f440e5a5875e45c036c99f1fb6356046 GIT binary patch literal 2332 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 Kc&(xHv;P1B$EH01 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon87.png b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Assets.xcassets/AppIcon.appiconset/Icon87.png new file mode 100644 index 0000000000000000000000000000000000000000..4954a4bd33f613d45f74dc0b12beb516e3b38661 GIT binary patch literal 2758 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 z + + + + + diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist new file mode 100644 index 00000000..e17844fd --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDisplayName + LaunchDarkly.XamarinSdk.iOS.Tests + CFBundleIdentifier + com.launchdarkly.XamarinSdkTests + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 10.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIMainStoryboardFile~ipad + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj new file mode 100644 index 00000000..627ffabe --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj @@ -0,0 +1,204 @@ + + + + Debug + iPhoneSimulator + {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32} + {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {edc1b0fa-90cd-4038-8fad-98fe74adb368} + Exe + LaunchDarkly.XamarinSdk.iOS.Toasts + LaunchDarkly.XamarinSdk.iOS.Toasts + Resources + true + NSUrlSessionHandler + + + true + full + false + bin\iPhoneSimulator\Debug + DEBUG + prompt + 4 + false + x86_64 + None + true + + + none + true + bin\iPhoneSimulator\Release + prompt + 4 + None + x86_64 + false + + + true + full + false + bin\iPhone\Debug + DEBUG + prompt + 4 + false + ARM64 + Entitlements.plist + iPhone Developer + true + + + none + true + bin\iPhone\Release + prompt + 4 + Entitlements.plist + ARM64 + false + iPhone Developer + + + + + + + + + + + + 3.4.1 + + + 2.5.25 + + + 2.4.1 + + + 4.0.0.497661 + + + 1.0.20 + + + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + + + + + + + + + + + + + + + + + SharedTestCode\BaseTest.cs + + + SharedTestCode\ConfigurationTest.cs + + + SharedTestCode\FeatureFlagListenerTests.cs + + + SharedTestCode\FeatureFlagRequestorTests.cs + + + SharedTestCode\FeatureFlagTests.cs + + + SharedTestCode\FlagCacheManagerTests.cs + + + SharedTestCode\LdClientEndToEndTests.cs + + + SharedTestCode\LdClientEvaluationTests.cs + + + SharedTestCode\LdClientEventTests.cs + + + SharedTestCode\LdClientTests.cs + + + SharedTestCode\LogSink.cs + + + SharedTestCode\MobilePollingProcessorTests.cs + + + SharedTestCode\MockComponents.cs + + + SharedTestCode\TestUtil.cs + + + SharedTestCode\UserFlagCacheTests.cs + + + SharedTestCode\WireMockExtensions.cs + + + + + {7717A2B2-9905-40A7-989F-790139D69543} + LaunchDarkly.XamarinSdk + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard new file mode 100644 index 00000000..f18534b4 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs new file mode 100644 index 00000000..fa24461b --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs @@ -0,0 +1,12 @@ +using UIKit; + +namespace LaunchDarkly.Xamarin.iOS.Tests +{ + public class Application + { + static void Main(string[] args) + { + UIApplication.Main(args, null, "AppDelegate"); + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.storyboard b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.storyboard new file mode 100644 index 00000000..a3f40d68 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b2eb5309 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LaunchDarkly.XamarinSdk.iOS.Toasts")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LaunchDarkly.XamarinSdk.iOS.Toasts")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("50c7b8c9-e664-45af-b88e-0c9b8b9c1be1")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Resources/LaunchScreen.xib b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Resources/LaunchScreen.xib new file mode 100644 index 00000000..1f1c1cb7 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2022ad94ac745342dc0e9e1135d3460922be8eeb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 25 Jun 2019 13:52:02 -0700 Subject: [PATCH 120/254] revert accidental rename --- ...Toasts.csproj => LaunchDarkly.XamarinSdk.iOS.Tests.csproj} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/LaunchDarkly.XamarinSdk.iOS.Tests/{LaunchDarkly.XamarinSdk.iOS.Toasts.csproj => LaunchDarkly.XamarinSdk.iOS.Tests.csproj} (98%) diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj similarity index 98% rename from tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj rename to tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index 627ffabe..a2988ae1 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Toasts.csproj +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -7,8 +7,8 @@ {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} {edc1b0fa-90cd-4038-8fad-98fe74adb368} Exe - LaunchDarkly.XamarinSdk.iOS.Toasts - LaunchDarkly.XamarinSdk.iOS.Toasts + LaunchDarkly.XamarinSdk.iOS.Tests + LaunchDarkly.XamarinSdk.iOS.Tests Resources true NSUrlSessionHandler From e223aa3905c6308fe89575b2af4acef684dc6ea3 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 25 Jun 2019 15:24:09 -0700 Subject: [PATCH 121/254] use mono-mdk --- .circleci/config.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d47f923..504e452b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,6 +56,9 @@ jobs: macos: xcode: "10.2.1" + environment: + PATH: /usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Frameworks/Mono.framework/Commands + steps: - restore_cache: key: homebrew @@ -64,8 +67,8 @@ jobs: name: Install Xamarin tools command: | brew install caskroom/cask/brew-cask - brew cask install xamarin xamarin-ios && brew install mono - # Note, "mono" provides the msbuild CLI tool + brew cask install xamarin xamarin-ios mono-mdk + # Note, "mono-mdk" provides the msbuild CLI tool - save_cache: key: homebrew From 486ab22d8359a6f9296897a072bd27407b72762b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 11:04:39 -0700 Subject: [PATCH 122/254] rm obsolete project --- .../AppDelegate.cs | 29 -- .../Entitlements.plist | 6 - LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist | 36 -- .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 431 ------------------ .../LaunchScreen.storyboard | 27 -- LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs | 12 - .../packages.config | 138 ------ 7 files changed, 679 deletions(-) delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs delete mode 100644 LaunchDarkly.XamarinSdk.iOS.Tests/packages.config diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs deleted file mode 100644 index 4beefc8f..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using Foundation; -using UIKit; - -using Xunit.Runner; -using Xunit.Sdk; - -namespace LaunchDarkly.Xamarin.iOS.Tests -{ - // This is based on code that was generated automatically by the xunit.runner.devices package. - // It configures the test-runner app that is implemented by that package, telling it where to - // find the tests, and also configuring it to run them immediately and then quit rather than - // waiting for user input. Output from the test run goes to the system log. - - [Register("AppDelegate")] - public partial class AppDelegate : RunnerAppDelegate - { - public override bool FinishedLaunching(UIApplication app, NSDictionary options) - { - AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); - AddTestAssembly(Assembly.GetExecutingAssembly()); - - AutoStart = true; - TerminateAfterExecution = true; - - return base.FinishedLaunching(app, options); - } - } -} \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist b/LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist deleted file mode 100644 index 9ae59937..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/Entitlements.plist +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist b/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist deleted file mode 100644 index 1cbfd464..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - CFBundleName - LaunchDarkly.Xamarin.iOS.Tests - CFBundleIdentifier - com.launchdarkly.LaunchDarkly-Xamarin-iOS-Tests - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 12.1 - UIDeviceFamily - - 1 - 2 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UILaunchStoryboardName - LaunchScreen - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj deleted file mode 100644 index ec344391..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ /dev/null @@ -1,431 +0,0 @@ - - - - - - - Debug - iPhoneSimulator - {066AA0F9-449A-48F5-9492-D698F0EFD923} - {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Exe - LaunchDarkly.Xamarin.iOS.Tests - LaunchDarkly.XamarinSdk.iOS.Tests - Resources - - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG; - prompt - 4 - iPhone Developer - true - true - true - 46596 - None - x86_64 - NSUrlSessionHandler - false - - - - pdbonly - true - bin\iPhone\Release - - prompt - 4 - iPhone Developer - true - true - Entitlements.plist - SdkOnly - ARM64 - NSUrlSessionHandler - - - - pdbonly - true - bin\iPhoneSimulator\Release - - prompt - 4 - iPhone Developer - true - None - x86_64 - NSUrlSessionHandler - - - - true - full - false - bin\iPhone\Debug - DEBUG; - prompt - 4 - iPhone Developer - true - true - true - true - true - Entitlements.plist - 42164 - SdkOnly - ARM64 - NSUrlSessionHandler - - - - - - - - - - - - - - - - ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll - - - ..\packages\Newtonsoft.Json.11.0.2\lib\netstandard2.0\Newtonsoft.Json.dll - - - - - ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll - - - ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - - - ..\packages\LaunchDarkly.CommonSdk.2.1.2\lib\netstandard2.0\LaunchDarkly.CommonSdk.dll - - - ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\xamarinios10\Plugin.DeviceInfo.dll - - - ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Core.dll - - - ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Platform.dll - - - ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Platform.iOS.dll - - - ..\packages\Xamarin.Forms.3.3.0.912540\lib\Xamarin.iOS10\Xamarin.Forms.Xaml.dll - - - ..\packages\xunit.runner.devices.2.5.25\lib\xamarinios10\xunit.runner.devices.dll - - - ..\packages\xunit.runner.devices.2.5.25\lib\xamarinios10\xunit.runner.utility.netstandard15.dll - - - ..\packages\xunit.abstractions.2.0.3\lib\netstandard2.0\xunit.abstractions.dll - - - ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - ..\packages\xunit.extensibility.core.2.4.1\lib\netstandard1.1\xunit.core.dll - - - ..\packages\xunit.extensibility.execution.2.4.1\lib\netstandard1.1\xunit.execution.dotnet.dll - - - ..\packages\Microsoft.AspNetCore.Diagnostics.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Diagnostics.Abstractions.dll - - - ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - - ..\packages\Microsoft.Extensions.DependencyInjection.2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll - - - ..\packages\Microsoft.Extensions.FileSystemGlobbing.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileSystemGlobbing.dll - - - ..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll - - - ..\packages\Microsoft.Extensions.ObjectPool.2.1.1\lib\netstandard2.0\Microsoft.Extensions.ObjectPool.dll - - - ..\packages\MimeKitLite.2.0.7\lib\xamarinios\MimeKitLite.dll - - - ..\packages\Fare.2.1.1\lib\netstandard1.1\Fare.dll - - - ..\packages\JmesPath.Net.1.0.125\lib\netstandard2.0\JmesPath.Net.dll - - - ..\packages\NLipsum.1.1.0\lib\netstandard1.0\NLipsum.Core.dll - - - ..\packages\RandomDataGenerator.Net.1.0.8\lib\netstandard2.0\RandomDataGenerator.dll - - - ..\packages\SimMetrics.Net.1.0.5\lib\netstandard2.0\SimMetrics.Net.dll - - - ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll - - - ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - - ..\packages\System.Diagnostics.DiagnosticSource.4.5.0\lib\netstandard1.3\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\RestEase.1.4.7\lib\netstandard2.0\RestEase.dll - - - ..\packages\System.Linq.Dynamic.Core.1.0.12\lib\netstandard2.0\System.Linq.Dynamic.Core.dll - - - ..\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll - - - ..\packages\Handlebars.Net.1.9.5\lib\netstandard2.0\Handlebars.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll - - - ..\packages\Microsoft.Extensions.Primitives.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll - - - ..\packages\Microsoft.AspNetCore.Http.Features.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Features.dll - - - ..\packages\Microsoft.Extensions.Configuration.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Hosting.Server.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Server.Abstractions.dll - - - ..\packages\Microsoft.Extensions.Configuration.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll - - - ..\packages\Microsoft.Extensions.Configuration.Binder.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll - - - ..\packages\Microsoft.Extensions.Configuration.CommandLine.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.CommandLine.dll - - - ..\packages\Microsoft.Extensions.Configuration.EnvironmentVariables.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.EnvironmentVariables.dll - - - ..\packages\Microsoft.Extensions.FileProviders.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Abstractions.dll - - - ..\packages\Microsoft.Extensions.FileProviders.Physical.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Physical.dll - - - ..\packages\Microsoft.Extensions.Configuration.FileExtensions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.FileExtensions.dll - - - ..\packages\Microsoft.Extensions.Configuration.Json.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Json.dll - - - ..\packages\Microsoft.Extensions.Configuration.UserSecrets.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.UserSecrets.dll - - - ..\packages\Microsoft.Extensions.Hosting.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Hosting.Abstractions.dll - - - ..\packages\Microsoft.Extensions.Options.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll - - - ..\packages\Microsoft.Extensions.Logging.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.dll - - - ..\packages\Microsoft.Extensions.Logging.Debug.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Debug.dll - - - ..\packages\Microsoft.Extensions.Options.ConfigurationExtensions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.ConfigurationExtensions.dll - - - ..\packages\Microsoft.Extensions.Logging.Configuration.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Configuration.dll - - - ..\packages\Microsoft.Extensions.Logging.Console.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Console.dll - - - ..\packages\Microsoft.Net.Http.Headers.2.1.1\lib\netstandard2.0\Microsoft.Net.Http.Headers.dll - - - ..\packages\System.IO.Pipelines.4.5.2\lib\netstandard2.0\System.IO.Pipelines.dll - - - ..\packages\Microsoft.AspNetCore.Connections.Abstractions.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Connections.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.dll - - - ..\packages\System.Security.Principal.Windows.4.5.1\lib\netstandard2.0\System.Security.Principal.Windows.dll - - - ..\packages\System.Text.Encodings.Web.4.5.0\lib\netstandard2.0\System.Text.Encodings.Web.dll - - - ..\packages\Microsoft.AspNetCore.Http.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Authentication.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Hosting.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Http.Extensions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.Extensions.dll - - - ..\packages\Microsoft.AspNetCore.HttpOverrides.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.HttpOverrides.dll - - - ..\packages\Microsoft.AspNetCore.Routing.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Routing.Abstractions.dll - - - ..\packages\Microsoft.AspNetCore.Routing.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Routing.dll - - - ..\packages\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll - - - ..\packages\Microsoft.AspNetCore.WebUtilities.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.WebUtilities.dll - - - ..\packages\Microsoft.AspNetCore.Diagnostics.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Diagnostics.dll - - - ..\packages\Microsoft.AspNetCore.Http.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Http.dll - - - ..\packages\Microsoft.AspNetCore.Authentication.Core.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Core.dll - - - ..\packages\Microsoft.AspNetCore.HostFiltering.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.HostFiltering.dll - - - ..\packages\Microsoft.AspNetCore.Hosting.2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.dll - - - ..\packages\Microsoft.AspNetCore.Server.IISIntegration.2.1.2\lib\netstandard2.0\Microsoft.AspNetCore.Server.IISIntegration.dll - - - ..\packages\Microsoft.AspNetCore.Server.Kestrel.Core.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Core.dll - - - ..\packages\Microsoft.AspNetCore.Server.Kestrel.Https.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.Https.dll - - - ..\packages\Microsoft.AspNetCore.Server.Kestrel.2.1.3\lib\netstandard2.0\Microsoft.AspNetCore.Server.Kestrel.dll - - - ..\packages\Microsoft.AspNetCore.2.1.4\lib\netstandard2.0\Microsoft.AspNetCore.dll - - - ..\packages\XPath2.1.0.6.1\lib\netstandard2.0\XPath2.dll - - - ..\packages\XPath2.Extensions.1.0.6.1\lib\netstandard2.0\XPath2.Extensions.dll - - - ..\packages\WireMock.Net.1.0.20\lib\netstandard2.0\WireMock.Net.dll - - - - - - - - - - - - - - - SharedTestCode\BaseTest.cs - - - SharedTestCode\ConfigurationTest.cs - - - SharedTestCode\FeatureFlagListenerTests.cs - - - SharedTestCode\FeatureFlagRequestorTests.cs - - - SharedTestCode\FeatureFlagTests.cs - - - SharedTestCode\FlagCacheManagerTests.cs - - - SharedTestCode\LdClientEndToEndTests.cs - - - SharedTestCode\LdClientEvaluationTests.cs - - - SharedTestCode\LdClientEventTests.cs - - - SharedTestCode\LdClientTests.cs - - - SharedTestCode\LogSink.cs - - - SharedTestCode\MobilePollingProcessorTests.cs - - - SharedTestCode\MockComponents.cs - - - SharedTestCode\TestUtil.cs - - - SharedTestCode\UserFlagCacheTests.cs - - - SharedTestCode\WireMockExtensions.cs - - - - - - - - {7717A2B2-9905-40A7-989F-790139D69543} - LaunchDarkly.XamarinSdk - - - - - - - - - \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard b/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard deleted file mode 100644 index 5d2e905a..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs b/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs deleted file mode 100644 index fa24461b..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/Main.cs +++ /dev/null @@ -1,12 +0,0 @@ -using UIKit; - -namespace LaunchDarkly.Xamarin.iOS.Tests -{ - public class Application - { - static void Main(string[] args) - { - UIApplication.Main(args, null, "AppDelegate"); - } - } -} diff --git a/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config b/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config deleted file mode 100644 index 8a78858e..00000000 --- a/LaunchDarkly.XamarinSdk.iOS.Tests/packages.config +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 291d9d33283ffce6f50ef4e7724015e58ec7f232 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 11:31:02 -0700 Subject: [PATCH 123/254] better test output processing --- .circleci/config.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 504e452b..af05f0e8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -110,5 +110,9 @@ jobs: command: "( tail -f -c+0 test-run.log & ) | grep -q 'Tests run:'" - run: - name: Show test output - command: cat test-run.log + name: Show all test output + command: | + cat test-run.log | tr -s ' ' | cut -d ' ' -f 1,2,9- + grep '\[FAIL\]' test-run.log >/dev/null && exit 1 + # "exit 1" causes the CI job to fail if there were any test failures. Note that we still won't have a + # JUnit-compatible test results file; you'll just have to look at the output. From 0a387d82fdd26b682b5b024bfc722be11bbdbe64 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 11:54:26 -0700 Subject: [PATCH 124/254] fix script exit logic --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af05f0e8..7f2235d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -113,6 +113,6 @@ jobs: name: Show all test output command: | cat test-run.log | tr -s ' ' | cut -d ' ' -f 1,2,9- - grep '\[FAIL\]' test-run.log >/dev/null && exit 1 + if grep '\[FAIL\]' test-run.log >/dev/null; then exit 1; fi # "exit 1" causes the CI job to fail if there were any test failures. Note that we still won't have a # JUnit-compatible test results file; you'll just have to look at the output. From 0020505dacfb48199f8f42973e517551a8d9a35c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 18:02:57 -0700 Subject: [PATCH 125/254] add a new Android test project made from scratch --- .../LDAndroidTests.cs | 62 - ...unchDarkly.XamarinSdk.Android.Tests.csproj | 214 - .../MainActivity.cs | 23 - .../Properties/AndroidManifest.xml | 6 - .../Properties/AssemblyInfo.cs | 27 - .../Resources/Resource.designer.cs | 886 -- .../Resources/mipmap-hdpi/Icon.png | Bin 2201 -> 0 bytes .../Resources/mipmap-mdpi/Icon.png | Bin 1410 -> 0 bytes .../Resources/mipmap-xhdpi/Icon.png | Bin 3237 -> 0 bytes .../Resources/mipmap-xxhdpi/Icon.png | Bin 5414 -> 0 bytes .../Resources/mipmap-xxxhdpi/Icon.png | Bin 7825 -> 0 bytes .../designtime/build.props | 24 - .../packages.config | 86 - LaunchDarkly.XamarinSdk.sln | 28 +- .../Assets/AboutAssets.txt | 19 + ...unchDarkly.XamarinSdk.Android.Tests.csproj | 180 + .../MainActivity.cs | 24 + .../Properties/AndroidManifest.xml | 5 + .../Properties/AssemblyInfo.cs | 30 + .../Resources/AboutResources.txt | 2 +- .../Resources/Resource.designer.cs | 9543 +++++++++++++++++ .../Resources/layout/activity_main.axml | 3 + .../mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../Resources/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1634 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1441 bytes .../mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3552 bytes .../Resources/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1362 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 958 bytes .../mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2413 bytes .../Resources/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2307 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2056 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4858 bytes .../Resources/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3871 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3403 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8001 bytes .../Resources/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5016 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 4889 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 10893 bytes .../Resources/values/colors.xml | 6 + .../values/ic_launcher_background.xml | 4 + .../Resources/values/strings.xml | 5 + .../Resources/values/styles.xml | 10 + 43 files changed, 9854 insertions(+), 1343 deletions(-) delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/Icon.png delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/Icon.png delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/Icon.png delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/Icon.png delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/Icon.png delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/designtime/build.props delete mode 100644 LaunchDarkly.XamarinSdk.Android.Tests/packages.config create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs rename {LaunchDarkly.XamarinSdk.Android.Tests => tests/LaunchDarkly.XamarinSdk.Android.Tests}/Resources/AboutResources.txt (97%) create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/layout/activity_main.axml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_round.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/ic_launcher.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/ic_launcher_round.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/ic_launcher.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/ic_launcher_round.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/ic_launcher.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/colors.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/ic_launcher_background.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/strings.xml create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs b/LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs deleted file mode 100644 index 931b7c19..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/LDAndroidTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using NUnit.Framework; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin.Android.Tests -{ - [TestFixture] - public class LDAndroidTests - { - private ILdMobileClient client; - - [SetUp] - public void Setup() - { - var user = LaunchDarkly.Client.User.WithKey("test-user"); - var timeSpan = TimeSpan.FromSeconds(10); - client = LdClient.Init("MOBILE_KEY", user, timeSpan); - } - - [TearDown] - public void Tear() { LdClient.Instance = null; } - - [Test] - public void BooleanFeatureFlag() - { - Console.WriteLine("Test Boolean Variation"); - Assert.True(client.BoolVariation("boolean-feature-flag")); - } - - [Test] - public void IntFeatureFlag() - { - Console.WriteLine("Test Integer Variation"); - Assert.True(client.IntVariation("int-feature-flag") == 2); - } - - [Test] - public void StringFeatureFlag() - { - Console.WriteLine("Test String Variation"); - Assert.True(client.StringVariation("string-feature-flag", "false").Equals("bravo")); - } - - [Test] - public void JsonFeatureFlag() - { - string json = @"{ - ""test2"": ""testing2"" - }"; - Console.WriteLine("Test JSON Variation"); - JToken jsonToken = JToken.FromObject(JObject.Parse(json)); - Assert.True(JToken.DeepEquals(jsonToken, client.JsonVariation("json-feature-flag", "false"))); - } - - [Test] - public void FloatFeatureFlag() - { - Console.WriteLine("Test Float Variation"); - Assert.True(client.FloatVariation("float-feature-flag") == 1.5); - } - } -} diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj deleted file mode 100644 index fc46d530..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ /dev/null @@ -1,214 +0,0 @@ - - - - Debug - AnyCPU - {0B18C336-C770-42C1-B77A-E4A49F789677} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - LaunchDarkly.Xamarin.Android.Tests - LaunchDarkly.XamarinSdk.Android.Tests - v9.0 - MonoAndroid90 - True - Resources\Resource.designer.cs - Resource - Resources - Assets - Properties\AndroidManifest.xml - - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - None - - arm64-v8a;armeabi-v7a;x86 - - - true - pdbonly - true - bin\Release - prompt - 4 - true - false - - - - - ..\src\LaunchDarkly.XamarinSdk\bin\Debug\monoandroid81\LaunchDarkly.XamarinSdk.dll - - - - - - - - - ..\packages\Common.Logging.Core.3.4.1\lib\netstandard1.0\Common.Logging.Core.dll - - - ..\packages\Newtonsoft.Json.9.0.1\lib\netstandard1.0\Newtonsoft.Json.dll - - - - - ..\packages\Common.Logging.3.4.1\lib\netstandard1.3\Common.Logging.dll - - - ..\packages\LaunchDarkly.EventSource.3.3.0\lib\netstandard1.4\LaunchDarkly.EventSource.dll - - - ..\packages\LaunchDarkly.CommonSdk.2.1.2\lib\netstandard2.0\LaunchDarkly.CommonSdk.dll - - - ..\packages\Plugin.CurrentActivity.2.1.0.4\lib\monoandroid44\Plugin.CurrentActivity.dll - - - - ..\packages\Xam.Plugin.DeviceInfo.4.1.1\lib\monoandroid71\Plugin.DeviceInfo.dll - - - ..\packages\Xamarin.Android.Support.Annotations.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Annotations.dll - - - ..\packages\Xamarin.Android.Arch.Core.Common.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Core.Common.dll - - - ..\packages\Xamarin.Android.Arch.Core.Runtime.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Core.Runtime.dll - - - ..\packages\Xamarin.Android.Arch.Lifecycle.Common.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.Common.dll - - - ..\packages\Xamarin.Android.Arch.Lifecycle.LiveData.Core.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.LiveData.Core.dll - - - ..\packages\Xamarin.Android.Arch.Lifecycle.LiveData.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.LiveData.dll - - - ..\packages\Xamarin.Android.Arch.Lifecycle.Runtime.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.Runtime.dll - - - ..\packages\Xamarin.Android.Arch.Lifecycle.ViewModel.1.1.1.1\lib\monoandroid90\Xamarin.Android.Arch.Lifecycle.ViewModel.dll - - - ..\packages\Xamarin.Android.Support.Collections.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Collections.dll - - - ..\packages\Xamarin.Android.Support.CursorAdapter.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CursorAdapter.dll - - - ..\packages\Xamarin.Android.Support.DocumentFile.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.DocumentFile.dll - - - ..\packages\Xamarin.Android.Support.Interpolator.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Interpolator.dll - - - ..\packages\Xamarin.Android.Support.LocalBroadcastManager.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.LocalBroadcastManager.dll - - - ..\packages\Xamarin.Android.Support.Print.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Print.dll - - - ..\packages\Xamarin.Android.Support.VersionedParcelable.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.VersionedParcelable.dll - - - ..\packages\Xamarin.Android.Support.Compat.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Compat.dll - - - ..\packages\Xamarin.Android.Support.AsyncLayoutInflater.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.AsyncLayoutInflater.dll - - - ..\packages\Xamarin.Android.Support.CustomView.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CustomView.dll - - - ..\packages\Xamarin.Android.Support.CoordinaterLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.CoordinaterLayout.dll - - - ..\packages\Xamarin.Android.Support.DrawerLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.DrawerLayout.dll - - - ..\packages\Xamarin.Android.Support.Loader.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Loader.dll - - - ..\packages\Xamarin.Android.Support.Core.Utils.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Core.Utils.dll - - - ..\packages\Xamarin.Android.Support.Media.Compat.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Media.Compat.dll - - - ..\packages\Xamarin.Android.Support.SlidingPaneLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.SlidingPaneLayout.dll - - - ..\packages\Xamarin.Android.Support.SwipeRefreshLayout.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.SwipeRefreshLayout.dll - - - ..\packages\Xamarin.Android.Support.ViewPager.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.ViewPager.dll - - - ..\packages\Xamarin.Android.Support.Core.UI.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Core.UI.dll - - - ..\packages\Xamarin.Android.Support.Fragment.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.Fragment.dll - - - ..\packages\Xamarin.Android.Support.v4.28.0.0.1\lib\monoandroid90\Xamarin.Android.Support.v4.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs b/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs deleted file mode 100644 index 22d1b416..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; - -using Android.App; -using Android.OS; -using Xamarin.Android.NUnitLite; - -namespace LaunchDarkly.Xamarin.Android.Tests -{ - [Activity(Label = "LaunchDarkly.Xamarin.Android.Tests", MainLauncher = true)] - public class MainActivity : TestSuiteActivity - { - protected override void OnCreate(Bundle bundle) - { - // tests can be inside the main assembly - AddTest(Assembly.GetExecutingAssembly()); - // or in any reference assemblies - // AddTest (typeof (Your.Library.TestClass).Assembly); - - // Once you called base.OnCreate(), you cannot add more assemblies. - base.OnCreate(bundle); - } - } -} diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml b/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml deleted file mode 100644 index 458b1f79..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs b/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 80ab1048..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using Android.App; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("LaunchDarkly.Xamarin.Android.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("${AuthorCopyright}")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.0")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs deleted file mode 100644 index 8144be89..00000000 --- a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs +++ /dev/null @@ -1,886 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("LaunchDarkly.Xamarin.Android.Tests.Resource", IsApplication=true)] - -namespace LaunchDarkly.Xamarin.Android.Tests -{ - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public static void UpdateIdValues() - { - global::Xamarin.Android.NUnitLite.Resource.Id.OptionHostName = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionHostName; - global::Xamarin.Android.NUnitLite.Resource.Id.OptionPort = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionPort; - global::Xamarin.Android.NUnitLite.Resource.Id.OptionRemoteServer = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionRemoteServer; - global::Xamarin.Android.NUnitLite.Resource.Id.OptionsButton = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.OptionsButton; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultFullName = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultFullName; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultMessage = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultMessage; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultResultState = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultResultState; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultRunSingleMethodTest = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultRunSingleMethodTest; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultStackTrace = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultStackTrace; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsFailed = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsFailed; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsId = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsId; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsIgnored = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsIgnored; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsInconclusive = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsInconclusive; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsMessage = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsMessage; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsPassed = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsPassed; - global::Xamarin.Android.NUnitLite.Resource.Id.ResultsResult = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.ResultsResult; - global::Xamarin.Android.NUnitLite.Resource.Id.RunTestsButton = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.RunTestsButton; - global::Xamarin.Android.NUnitLite.Resource.Id.TestSuiteListView = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Id.TestSuiteListView; - global::Xamarin.Android.NUnitLite.Resource.Layout.options = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.options; - global::Xamarin.Android.NUnitLite.Resource.Layout.results = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.results; - global::Xamarin.Android.NUnitLite.Resource.Layout.test_result = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.test_result; - global::Xamarin.Android.NUnitLite.Resource.Layout.test_suite = global::LaunchDarkly.Xamarin.Android.Tests.Resource.Layout.test_suite; - } - - public partial class Attribute - { - - // aapt resource value: 0x7f010009 - public const int alpha = 2130771977; - - // aapt resource value: 0x7f010000 - public const int coordinatorLayoutStyle = 2130771968; - - // aapt resource value: 0x7f010011 - public const int font = 2130771985; - - // aapt resource value: 0x7f01000a - public const int fontProviderAuthority = 2130771978; - - // aapt resource value: 0x7f01000d - public const int fontProviderCerts = 2130771981; - - // aapt resource value: 0x7f01000e - public const int fontProviderFetchStrategy = 2130771982; - - // aapt resource value: 0x7f01000f - public const int fontProviderFetchTimeout = 2130771983; - - // aapt resource value: 0x7f01000b - public const int fontProviderPackage = 2130771979; - - // aapt resource value: 0x7f01000c - public const int fontProviderQuery = 2130771980; - - // aapt resource value: 0x7f010010 - public const int fontStyle = 2130771984; - - // aapt resource value: 0x7f010013 - public const int fontVariationSettings = 2130771987; - - // aapt resource value: 0x7f010012 - public const int fontWeight = 2130771986; - - // aapt resource value: 0x7f010001 - public const int keylines = 2130771969; - - // aapt resource value: 0x7f010004 - public const int layout_anchor = 2130771972; - - // aapt resource value: 0x7f010006 - public const int layout_anchorGravity = 2130771974; - - // aapt resource value: 0x7f010003 - public const int layout_behavior = 2130771971; - - // aapt resource value: 0x7f010008 - public const int layout_dodgeInsetEdges = 2130771976; - - // aapt resource value: 0x7f010007 - public const int layout_insetEdge = 2130771975; - - // aapt resource value: 0x7f010005 - public const int layout_keyline = 2130771973; - - // aapt resource value: 0x7f010002 - public const int statusBarBackground = 2130771970; - - // aapt resource value: 0x7f010014 - public const int ttcIndex = 2130771988; - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class Color - { - - // aapt resource value: 0x7f060003 - public const int notification_action_color_filter = 2131099651; - - // aapt resource value: 0x7f060004 - public const int notification_icon_bg_color = 2131099652; - - // aapt resource value: 0x7f060000 - public const int notification_material_background_media_default_color = 2131099648; - - // aapt resource value: 0x7f060001 - public const int primary_text_default_material_dark = 2131099649; - - // aapt resource value: 0x7f060005 - public const int ripple_material_light = 2131099653; - - // aapt resource value: 0x7f060002 - public const int secondary_text_default_material_dark = 2131099650; - - // aapt resource value: 0x7f060006 - public const int secondary_text_default_material_light = 2131099654; - - static Color() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Color() - { - } - } - - public partial class Dimension - { - - // aapt resource value: 0x7f070008 - public const int compat_button_inset_horizontal_material = 2131165192; - - // aapt resource value: 0x7f070009 - public const int compat_button_inset_vertical_material = 2131165193; - - // aapt resource value: 0x7f07000a - public const int compat_button_padding_horizontal_material = 2131165194; - - // aapt resource value: 0x7f07000b - public const int compat_button_padding_vertical_material = 2131165195; - - // aapt resource value: 0x7f07000c - public const int compat_control_corner_material = 2131165196; - - // aapt resource value: 0x7f07000d - public const int compat_notification_large_icon_max_height = 2131165197; - - // aapt resource value: 0x7f07000e - public const int compat_notification_large_icon_max_width = 2131165198; - - // aapt resource value: 0x7f07000f - public const int notification_action_icon_size = 2131165199; - - // aapt resource value: 0x7f070010 - public const int notification_action_text_size = 2131165200; - - // aapt resource value: 0x7f070011 - public const int notification_big_circle_margin = 2131165201; - - // aapt resource value: 0x7f070005 - public const int notification_content_margin_start = 2131165189; - - // aapt resource value: 0x7f070012 - public const int notification_large_icon_height = 2131165202; - - // aapt resource value: 0x7f070013 - public const int notification_large_icon_width = 2131165203; - - // aapt resource value: 0x7f070006 - public const int notification_main_column_padding_top = 2131165190; - - // aapt resource value: 0x7f070007 - public const int notification_media_narrow_margin = 2131165191; - - // aapt resource value: 0x7f070014 - public const int notification_right_icon_size = 2131165204; - - // aapt resource value: 0x7f070004 - public const int notification_right_side_padding_top = 2131165188; - - // aapt resource value: 0x7f070015 - public const int notification_small_icon_background_padding = 2131165205; - - // aapt resource value: 0x7f070016 - public const int notification_small_icon_size_as_large = 2131165206; - - // aapt resource value: 0x7f070017 - public const int notification_subtext_size = 2131165207; - - // aapt resource value: 0x7f070018 - public const int notification_top_pad = 2131165208; - - // aapt resource value: 0x7f070019 - public const int notification_top_pad_large_text = 2131165209; - - // aapt resource value: 0x7f070000 - public const int subtitle_corner_radius = 2131165184; - - // aapt resource value: 0x7f070001 - public const int subtitle_outline_width = 2131165185; - - // aapt resource value: 0x7f070002 - public const int subtitle_shadow_offset = 2131165186; - - // aapt resource value: 0x7f070003 - public const int subtitle_shadow_radius = 2131165187; - - static Dimension() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Dimension() - { - } - } - - public partial class Drawable - { - - // aapt resource value: 0x7f020000 - public const int notification_action_background = 2130837504; - - // aapt resource value: 0x7f020001 - public const int notification_bg = 2130837505; - - // aapt resource value: 0x7f020002 - public const int notification_bg_low = 2130837506; - - // aapt resource value: 0x7f020003 - public const int notification_bg_low_normal = 2130837507; - - // aapt resource value: 0x7f020004 - public const int notification_bg_low_pressed = 2130837508; - - // aapt resource value: 0x7f020005 - public const int notification_bg_normal = 2130837509; - - // aapt resource value: 0x7f020006 - public const int notification_bg_normal_pressed = 2130837510; - - // aapt resource value: 0x7f020007 - public const int notification_icon_background = 2130837511; - - // aapt resource value: 0x7f02000a - public const int notification_template_icon_bg = 2130837514; - - // aapt resource value: 0x7f02000b - public const int notification_template_icon_low_bg = 2130837515; - - // aapt resource value: 0x7f020008 - public const int notification_tile_bg = 2130837512; - - // aapt resource value: 0x7f020009 - public const int notify_panel_notification_icon_bg = 2130837513; - - static Drawable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Drawable() - { - } - } - - public partial class Id - { - - // aapt resource value: 0x7f0a0032 - public const int OptionHostName = 2131361842; - - // aapt resource value: 0x7f0a0033 - public const int OptionPort = 2131361843; - - // aapt resource value: 0x7f0a0031 - public const int OptionRemoteServer = 2131361841; - - // aapt resource value: 0x7f0a0041 - public const int OptionsButton = 2131361857; - - // aapt resource value: 0x7f0a003c - public const int ResultFullName = 2131361852; - - // aapt resource value: 0x7f0a003e - public const int ResultMessage = 2131361854; - - // aapt resource value: 0x7f0a003d - public const int ResultResultState = 2131361853; - - // aapt resource value: 0x7f0a003b - public const int ResultRunSingleMethodTest = 2131361851; - - // aapt resource value: 0x7f0a003f - public const int ResultStackTrace = 2131361855; - - // aapt resource value: 0x7f0a0037 - public const int ResultsFailed = 2131361847; - - // aapt resource value: 0x7f0a0034 - public const int ResultsId = 2131361844; - - // aapt resource value: 0x7f0a0038 - public const int ResultsIgnored = 2131361848; - - // aapt resource value: 0x7f0a0039 - public const int ResultsInconclusive = 2131361849; - - // aapt resource value: 0x7f0a003a - public const int ResultsMessage = 2131361850; - - // aapt resource value: 0x7f0a0036 - public const int ResultsPassed = 2131361846; - - // aapt resource value: 0x7f0a0035 - public const int ResultsResult = 2131361845; - - // aapt resource value: 0x7f0a0040 - public const int RunTestsButton = 2131361856; - - // aapt resource value: 0x7f0a0042 - public const int TestSuiteListView = 2131361858; - - // aapt resource value: 0x7f0a0020 - public const int action0 = 2131361824; - - // aapt resource value: 0x7f0a001d - public const int action_container = 2131361821; - - // aapt resource value: 0x7f0a0024 - public const int action_divider = 2131361828; - - // aapt resource value: 0x7f0a001e - public const int action_image = 2131361822; - - // aapt resource value: 0x7f0a001f - public const int action_text = 2131361823; - - // aapt resource value: 0x7f0a002e - public const int actions = 2131361838; - - // aapt resource value: 0x7f0a0017 - public const int all = 2131361815; - - // aapt resource value: 0x7f0a0018 - public const int async = 2131361816; - - // aapt resource value: 0x7f0a0019 - public const int blocking = 2131361817; - - // aapt resource value: 0x7f0a0008 - public const int bottom = 2131361800; - - // aapt resource value: 0x7f0a0021 - public const int cancel_action = 2131361825; - - // aapt resource value: 0x7f0a0009 - public const int center = 2131361801; - - // aapt resource value: 0x7f0a000a - public const int center_horizontal = 2131361802; - - // aapt resource value: 0x7f0a000b - public const int center_vertical = 2131361803; - - // aapt resource value: 0x7f0a0029 - public const int chronometer = 2131361833; - - // aapt resource value: 0x7f0a000c - public const int clip_horizontal = 2131361804; - - // aapt resource value: 0x7f0a000d - public const int clip_vertical = 2131361805; - - // aapt resource value: 0x7f0a000e - public const int end = 2131361806; - - // aapt resource value: 0x7f0a0030 - public const int end_padder = 2131361840; - - // aapt resource value: 0x7f0a000f - public const int fill = 2131361807; - - // aapt resource value: 0x7f0a0010 - public const int fill_horizontal = 2131361808; - - // aapt resource value: 0x7f0a0011 - public const int fill_vertical = 2131361809; - - // aapt resource value: 0x7f0a001a - public const int forever = 2131361818; - - // aapt resource value: 0x7f0a002b - public const int icon = 2131361835; - - // aapt resource value: 0x7f0a002f - public const int icon_group = 2131361839; - - // aapt resource value: 0x7f0a002a - public const int info = 2131361834; - - // aapt resource value: 0x7f0a001b - public const int italic = 2131361819; - - // aapt resource value: 0x7f0a0012 - public const int left = 2131361810; - - // aapt resource value: 0x7f0a0000 - public const int line1 = 2131361792; - - // aapt resource value: 0x7f0a0001 - public const int line3 = 2131361793; - - // aapt resource value: 0x7f0a0023 - public const int media_actions = 2131361827; - - // aapt resource value: 0x7f0a0016 - public const int none = 2131361814; - - // aapt resource value: 0x7f0a001c - public const int normal = 2131361820; - - // aapt resource value: 0x7f0a002d - public const int notification_background = 2131361837; - - // aapt resource value: 0x7f0a0026 - public const int notification_main_column = 2131361830; - - // aapt resource value: 0x7f0a0025 - public const int notification_main_column_container = 2131361829; - - // aapt resource value: 0x7f0a0013 - public const int right = 2131361811; - - // aapt resource value: 0x7f0a002c - public const int right_icon = 2131361836; - - // aapt resource value: 0x7f0a0027 - public const int right_side = 2131361831; - - // aapt resource value: 0x7f0a0014 - public const int start = 2131361812; - - // aapt resource value: 0x7f0a0022 - public const int status_bar_latest_event_content = 2131361826; - - // aapt resource value: 0x7f0a0002 - public const int tag_transition_group = 2131361794; - - // aapt resource value: 0x7f0a0003 - public const int tag_unhandled_key_event_manager = 2131361795; - - // aapt resource value: 0x7f0a0004 - public const int tag_unhandled_key_listeners = 2131361796; - - // aapt resource value: 0x7f0a0005 - public const int text = 2131361797; - - // aapt resource value: 0x7f0a0006 - public const int text2 = 2131361798; - - // aapt resource value: 0x7f0a0028 - public const int time = 2131361832; - - // aapt resource value: 0x7f0a0007 - public const int title = 2131361799; - - // aapt resource value: 0x7f0a0015 - public const int top = 2131361813; - - static Id() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Id() - { - } - } - - public partial class Integer - { - - // aapt resource value: 0x7f080000 - public const int cancel_button_image_alpha = 2131230720; - - // aapt resource value: 0x7f080001 - public const int status_bar_notification_info_maxnum = 2131230721; - - static Integer() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Integer() - { - } - } - - public partial class Layout - { - - // aapt resource value: 0x7f040000 - public const int notification_action = 2130968576; - - // aapt resource value: 0x7f040001 - public const int notification_action_tombstone = 2130968577; - - // aapt resource value: 0x7f040002 - public const int notification_media_action = 2130968578; - - // aapt resource value: 0x7f040003 - public const int notification_media_cancel_action = 2130968579; - - // aapt resource value: 0x7f040004 - public const int notification_template_big_media = 2130968580; - - // aapt resource value: 0x7f040005 - public const int notification_template_big_media_custom = 2130968581; - - // aapt resource value: 0x7f040006 - public const int notification_template_big_media_narrow = 2130968582; - - // aapt resource value: 0x7f040007 - public const int notification_template_big_media_narrow_custom = 2130968583; - - // aapt resource value: 0x7f040008 - public const int notification_template_custom_big = 2130968584; - - // aapt resource value: 0x7f040009 - public const int notification_template_icon_group = 2130968585; - - // aapt resource value: 0x7f04000a - public const int notification_template_lines_media = 2130968586; - - // aapt resource value: 0x7f04000b - public const int notification_template_media = 2130968587; - - // aapt resource value: 0x7f04000c - public const int notification_template_media_custom = 2130968588; - - // aapt resource value: 0x7f04000d - public const int notification_template_part_chronometer = 2130968589; - - // aapt resource value: 0x7f04000e - public const int notification_template_part_time = 2130968590; - - // aapt resource value: 0x7f04000f - public const int options = 2130968591; - - // aapt resource value: 0x7f040010 - public const int results = 2130968592; - - // aapt resource value: 0x7f040011 - public const int test_result = 2130968593; - - // aapt resource value: 0x7f040012 - public const int test_suite = 2130968594; - - static Layout() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Layout() - { - } - } - - public partial class Mipmap - { - - // aapt resource value: 0x7f030000 - public const int Icon = 2130903040; - - static Mipmap() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Mipmap() - { - } - } - - public partial class String - { - - // aapt resource value: 0x7f090000 - public const int status_bar_notification_info_overflow = 2131296256; - - static String() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private String() - { - } - } - - public partial class Style - { - - // aapt resource value: 0x7f050006 - public const int TextAppearance_Compat_Notification = 2131034118; - - // aapt resource value: 0x7f050007 - public const int TextAppearance_Compat_Notification_Info = 2131034119; - - // aapt resource value: 0x7f050000 - public const int TextAppearance_Compat_Notification_Info_Media = 2131034112; - - // aapt resource value: 0x7f05000c - public const int TextAppearance_Compat_Notification_Line2 = 2131034124; - - // aapt resource value: 0x7f050004 - public const int TextAppearance_Compat_Notification_Line2_Media = 2131034116; - - // aapt resource value: 0x7f050001 - public const int TextAppearance_Compat_Notification_Media = 2131034113; - - // aapt resource value: 0x7f050008 - public const int TextAppearance_Compat_Notification_Time = 2131034120; - - // aapt resource value: 0x7f050002 - public const int TextAppearance_Compat_Notification_Time_Media = 2131034114; - - // aapt resource value: 0x7f050009 - public const int TextAppearance_Compat_Notification_Title = 2131034121; - - // aapt resource value: 0x7f050003 - public const int TextAppearance_Compat_Notification_Title_Media = 2131034115; - - // aapt resource value: 0x7f05000a - public const int Widget_Compat_NotificationActionContainer = 2131034122; - - // aapt resource value: 0x7f05000b - public const int Widget_Compat_NotificationActionText = 2131034123; - - // aapt resource value: 0x7f050005 - public const int Widget_Support_CoordinatorLayout = 2131034117; - - static Style() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Style() - { - } - } - - public partial class Styleable - { - - public static int[] ColorStateListItem = new int[] { - 16843173, - 16843551, - 2130771977}; - - // aapt resource value: 2 - public const int ColorStateListItem_alpha = 2; - - // aapt resource value: 1 - public const int ColorStateListItem_android_alpha = 1; - - // aapt resource value: 0 - public const int ColorStateListItem_android_color = 0; - - public static int[] CoordinatorLayout = new int[] { - 2130771969, - 2130771970}; - - // aapt resource value: 0 - public const int CoordinatorLayout_keylines = 0; - - // aapt resource value: 1 - public const int CoordinatorLayout_statusBarBackground = 1; - - public static int[] CoordinatorLayout_Layout = new int[] { - 16842931, - 2130771971, - 2130771972, - 2130771973, - 2130771974, - 2130771975, - 2130771976}; - - // aapt resource value: 0 - public const int CoordinatorLayout_Layout_android_layout_gravity = 0; - - // aapt resource value: 2 - public const int CoordinatorLayout_Layout_layout_anchor = 2; - - // aapt resource value: 4 - public const int CoordinatorLayout_Layout_layout_anchorGravity = 4; - - // aapt resource value: 1 - public const int CoordinatorLayout_Layout_layout_behavior = 1; - - // aapt resource value: 6 - public const int CoordinatorLayout_Layout_layout_dodgeInsetEdges = 6; - - // aapt resource value: 5 - public const int CoordinatorLayout_Layout_layout_insetEdge = 5; - - // aapt resource value: 3 - public const int CoordinatorLayout_Layout_layout_keyline = 3; - - public static int[] FontFamily = new int[] { - 2130771978, - 2130771979, - 2130771980, - 2130771981, - 2130771982, - 2130771983}; - - // aapt resource value: 0 - public const int FontFamily_fontProviderAuthority = 0; - - // aapt resource value: 3 - public const int FontFamily_fontProviderCerts = 3; - - // aapt resource value: 4 - public const int FontFamily_fontProviderFetchStrategy = 4; - - // aapt resource value: 5 - public const int FontFamily_fontProviderFetchTimeout = 5; - - // aapt resource value: 1 - public const int FontFamily_fontProviderPackage = 1; - - // aapt resource value: 2 - public const int FontFamily_fontProviderQuery = 2; - - public static int[] FontFamilyFont = new int[] { - 16844082, - 16844083, - 16844095, - 16844143, - 16844144, - 2130771984, - 2130771985, - 2130771986, - 2130771987, - 2130771988}; - - // aapt resource value: 0 - public const int FontFamilyFont_android_font = 0; - - // aapt resource value: 2 - public const int FontFamilyFont_android_fontStyle = 2; - - // aapt resource value: 4 - public const int FontFamilyFont_android_fontVariationSettings = 4; - - // aapt resource value: 1 - public const int FontFamilyFont_android_fontWeight = 1; - - // aapt resource value: 3 - public const int FontFamilyFont_android_ttcIndex = 3; - - // aapt resource value: 6 - public const int FontFamilyFont_font = 6; - - // aapt resource value: 5 - public const int FontFamilyFont_fontStyle = 5; - - // aapt resource value: 8 - public const int FontFamilyFont_fontVariationSettings = 8; - - // aapt resource value: 7 - public const int FontFamilyFont_fontWeight = 7; - - // aapt resource value: 9 - public const int FontFamilyFont_ttcIndex = 9; - - public static int[] GradientColor = new int[] { - 16843165, - 16843166, - 16843169, - 16843170, - 16843171, - 16843172, - 16843265, - 16843275, - 16844048, - 16844049, - 16844050, - 16844051}; - - // aapt resource value: 7 - public const int GradientColor_android_centerColor = 7; - - // aapt resource value: 3 - public const int GradientColor_android_centerX = 3; - - // aapt resource value: 4 - public const int GradientColor_android_centerY = 4; - - // aapt resource value: 1 - public const int GradientColor_android_endColor = 1; - - // aapt resource value: 10 - public const int GradientColor_android_endX = 10; - - // aapt resource value: 11 - public const int GradientColor_android_endY = 11; - - // aapt resource value: 5 - public const int GradientColor_android_gradientRadius = 5; - - // aapt resource value: 0 - public const int GradientColor_android_startColor = 0; - - // aapt resource value: 8 - public const int GradientColor_android_startX = 8; - - // aapt resource value: 9 - public const int GradientColor_android_startY = 9; - - // aapt resource value: 6 - public const int GradientColor_android_tileMode = 6; - - // aapt resource value: 2 - public const int GradientColor_android_type = 2; - - public static int[] GradientColorItem = new int[] { - 16843173, - 16844052}; - - // aapt resource value: 0 - public const int GradientColorItem_android_color = 0; - - // aapt resource value: 1 - public const int GradientColorItem_android_offset = 1; - - static Styleable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Styleable() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/Icon.png deleted file mode 100644 index f4c804644c5e47daffed8e8573b5da99d0939bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2201 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 diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/Icon.png deleted file mode 100644 index b7e2e57aa90f8687bdfa19d82ff6bda71535b8d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3237 zcmV;W3|jMvP)^6vM1H+8{cTIMGCp-rkq|d6Q`X000000KVc^s{*;mmey7*~~A2;^*oqNtVGl>0Q=zrkGU;4Md{JC&%xm)PXcjCRQ zG1b%C69dw%tFEM4R|X_oKRPnoy1X~d%RQ|*ZXb6s_8CdJe$JTe)wa^TaZ*)tQIdg_3 zz6~ut1{;8O021EGgiQNduWn^35-}`>ws3Cs`ghf>)O##E-vD*tolT&t-zGeuXwJNc z-y#viU^t5k@x{+p;Vx7V;3U3_@W=!ZTGPG6^f))RS zglCuluyz490pZYokO?_+@>5|WdQKvOCin$~I8?WCn?Z|Dh)z_$d;3YC9T47=vN^j6 zzwN+R|6((Hsc1&sT8|eKKwOUr2*51}0HNs_fOkN=5$=DVg=dLN5)mYUE*4pd99gvV zm)yZ?i*6tJ1DM(YV+sQp?*uTRWW--aR=z(X5kV2$LNIu{#X`TlI4y zC?3LL09C&YtWjyF3kPx_=bvgPYF6s4pXz6(9)yGNjMjrpfLQuA|1@$!B7z`@MeNvl zJ7Yw6)$cxl+0{?QPdb7E*Z_FFYNl@=s9lhM5gnpYtKJ5Sk4=a*Mtfybdkz{% z1Q+%PVf!t2iqX6|<@aRE>qR#Q6A)zdq^lo5)sKL`TQQQ+|D+YoJ&ql#S^8_P@YsNX z@a!8e4#J~jf;54u{P3Xq;Rwj~`#Sb`?3hGw;C-k+cndcnb_SVv0RG@bFna%(YW0H& zz(3cCQaAT?7nIg#PnSE|2Zr$6)I(Q49X%b}kb3kC1CA!-jj0d0_2_Ad@QU{j^m~8) ze!4Xnw48pA#P?kNO~Bi={{(s2nHs(!G=M)R1XoM%gEsq6{fYDQW{eh|pLlrbhmM_! zmmUCR@ew-QeEKbVULw5U>CL*XAF6jUO6^v<)_<{6&cud{zbvJJueox=+ z3ifs*JodT+2q2ce8kJf}gbGS;g5k1cWJ}q0gi-yBw!=t0aP-!GNb!;ID&o&HM*8d2 z%=BEK13CY@cCu{EzVjk_fLWh;vmK7Xryj}(FaRhR*WYOmCr?U*8}4uDpLFH#=&cDx zpXVnYQhY}H(cD{)+Q}Co{_bPNoV^psE+}rq&z7wCj$5VkBc`u2U1r;%#1FArkA1Ys znvtFC+Vok8prBub-f!hreF)FW`RaL}xZDgdazAMRZ33d)(N=x!f@&jmv0z?DLQ|qU z9GSzG8#htwA*vpZ&XLnY>p`U2S9)`sv5OMngyRdN8>3$>n)(aXY7gSFo>UsDgy8&^L$n<#@woqQH&S@k@TGw7Ry9*` zPpznC>c=Zq@{Nn=0JqPh$F%Tj24LaoypK&lqH_i?qG(3n)Xd0niTF482^hR){uabe zJWzwLctn#IHB~RR(ZYxMl}`X`Ef~pLO1nUR5II#c;~zmJBF%730FhZylN`K^#Dg+F zv-(xZ8IvDl55`YP#BM(O2RKx2q?ah+{Z_q~E1SgaMI=6*`Kjs$=;Xr`A5r`xw&b=8 z4i}p<7A+{h+G*kWt;fCk$;{7)ojeot#)JhLJBbg@e62UHZTl1*L03I3Iwd?m@nG@! zXT9pgpM|$UW6;{kfN!+T%iBS;8LH}umP3e7^?joCpuQjS+j`hU*GINNW6FS|(+@|w z9MP514V`l68|q}grxFhk$fX9~23-C1hc0ML8t`}fRu>c1)kRZEBZLB4DM~Bxi?tdx zh1f`_lu)Y{V(m~Fhfx?9xHE&-L5Fc*Cj8hw^-B{|(mv7pKB44%B+YHc5Z|IL*wPynX`kPmFOkm*OUWbK~|8UBF-gT$@$5*9@k>E+G4 zHtdK`gzjOJ4oM86^#I|3&9sOZmSg&(fWBYFhsnB zbimsKftrMPKz<;|Ad`E@j!O?LoV7UIj z`vh?*(SB*AnaDCxc>K4<8JklMOgx_H$Vf-iO`Lds`O$wpJVP7`E{RjP=u5q1V&$Gl znWrH&@C_V~ zbo$v4z1sAk*tMygxDy;gC4C*6$oHSAIo&;{gYwdf6OK>4_zSxo#)K0`Vm4^_zH^-> z&g8FN`@e$hwcFNijg4sw7}Au-q`@`RXs;_;k>4e_8nnafLm)@On{Lsr8XdO*gIN#cp0NgM@hCfd!M#itwfS zo5Ydeia5gye4MSU>V8k_yw(CtIvyWjQloS0Ju5nrno-qVt`SH0qszW6r7Zh({g=8? zoP0qz?B$_o0hoS9*#QvxxptJe5gZX$N{Sv7xus3TmGPs} z?e>Z0yST37#DiL|o^Swde>UDFE;wx`YD@F#`aU|hg=ONI|TJQZ$Inz_I-E? z&-(F#b_Timxo@xORxs(1F0sek8kHT44$x{h&uh*3Z5(XuasW z!7=UXiN_JXSa(BJ*3Z5(*qbQ#cO(N(7+$aHH6K8GQhP#YQknI0Kh9nY{Zu>Re0|42 zXHlQ^Gw%#ad_{X=liEW-+Z|1QY_jZ#1-X2gj&)#CAIULZ(;aTU9;f(cq7siKD;QEgT&qR@l5)(U3lsODLMQ=satLQn)+0uhQ&@x1Ll_!?h1!Pqn zh62%Bp6E5hQ4cN#IZ78=nkjx2Sq=mBlqq^l7d`*y>V>C}<}f_foBB#ss#2AzRHf=) Xx9ez7Rf!D(00000NkvXXu0mjfmS|-l diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/Icon.png b/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/Icon.png deleted file mode 100644 index 8d20a38d1782921964638e873ffc98d148ebda6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5414 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 c0$sN|hZ~f>0q(p1ua5js-$bwSo@3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/LaunchDarkly.XamarinSdk.sln b/LaunchDarkly.XamarinSdk.sln index 9611317e..98809c03 100644 --- a/LaunchDarkly.XamarinSdk.sln +++ b/LaunchDarkly.XamarinSdk.sln @@ -6,10 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Tests", "tests\LaunchDarkly.XamarinSdk.Tests\LaunchDarkly.XamarinSdk.Tests.csproj", "{F6B71DFE-314C-4F27-A219-A14569C8CF48}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Android.Tests", "LaunchDarkly.XamarinSdk.Android.Tests\LaunchDarkly.XamarinSdk.Android.Tests.csproj", "{0B18C336-C770-42C1-B77A-E4A49F789677}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.iOS.Tests", "tests\LaunchDarkly.XamarinSdk.iOS.Tests\LaunchDarkly.XamarinSdk.iOS.Tests.csproj", "{5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.XamarinSdk.Android.Tests", "tests\LaunchDarkly.XamarinSdk.Android.Tests\LaunchDarkly.XamarinSdk.Android.Tests.csproj", "{2E7720E4-01A0-403B-863C-C6C596DF5926}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,18 +44,6 @@ Global {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.ActiveCfg = Debug|Any CPU {F6B71DFE-314C-4F27-A219-A14569C8CF48}.Debug|iPhone.Build.0 = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|Any CPU.Build.0 = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhone.ActiveCfg = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhone.Build.0 = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {0B18C336-C770-42C1-B77A-E4A49F789677}.Debug|iPhone.Build.0 = Debug|Any CPU {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|Any CPU.ActiveCfg = Release|iPhone @@ -68,6 +56,18 @@ Global {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhone.ActiveCfg = Debug|iPhone {5EFF7561-35C1-4C62-B0BE-A76E37DCEB32}.Debug|iPhone.Build.0 = Debug|iPhone + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|Any CPU.Build.0 = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|iPhone.ActiveCfg = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|iPhone.Build.0 = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {2E7720E4-01A0-403B-863C-C6C596DF5926}.Debug|iPhone.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt new file mode 100644 index 00000000..b0633374 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with you package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj new file mode 100644 index 00000000..0202bfd4 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -0,0 +1,180 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {2E7720E4-01A0-403B-863C-C6C596DF5926} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {122416d6-6b49-4ee2-a1e8-b825f31c79fe} + Library + Properties + LaunchDarkly.XamarinSdk.Android.Tests + LaunchDarkly.XamarinSdk.Android.Tests + 512 + True + Resources\Resource.designer.cs + Resource + Off + v8.1 + Properties\AndroidManifest.xml + Resources + Assets + true + + + True + portable + False + bin\Debug\ + DEBUG;TRACE + prompt + 4 + True + None + False + + + True + pdbonly + True + bin\Release\ + TRACE + prompt + 4 + true + False + SdkOnly + True + + + + + + + + + + + + + + + SharedTestCode\BaseTest.cs + + + SharedTestCode\ConfigurationTest.cs + + + SharedTestCode\FeatureFlagListenerTests.cs + + + SharedTestCode\FeatureFlagRequestorTests.cs + + + SharedTestCode\FeatureFlagTests.cs + + + SharedTestCode\FlagCacheManagerTests.cs + + + SharedTestCode\LDClientEndToEndTests.cs + + + SharedTestCode\LdClientEvaluationTests.cs + + + SharedTestCode\LdClientEventTests.cs + + + SharedTestCode\LdClientTests.cs + + + SharedTestCode\LogSink.cs + + + SharedTestCode\MobilePollingProcessorTests.cs + + + SharedTestCode\MockComponents.cs + + + SharedTestCode\TestUtil.cs + + + SharedTestCode\UserFlagCacheTests.cs + + + SharedTestCode\WireMockExtensions.cs + + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3.4.1 + + + 1.0.20 + + + 2.4.1 + + + 4.0.0.497661 + + + 2.5.25 + + + + + + + + {7717A2B2-9905-40A7-989F-790139D69543} + LaunchDarkly.XamarinSdk + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs new file mode 100644 index 00000000..ea291a39 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +using Android.App; +using Android.OS; +using Xunit.Runners.UI; +using Xunit.Sdk; + +namespace LaunchDarkly.Xamarin.Android.Tests +{ + [Activity(Label = "LaunchDarkly.Xamarin.Android.Tests", MainLauncher = true)] + public class MainActivity : RunnerActivity + { + protected override void OnCreate(Bundle bundle) + { + AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); + AddTestAssembly(Assembly.GetExecutingAssembly()); + + AutoStart = true; + //TerminateAfterExecution = true; + + base.OnCreate(bundle); + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml new file mode 100644 index 00000000..4e00401d --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bdf7f78e --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Android.App; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LaunchDarkly.XamarinSdk.Android.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LaunchDarkly.XamarinSdk.Android.Tests")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt similarity index 97% rename from LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt rename to tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt index 10f52d46..c2bca974 100644 --- a/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt @@ -41,4 +41,4 @@ public class R { You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main to reference the layout/main.axml file, or R.strings.first_string to reference the first -string in the dictionary file values/strings.xml. +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs new file mode 100644 index 00000000..a119fbb2 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs @@ -0,0 +1,9543 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +[assembly: global::Android.Runtime.ResourceDesignerAttribute("LaunchDarkly.XamarinSdk.Android.Tests.Resource", IsApplication=true)] + +namespace LaunchDarkly.XamarinSdk.Android.Tests +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + public partial class Resource + { + + static Resource() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + public static void UpdateIdValues() + { + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_in; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_out; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_grow_fade_in_from_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_grow_fade_in_from_bottom; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_popup_enter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_popup_enter; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_popup_exit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_popup_exit; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_shrink_fade_out_from_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_shrink_fade_out_from_bottom; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_slide_in_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_slide_in_bottom; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_slide_in_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_slide_in_top; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_slide_out_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_slide_out_bottom; + global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_slide_out_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_slide_out_top; + global::Xamarin.Forms.Platform.Android.Resource.Animation.design_bottom_sheet_slide_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.design_bottom_sheet_slide_in; + global::Xamarin.Forms.Platform.Android.Resource.Animation.design_bottom_sheet_slide_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.design_bottom_sheet_slide_out; + global::Xamarin.Forms.Platform.Android.Resource.Animation.design_snackbar_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.design_snackbar_in; + global::Xamarin.Forms.Platform.Android.Resource.Animation.design_snackbar_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.design_snackbar_out; + global::Xamarin.Forms.Platform.Android.Resource.Animation.EnterFromLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.EnterFromLeft; + global::Xamarin.Forms.Platform.Android.Resource.Animation.EnterFromRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.EnterFromRight; + global::Xamarin.Forms.Platform.Android.Resource.Animation.ExitToLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.ExitToLeft; + global::Xamarin.Forms.Platform.Android.Resource.Animation.ExitToRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.ExitToRight; + global::Xamarin.Forms.Platform.Android.Resource.Animation.tooltip_enter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.tooltip_enter; + global::Xamarin.Forms.Platform.Android.Resource.Animation.tooltip_exit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.tooltip_exit; + global::Xamarin.Forms.Platform.Android.Resource.Animator.design_appbar_state_list_animator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animator.design_appbar_state_list_animator; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarDivider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarDivider; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarItemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarItemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarPopupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarPopupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarSize; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarSplitStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarSplitStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarTabBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarTabBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarTabStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarTabStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarTabTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarTabTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarWidgetTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionBarWidgetTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionDropDownStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionDropDownStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionMenuTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionMenuTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionMenuTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionMenuTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeCloseButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeCloseButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeCloseDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeCloseDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeCopyDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeCopyDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeCutDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeCutDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeFindDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeFindDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModePasteDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModePasteDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModePopupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModePopupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeSelectAllDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeSelectAllDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeShareDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeShareDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeSplitBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeSplitBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionModeWebSearchDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionModeWebSearchDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionOverflowButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionOverflowButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionOverflowMenuStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionOverflowMenuStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionProviderClass = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionProviderClass; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionViewClass = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.actionViewClass; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.activityChooserViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.activityChooserViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alertDialogButtonGroupStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alertDialogButtonGroupStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alertDialogCenterButtons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alertDialogCenterButtons; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alertDialogStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alertDialogStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alertDialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alertDialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.allowStacking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.allowStacking; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alpha; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.alphabeticModifiers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.alphabeticModifiers; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.arrowHeadLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.arrowHeadLength; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.arrowShaftLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.arrowShaftLength; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoCompleteTextViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoCompleteTextViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoSizeMaxTextSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoSizeMaxTextSize; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoSizeMinTextSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoSizeMinTextSize; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoSizePresetSizes = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoSizePresetSizes; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoSizeStepGranularity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoSizeStepGranularity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.autoSizeTextType = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.autoSizeTextType; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.background; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.backgroundSplit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.backgroundSplit; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.backgroundStacked = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.backgroundStacked; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.backgroundTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.backgroundTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.backgroundTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.backgroundTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.barLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.barLength; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.behavior_autoHide = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.behavior_autoHide; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.behavior_hideable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.behavior_hideable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.behavior_overlapTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.behavior_overlapTop; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.behavior_peekHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.behavior_peekHeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.behavior_skipCollapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.behavior_skipCollapsed; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.borderWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.borderWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.borderlessButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.borderlessButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.bottomSheetDialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.bottomSheetDialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.bottomSheetStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.bottomSheetStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonBarButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonBarButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonBarNegativeButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonBarNegativeButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonBarNeutralButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonBarNeutralButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonBarPositiveButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonBarPositiveButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonGravity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonPanelSideLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonPanelSideLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonStyleSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonStyleSmall; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.buttonTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.buttonTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardBackgroundColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardBackgroundColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardCornerRadius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardCornerRadius; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardElevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardElevation; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardMaxElevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardMaxElevation; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardPreventCornerOverlap = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardPreventCornerOverlap; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.cardUseCompatPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.cardUseCompatPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.checkboxStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.checkboxStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.checkedTextViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.checkedTextViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.closeIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.closeIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.closeItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.closeItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.collapseContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.collapseContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.collapseIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.collapseIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.collapsedTitleGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.collapsedTitleGravity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.collapsedTitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.collapsedTitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.color; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorAccent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorAccent; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorBackgroundFloating = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorBackgroundFloating; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorButtonNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorButtonNormal; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorControlActivated = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorControlActivated; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorControlHighlight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorControlHighlight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorControlNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorControlNormal; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorError = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorError; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorPrimary = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorPrimary; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorPrimaryDark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorPrimaryDark; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.colorSwitchThumbNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.colorSwitchThumbNormal; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.commitIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.commitIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetEndWithActions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetEndWithActions; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetLeft; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetRight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentInsetStartWithNavigation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentInsetStartWithNavigation; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentPaddingBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentPaddingBottom; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentPaddingLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentPaddingLeft; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentPaddingRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentPaddingRight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentPaddingTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentPaddingTop; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.contentScrim = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.contentScrim; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.controlBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.controlBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.counterEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.counterEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.counterMaxLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.counterMaxLength; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.counterOverflowTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.counterOverflowTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.counterTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.counterTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.customNavigationLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.customNavigationLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.defaultQueryHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.defaultQueryHint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dialogPreferredPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dialogPreferredPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.displayOptions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.displayOptions; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.divider; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dividerHorizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dividerHorizontal; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dividerPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dividerPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dividerVertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dividerVertical; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.drawableSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.drawableSize; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.drawerArrowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.drawerArrowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dropDownListViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dropDownListViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.dropdownListPreferredItemHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.dropdownListPreferredItemHeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.editTextBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.editTextBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.editTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.editTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.editTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.editTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.elevation; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.errorEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.errorEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.errorTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.errorTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandActivityOverflowButtonDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandActivityOverflowButtonDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expanded = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expanded; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleGravity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleMargin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleMargin; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleMarginBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleMarginBottom; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleMarginEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleMarginEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleMarginStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleMarginStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleMarginTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleMarginTop; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.expandedTitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.expandedTitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fabSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fabSize; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fastScrollEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fastScrollEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fastScrollHorizontalThumbDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fastScrollHorizontalThumbDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fastScrollHorizontalTrackDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fastScrollHorizontalTrackDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fastScrollVerticalThumbDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fastScrollVerticalThumbDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fastScrollVerticalTrackDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fastScrollVerticalTrackDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.font; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontFamily; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderAuthority; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderCerts; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchStrategy; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchTimeout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderPackage; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderQuery; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontWeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.foregroundInsidePadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.foregroundInsidePadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.gapBetweenBars = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.gapBetweenBars; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.goIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.goIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.headerLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.headerLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.height; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.hideOnContentScroll = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.hideOnContentScroll; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.hintAnimationEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.hintAnimationEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.hintEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.hintEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.hintTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.hintTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.homeAsUpIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.homeAsUpIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.homeLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.homeLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.icon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.iconTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.iconTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.iconTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.iconTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.iconifiedByDefault = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.iconifiedByDefault; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.imageButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.imageButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.indeterminateProgressStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.indeterminateProgressStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.initialActivityCount = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.initialActivityCount; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.insetForeground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.insetForeground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.isLightTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.isLightTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.itemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.itemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.itemIconTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.itemIconTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.itemPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.itemPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.itemTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.itemTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.itemTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.itemTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.keylines = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.keylines; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layoutManager = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layoutManager; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_anchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_anchor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_anchorGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_anchorGravity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_behavior = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_behavior; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_collapseMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_collapseMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_collapseParallaxMultiplier = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_collapseParallaxMultiplier; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_dodgeInsetEdges = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_dodgeInsetEdges; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_insetEdge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_insetEdge; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_keyline = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_keyline; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_scrollFlags = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_scrollFlags; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.layout_scrollInterpolator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.layout_scrollInterpolator; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listChoiceBackgroundIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listChoiceBackgroundIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listDividerAlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listDividerAlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listMenuViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listMenuViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPopupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPopupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPreferredItemHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPreferredItemHeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPreferredItemHeightLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPreferredItemHeightLarge; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPreferredItemHeightSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPreferredItemHeightSmall; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPreferredItemPaddingLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPreferredItemPaddingLeft; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.listPreferredItemPaddingRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.listPreferredItemPaddingRight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.logo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.logo; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.logoDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.logoDescription; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.maxActionInlineWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.maxActionInlineWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.maxButtonHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.maxButtonHeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.measureWithLargestChild = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.measureWithLargestChild; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.menu; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.multiChoiceItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.multiChoiceItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.navigationContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.navigationContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.navigationIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.navigationIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.navigationMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.navigationMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.numericModifiers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.numericModifiers; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.overlapAnchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.overlapAnchor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.paddingBottomNoButtons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.paddingBottomNoButtons; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.paddingEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.paddingEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.paddingStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.paddingStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.paddingTopNoTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.paddingTopNoTitle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.panelBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.panelBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.panelMenuListTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.panelMenuListTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.panelMenuListWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.panelMenuListWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.passwordToggleContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.passwordToggleContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.passwordToggleDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.passwordToggleDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.passwordToggleEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.passwordToggleEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.passwordToggleTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.passwordToggleTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.passwordToggleTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.passwordToggleTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.popupMenuStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.popupMenuStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.popupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.popupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.popupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.popupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.preserveIconSpacing = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.preserveIconSpacing; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.pressedTranslationZ = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.pressedTranslationZ; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.progressBarPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.progressBarPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.progressBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.progressBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.queryBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.queryBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.queryHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.queryHint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.radioButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.radioButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.ratingBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.ratingBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.ratingBarStyleIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.ratingBarStyleIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.ratingBarStyleSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.ratingBarStyleSmall; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.reverseLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.reverseLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.rippleColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.rippleColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.scrimAnimationDuration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.scrimAnimationDuration; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.scrimVisibleHeightTrigger = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.scrimVisibleHeightTrigger; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.searchHintIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.searchHintIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.searchIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.searchIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.searchViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.searchViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.seekBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.seekBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.selectableItemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.selectableItemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.selectableItemBackgroundBorderless = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.selectableItemBackgroundBorderless; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.showAsAction = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.showAsAction; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.showDividers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.showDividers; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.showText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.showText; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.showTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.showTitle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.singleChoiceItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.singleChoiceItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.spanCount = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.spanCount; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.spinBars = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.spinBars; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.spinnerDropDownItemStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.spinnerDropDownItemStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.spinnerStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.spinnerStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.splitTrack = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.splitTrack; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.srcCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.srcCompat; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.stackFromEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.stackFromEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.state_above_anchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.state_above_anchor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.state_collapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.state_collapsed; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.state_collapsible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.state_collapsible; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.statusBarBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.statusBarBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.statusBarScrim = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.statusBarScrim; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.subMenuArrow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.subMenuArrow; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.submitBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.submitBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.subtitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.subtitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.subtitleTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.subtitleTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.subtitleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.subtitleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.suggestionRowLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.suggestionRowLayout; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.switchMinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.switchMinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.switchPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.switchPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.switchStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.switchStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.switchTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.switchTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabContentStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabContentStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabGravity; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabIndicatorColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabIndicatorColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabIndicatorHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabIndicatorHeight; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabMaxWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabMaxWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabMinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabMinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabPaddingBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabPaddingBottom; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabPaddingEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabPaddingEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabPaddingStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabPaddingStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabPaddingTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabPaddingTop; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabSelectedTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabSelectedTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tabTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tabTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAllCaps = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAllCaps; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceLargePopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceLargePopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceListItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceListItem; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceListItemSecondary = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceListItemSecondary; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceListItemSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceListItemSmall; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearancePopupMenuHeader = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearancePopupMenuHeader; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceSearchResultSubtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceSearchResultSubtitle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceSearchResultTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceSearchResultTitle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textAppearanceSmallPopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textAppearanceSmallPopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textColorAlertDialogListItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textColorAlertDialogListItem; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textColorError = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textColorError; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.textColorSearchUrl = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.textColorSearchUrl; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.theme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.theme; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.thickness = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.thickness; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.thumbTextPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.thumbTextPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.thumbTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.thumbTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.thumbTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.thumbTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tickMark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tickMark; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tickMarkTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tickMarkTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tickMarkTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tickMarkTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.title; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMargin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMargin; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMarginBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMarginBottom; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMarginEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMarginEnd; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMarginStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMarginStart; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMarginTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMarginTop; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleMargins = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleMargins; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.titleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.titleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.toolbarId = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.toolbarId; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.toolbarNavigationButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.toolbarNavigationButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.toolbarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.toolbarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tooltipForegroundColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tooltipForegroundColor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tooltipFrameBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tooltipFrameBackground; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.tooltipText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.tooltipText; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.track = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.track; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.trackTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.trackTint; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.trackTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.trackTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.useCompatPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.useCompatPadding; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.voiceIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.voiceIcon; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowActionBarOverlay = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowActionBarOverlay; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowActionModeOverlay = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowActionModeOverlay; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowFixedHeightMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowFixedHeightMajor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowFixedHeightMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowFixedHeightMinor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowFixedWidthMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowFixedWidthMajor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowFixedWidthMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowFixedWidthMinor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowMinWidthMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowMinWidthMajor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowMinWidthMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowMinWidthMinor; + global::Xamarin.Forms.Platform.Android.Resource.Attribute.windowNoTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.windowNoTitle; + global::Xamarin.Forms.Platform.Android.Resource.Boolean.abc_action_bar_embed_tabs = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_action_bar_embed_tabs; + global::Xamarin.Forms.Platform.Android.Resource.Boolean.abc_allow_stacked_button_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_allow_stacked_button_bar; + global::Xamarin.Forms.Platform.Android.Resource.Boolean.abc_config_actionMenuItemAllCaps = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_config_actionMenuItemAllCaps; + global::Xamarin.Forms.Platform.Android.Resource.Boolean.abc_config_closeDialogWhenTouchOutside = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_config_closeDialogWhenTouchOutside; + global::Xamarin.Forms.Platform.Android.Resource.Boolean.abc_config_showMenuShortcutsWhenKeyboardPresent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_config_showMenuShortcutsWhenKeyboardPresent; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_background_cache_hint_selector_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_background_cache_hint_selector_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_background_cache_hint_selector_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_background_cache_hint_selector_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_btn_colored_borderless_text_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_btn_colored_borderless_text_material; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_btn_colored_text_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_btn_colored_text_material; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_color_highlight_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_color_highlight_material; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_hint_foreground_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_hint_foreground_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_hint_foreground_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_hint_foreground_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_input_method_navigation_guard = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_input_method_navigation_guard; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_primary_text_disable_only_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_primary_text_disable_only_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_primary_text_disable_only_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_primary_text_disable_only_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_primary_text_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_primary_text_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_primary_text_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_primary_text_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_search_url_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_search_url_text; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_search_url_text_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_search_url_text_normal; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_search_url_text_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_search_url_text_pressed; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_search_url_text_selected = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_search_url_text_selected; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_secondary_text_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_secondary_text_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_secondary_text_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_secondary_text_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_btn_checkable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_btn_checkable; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_default = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_default; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_edittext = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_edittext; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_seek_thumb = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_seek_thumb; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_spinner; + global::Xamarin.Forms.Platform.Android.Resource.Color.abc_tint_switch_track = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.abc_tint_switch_track; + global::Xamarin.Forms.Platform.Android.Resource.Color.accent_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.accent_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.accent_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.accent_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.background_floating_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.background_floating_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.background_floating_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.background_floating_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.background_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.background_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.background_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.background_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_disabled_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_disabled_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_disabled_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_disabled_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_inverse_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_inverse_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_inverse_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_inverse_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.bright_foreground_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.bright_foreground_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.button_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.button_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.button_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.button_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.cardview_dark_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.cardview_dark_background; + global::Xamarin.Forms.Platform.Android.Resource.Color.cardview_light_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.cardview_light_background; + global::Xamarin.Forms.Platform.Android.Resource.Color.cardview_shadow_end_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.cardview_shadow_end_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.cardview_shadow_start_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.cardview_shadow_start_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_bottom_navigation_shadow_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_bottom_navigation_shadow_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_error = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_error; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_shadow_end_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_shadow_end_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_shadow_mid_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_shadow_mid_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_shadow_start_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_shadow_start_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_stroke_end_inner_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_stroke_end_inner_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_stroke_end_outer_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_stroke_end_outer_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_stroke_top_inner_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_stroke_top_inner_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_fab_stroke_top_outer_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_fab_stroke_top_outer_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_snackbar_background_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_snackbar_background_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.design_tint_password_toggle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.design_tint_password_toggle; + global::Xamarin.Forms.Platform.Android.Resource.Color.dim_foreground_disabled_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.dim_foreground_disabled_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.dim_foreground_disabled_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.dim_foreground_disabled_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.dim_foreground_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.dim_foreground_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.dim_foreground_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.dim_foreground_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.error_color_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.error_color_material; + global::Xamarin.Forms.Platform.Android.Resource.Color.foreground_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.foreground_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.foreground_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.foreground_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.highlighted_text_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.highlighted_text_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.highlighted_text_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.highlighted_text_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_blue_grey_800 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_blue_grey_800; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_blue_grey_900 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_blue_grey_900; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_blue_grey_950 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_blue_grey_950; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_deep_teal_200 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_deep_teal_200; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_deep_teal_500 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_deep_teal_500; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_100 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_100; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_300 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_300; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_50 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_50; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_600 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_600; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_800 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_800; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_850 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_850; + global::Xamarin.Forms.Platform.Android.Resource.Color.material_grey_900 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.material_grey_900; + global::Xamarin.Forms.Platform.Android.Resource.Color.notification_action_color_filter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_action_color_filter; + global::Xamarin.Forms.Platform.Android.Resource.Color.notification_icon_bg_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_icon_bg_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.notification_material_background_media_default_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_material_background_media_default_color; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_dark_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_dark_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_dark_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_dark_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_text_default_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_text_default_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_text_default_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_text_default_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_text_disabled_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_text_disabled_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.primary_text_disabled_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.primary_text_disabled_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.ripple_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.ripple_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.ripple_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.ripple_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.secondary_text_default_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_default_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.secondary_text_default_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_default_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.secondary_text_disabled_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_disabled_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.secondary_text_disabled_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_disabled_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_disabled_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_disabled_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_disabled_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_disabled_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_normal_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_normal_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.switch_thumb_normal_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.switch_thumb_normal_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Color.tooltip_background_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.tooltip_background_dark; + global::Xamarin.Forms.Platform.Android.Resource.Color.tooltip_background_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.tooltip_background_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_content_inset_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_content_inset_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_content_inset_with_nav = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_content_inset_with_nav; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_default_height_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_default_height_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_default_padding_end_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_default_padding_end_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_default_padding_start_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_default_padding_start_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_elevation_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_elevation_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_icon_vertical_padding_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_icon_vertical_padding_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_overflow_padding_end_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_overflow_padding_end_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_overflow_padding_start_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_overflow_padding_start_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_progress_bar_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_progress_bar_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_stacked_max_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_stacked_max_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_stacked_tab_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_stacked_tab_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_subtitle_bottom_margin_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_subtitle_bottom_margin_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_bar_subtitle_top_margin_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_bar_subtitle_top_margin_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_button_min_height_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_button_min_height_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_button_min_width_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_button_min_width_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_action_button_min_width_overflow_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_action_button_min_width_overflow_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_alert_dialog_button_bar_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_alert_dialog_button_bar_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_button_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_button_inset_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_button_inset_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_button_inset_vertical_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_button_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_button_padding_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_button_padding_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_button_padding_vertical_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_cascading_menus_min_smallest_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_cascading_menus_min_smallest_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_config_prefDialogWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_config_prefDialogWidth; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_control_corner_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_control_corner_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_control_inset_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_control_inset_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_control_padding_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_control_padding_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_fixed_height_major = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_fixed_height_major; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_fixed_height_minor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_fixed_height_minor; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_fixed_width_major = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_fixed_width_major; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_fixed_width_minor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_fixed_width_minor; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_list_padding_bottom_no_buttons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_list_padding_bottom_no_buttons; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_list_padding_top_no_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_list_padding_top_no_title; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_min_width_major = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_min_width_major; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_min_width_minor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_min_width_minor; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_padding_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_padding_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_padding_top_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_padding_top_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dialog_title_divider_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dialog_title_divider_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_disabled_alpha_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_disabled_alpha_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_disabled_alpha_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_disabled_alpha_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dropdownitem_icon_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dropdownitem_icon_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dropdownitem_text_padding_left = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dropdownitem_text_padding_left; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_dropdownitem_text_padding_right = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_dropdownitem_text_padding_right; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_edit_text_inset_bottom_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_edit_text_inset_bottom_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_edit_text_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_edit_text_inset_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_edit_text_inset_top_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_edit_text_inset_top_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_floating_window_z = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_floating_window_z; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_list_item_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_list_item_padding_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_panel_menu_list_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_panel_menu_list_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_progress_bar_height_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_progress_bar_height_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_search_view_preferred_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_search_view_preferred_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_search_view_preferred_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_search_view_preferred_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_seekbar_track_background_height_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_seekbar_track_background_height_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_seekbar_track_progress_height_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_seekbar_track_progress_height_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_select_dialog_padding_start_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_select_dialog_padding_start_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_switch_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_switch_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_body_1_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_body_1_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_body_2_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_body_2_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_button_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_button_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_caption_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_caption_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_display_1_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_display_1_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_display_2_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_display_2_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_display_3_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_display_3_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_display_4_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_display_4_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_headline_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_headline_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_large_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_large_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_medium_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_medium_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_menu_header_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_menu_header_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_menu_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_menu_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_small_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_small_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_subhead_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_subhead_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_subtitle_material_toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_subtitle_material_toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_title_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_title_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.abc_text_size_title_material_toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.abc_text_size_title_material_toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.cardview_compat_inset_shadow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.cardview_compat_inset_shadow; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.cardview_default_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.cardview_default_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.cardview_default_radius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.cardview_default_radius; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.compat_button_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.compat_button_inset_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_vertical_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.compat_button_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_horizontal_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.compat_button_padding_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_vertical_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.compat_control_corner_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_control_corner_material; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_appbar_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_appbar_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_active_item_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_active_item_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_active_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_active_text_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_item_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_item_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_item_min_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_item_min_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_margin; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_shadow_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_shadow_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_navigation_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_navigation_text_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_sheet_modal_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_sheet_modal_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_bottom_sheet_peek_height_min = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_bottom_sheet_peek_height_min; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_border_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_border_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_image_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_image_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_size_mini = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_size_mini; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_size_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_size_normal; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_fab_translation_z_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_fab_translation_z_pressed; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_icon_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_icon_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_icon_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_padding_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_padding_bottom; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_navigation_separator_vertical_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_navigation_separator_vertical_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_action_inline_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_action_inline_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_background_corner_radius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_background_corner_radius; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_extra_spacing_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_extra_spacing_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_min_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_min_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_padding_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_padding_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_padding_vertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_padding_vertical; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_padding_vertical_2lines = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_padding_vertical_2lines; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_snackbar_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_snackbar_text_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_tab_max_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_tab_max_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_tab_scrollable_min_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_tab_scrollable_min_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_tab_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_tab_text_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.design_tab_text_size_2line = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.design_tab_text_size_2line; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.disabled_alpha_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.disabled_alpha_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.disabled_alpha_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.disabled_alpha_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.fastscroll_default_thickness = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.fastscroll_default_thickness; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.fastscroll_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.fastscroll_margin; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.fastscroll_minimum_range = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.fastscroll_minimum_range; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.highlight_alpha_material_colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.highlight_alpha_material_colored; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.highlight_alpha_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.highlight_alpha_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.highlight_alpha_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.highlight_alpha_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.hint_alpha_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.hint_alpha_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.hint_alpha_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.hint_alpha_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.hint_pressed_alpha_material_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.hint_pressed_alpha_material_dark; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.hint_pressed_alpha_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.hint_pressed_alpha_material_light; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.item_touch_helper_max_drag_scroll_per_frame = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.item_touch_helper_max_drag_scroll_per_frame; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.item_touch_helper_swipe_escape_max_velocity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.item_touch_helper_swipe_escape_max_velocity; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.item_touch_helper_swipe_escape_velocity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.item_touch_helper_swipe_escape_velocity; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_action_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_icon_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_action_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_text_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_big_circle_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_big_circle_margin; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_content_margin_start = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_content_margin_start; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_large_icon_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_height; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_large_icon_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_width; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_main_column_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_main_column_padding_top; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_media_narrow_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_media_narrow_margin; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_right_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_icon_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_right_side_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_side_padding_top; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_small_icon_background_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_background_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_small_icon_size_as_large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_size_as_large; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_subtext_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_subtext_size; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_top_pad = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.notification_top_pad_large_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad_large_text; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_corner_radius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_corner_radius; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_horizontal_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_horizontal_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_margin; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_precise_anchor_extra_offset = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_precise_anchor_extra_offset; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_precise_anchor_threshold = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_precise_anchor_threshold; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_vertical_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_vertical_padding; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_y_offset_non_touch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_y_offset_non_touch; + global::Xamarin.Forms.Platform.Android.Resource.Dimension.tooltip_y_offset_touch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.tooltip_y_offset_touch; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ab_share_pack_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ab_share_pack_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_action_bar_item_background_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_action_bar_item_background_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_borderless_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_borderless_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_check_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_check_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_check_to_on_mtrl_000 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_check_to_on_mtrl_000; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_check_to_on_mtrl_015 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_check_to_on_mtrl_015; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_colored_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_colored_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_default_mtrl_shape = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_default_mtrl_shape; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_radio_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_radio_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_radio_to_on_mtrl_000 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_radio_to_on_mtrl_000; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_radio_to_on_mtrl_015 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_radio_to_on_mtrl_015; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_switch_to_on_mtrl_00001 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_switch_to_on_mtrl_00001; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_btn_switch_to_on_mtrl_00012 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_btn_switch_to_on_mtrl_00012; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_cab_background_internal_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_cab_background_internal_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_cab_background_top_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_cab_background_top_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_cab_background_top_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_cab_background_top_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_control_background_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_control_background_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_dialog_material_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_dialog_material_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_edit_text_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_edit_text_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_ab_back_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_ab_back_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_arrow_drop_right_black_24dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_arrow_drop_right_black_24dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_clear_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_clear_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_commit_search_api_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_commit_search_api_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_go_search_api_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_go_search_api_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_copy_mtrl_am_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_copy_mtrl_am_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_cut_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_cut_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_overflow_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_overflow_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_paste_mtrl_am_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_paste_mtrl_am_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_selectall_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_selectall_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_menu_share_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_menu_share_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_search_api_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_search_api_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_black_16dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_black_16dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_black_36dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_black_36dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_black_48dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_black_48dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_half_black_16dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_half_black_16dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_half_black_36dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_half_black_36dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_star_half_black_48dp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_star_half_black_48dp; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ic_voice_search_api_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ic_voice_search_api_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_item_background_holo_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_item_background_holo_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_item_background_holo_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_item_background_holo_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_divider_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_divider_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_focused_holo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_focused_holo; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_longpressed_holo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_longpressed_holo; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_pressed_holo_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_pressed_holo_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_pressed_holo_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_pressed_holo_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_background_transition_holo_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_background_transition_holo_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_background_transition_holo_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_background_transition_holo_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_disabled_holo_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_disabled_holo_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_disabled_holo_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_disabled_holo_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_holo_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_holo_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_list_selector_holo_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_list_selector_holo_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_menu_hardkey_panel_mtrl_mult = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_menu_hardkey_panel_mtrl_mult; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_popup_background_mtrl_mult = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_popup_background_mtrl_mult; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ratingbar_indicator_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ratingbar_indicator_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ratingbar_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ratingbar_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_ratingbar_small_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_ratingbar_small_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_scrubber_control_off_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_scrubber_control_off_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_scrubber_control_to_pressed_mtrl_000 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_scrubber_control_to_pressed_mtrl_000; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_scrubber_control_to_pressed_mtrl_005 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_scrubber_control_to_pressed_mtrl_005; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_scrubber_primary_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_scrubber_primary_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_scrubber_track_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_scrubber_track_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_seekbar_thumb_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_seekbar_thumb_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_seekbar_tick_mark_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_seekbar_tick_mark_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_seekbar_track_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_seekbar_track_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_spinner_mtrl_am_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_spinner_mtrl_am_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_spinner_textfield_background_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_spinner_textfield_background_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_switch_thumb_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_switch_thumb_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_switch_track_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_switch_track_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_tab_indicator_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_tab_indicator_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_tab_indicator_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_tab_indicator_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_cursor_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_cursor_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_left_mtrl_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_left_mtrl_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_left_mtrl_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_left_mtrl_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_middle_mtrl_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_middle_mtrl_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_middle_mtrl_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_middle_mtrl_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_right_mtrl_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_right_mtrl_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_text_select_handle_right_mtrl_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_text_select_handle_right_mtrl_light; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_textfield_activated_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_textfield_activated_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_textfield_default_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_textfield_default_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_textfield_search_activated_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_textfield_search_activated_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_textfield_search_default_mtrl_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_textfield_search_default_mtrl_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_textfield_search_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_textfield_search_material; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.abc_vector_test = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.abc_vector_test; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_hide_password = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_hide_password; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_hide_password_1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_hide_password_1; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_hide_password_2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_hide_password_2; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_hide_password_3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_hide_password_3; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_show_password = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_show_password; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_show_password_1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_show_password_1; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_show_password_2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_show_password_2; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.avd_show_password_3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.avd_show_password_3; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_bottom_navigation_item_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_bottom_navigation_item_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_fab_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_fab_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_ic_visibility = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_ic_visibility; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_ic_visibility_off = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_ic_visibility_off; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_password_eye = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_password_eye; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.design_snackbar_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.design_snackbar_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.navigation_empty_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.navigation_empty_icon; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_action_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_action_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg_low = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg_low_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_normal; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg_low_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_pressed; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_bg_normal_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal_pressed; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_icon_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_icon_background; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_template_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_template_icon_low_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_low_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notification_tile_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_tile_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.notify_panel_notification_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notify_panel_notification_icon_bg; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.tooltip_frame_dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.tooltip_frame_dark; + global::Xamarin.Forms.Platform.Android.Resource.Drawable.tooltip_frame_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.tooltip_frame_light; + global::Xamarin.Forms.Platform.Android.Resource.Id.ALT = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.ALT; + global::Xamarin.Forms.Platform.Android.Resource.Id.CTRL = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.CTRL; + global::Xamarin.Forms.Platform.Android.Resource.Id.FUNCTION = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.FUNCTION; + global::Xamarin.Forms.Platform.Android.Resource.Id.META = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.META; + global::Xamarin.Forms.Platform.Android.Resource.Id.SHIFT = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.SHIFT; + global::Xamarin.Forms.Platform.Android.Resource.Id.SYM = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.SYM; + global::Xamarin.Forms.Platform.Android.Resource.Id.action0 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action0; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_activity_content = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_activity_content; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_container; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_root = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_root; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_spinner; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_bar_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_bar_title; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_container; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_context_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_context_bar; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_divider; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_image = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_image; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_menu_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_menu_divider; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_menu_presenter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_menu_presenter; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_mode_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_mode_bar; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_mode_bar_stub = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_mode_bar_stub; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_mode_close_button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_mode_close_button; + global::Xamarin.Forms.Platform.Android.Resource.Id.action_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_text; + global::Xamarin.Forms.Platform.Android.Resource.Id.actions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.actions; + global::Xamarin.Forms.Platform.Android.Resource.Id.activity_chooser_view_content = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.activity_chooser_view_content; + global::Xamarin.Forms.Platform.Android.Resource.Id.add = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.add; + global::Xamarin.Forms.Platform.Android.Resource.Id.alertTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.alertTitle; + global::Xamarin.Forms.Platform.Android.Resource.Id.all = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.all; + global::Xamarin.Forms.Platform.Android.Resource.Id.always = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.always; + global::Xamarin.Forms.Platform.Android.Resource.Id.async = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.async; + global::Xamarin.Forms.Platform.Android.Resource.Id.auto = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.auto; + global::Xamarin.Forms.Platform.Android.Resource.Id.beginning = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.beginning; + global::Xamarin.Forms.Platform.Android.Resource.Id.blocking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.blocking; + global::Xamarin.Forms.Platform.Android.Resource.Id.bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.bottom; + global::Xamarin.Forms.Platform.Android.Resource.Id.bottomtab_navarea = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.bottomtab_navarea; + global::Xamarin.Forms.Platform.Android.Resource.Id.bottomtab_tabbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.bottomtab_tabbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.buttonPanel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.buttonPanel; + global::Xamarin.Forms.Platform.Android.Resource.Id.cancel_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.cancel_action; + global::Xamarin.Forms.Platform.Android.Resource.Id.center = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.center; + global::Xamarin.Forms.Platform.Android.Resource.Id.center_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.center_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Id.center_vertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.center_vertical; + global::Xamarin.Forms.Platform.Android.Resource.Id.checkbox = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.checkbox; + global::Xamarin.Forms.Platform.Android.Resource.Id.chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.chronometer; + global::Xamarin.Forms.Platform.Android.Resource.Id.clip_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.clip_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Id.clip_vertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.clip_vertical; + global::Xamarin.Forms.Platform.Android.Resource.Id.collapseActionView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.collapseActionView; + global::Xamarin.Forms.Platform.Android.Resource.Id.container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.container; + global::Xamarin.Forms.Platform.Android.Resource.Id.contentPanel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.contentPanel; + global::Xamarin.Forms.Platform.Android.Resource.Id.coordinator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.coordinator; + global::Xamarin.Forms.Platform.Android.Resource.Id.custom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.custom; + global::Xamarin.Forms.Platform.Android.Resource.Id.customPanel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.customPanel; + global::Xamarin.Forms.Platform.Android.Resource.Id.decor_content_parent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.decor_content_parent; + global::Xamarin.Forms.Platform.Android.Resource.Id.default_activity_button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.default_activity_button; + global::Xamarin.Forms.Platform.Android.Resource.Id.design_bottom_sheet = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.design_bottom_sheet; + global::Xamarin.Forms.Platform.Android.Resource.Id.design_menu_item_action_area = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.design_menu_item_action_area; + global::Xamarin.Forms.Platform.Android.Resource.Id.design_menu_item_action_area_stub = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.design_menu_item_action_area_stub; + global::Xamarin.Forms.Platform.Android.Resource.Id.design_menu_item_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.design_menu_item_text; + global::Xamarin.Forms.Platform.Android.Resource.Id.design_navigation_view = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.design_navigation_view; + global::Xamarin.Forms.Platform.Android.Resource.Id.disableHome = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.disableHome; + global::Xamarin.Forms.Platform.Android.Resource.Id.edit_query = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.edit_query; + global::Xamarin.Forms.Platform.Android.Resource.Id.end = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.end; + global::Xamarin.Forms.Platform.Android.Resource.Id.end_padder = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.end_padder; + global::Xamarin.Forms.Platform.Android.Resource.Id.enterAlways = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.enterAlways; + global::Xamarin.Forms.Platform.Android.Resource.Id.enterAlwaysCollapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.enterAlwaysCollapsed; + global::Xamarin.Forms.Platform.Android.Resource.Id.exitUntilCollapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.exitUntilCollapsed; + global::Xamarin.Forms.Platform.Android.Resource.Id.expand_activities_button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.expand_activities_button; + global::Xamarin.Forms.Platform.Android.Resource.Id.expanded_menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.expanded_menu; + global::Xamarin.Forms.Platform.Android.Resource.Id.fill = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.fill; + global::Xamarin.Forms.Platform.Android.Resource.Id.fill_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.fill_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Id.fill_vertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.fill_vertical; + global::Xamarin.Forms.Platform.Android.Resource.Id.@fixed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.@fixed; + global::Xamarin.Forms.Platform.Android.Resource.Id.flyoutcontent_appbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.flyoutcontent_appbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.flyoutcontent_recycler = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.flyoutcontent_recycler; + global::Xamarin.Forms.Platform.Android.Resource.Id.forever = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.forever; + global::Xamarin.Forms.Platform.Android.Resource.Id.ghost_view = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.ghost_view; + global::Xamarin.Forms.Platform.Android.Resource.Id.home = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.home; + global::Xamarin.Forms.Platform.Android.Resource.Id.homeAsUp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.homeAsUp; + global::Xamarin.Forms.Platform.Android.Resource.Id.icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon; + global::Xamarin.Forms.Platform.Android.Resource.Id.icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon_group; + global::Xamarin.Forms.Platform.Android.Resource.Id.ifRoom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.ifRoom; + global::Xamarin.Forms.Platform.Android.Resource.Id.image = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.image; + global::Xamarin.Forms.Platform.Android.Resource.Id.info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.info; + global::Xamarin.Forms.Platform.Android.Resource.Id.italic = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.italic; + global::Xamarin.Forms.Platform.Android.Resource.Id.item_touch_helper_previous_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.item_touch_helper_previous_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Id.largeLabel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.largeLabel; + global::Xamarin.Forms.Platform.Android.Resource.Id.left = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.left; + global::Xamarin.Forms.Platform.Android.Resource.Id.line1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line1; + global::Xamarin.Forms.Platform.Android.Resource.Id.line3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line3; + global::Xamarin.Forms.Platform.Android.Resource.Id.listMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.listMode; + global::Xamarin.Forms.Platform.Android.Resource.Id.list_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.list_item; + global::Xamarin.Forms.Platform.Android.Resource.Id.main_appbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.main_appbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.main_scrollview = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.main_scrollview; + global::Xamarin.Forms.Platform.Android.Resource.Id.main_tablayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.main_tablayout; + global::Xamarin.Forms.Platform.Android.Resource.Id.main_toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.main_toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.masked = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.masked; + global::Xamarin.Forms.Platform.Android.Resource.Id.media_actions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.media_actions; + global::Xamarin.Forms.Platform.Android.Resource.Id.message = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.message; + global::Xamarin.Forms.Platform.Android.Resource.Id.middle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.middle; + global::Xamarin.Forms.Platform.Android.Resource.Id.mini = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.mini; + global::Xamarin.Forms.Platform.Android.Resource.Id.multiply = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.multiply; + global::Xamarin.Forms.Platform.Android.Resource.Id.navigation_header_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.navigation_header_container; + global::Xamarin.Forms.Platform.Android.Resource.Id.never = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.never; + global::Xamarin.Forms.Platform.Android.Resource.Id.none = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.none; + global::Xamarin.Forms.Platform.Android.Resource.Id.normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.normal; + global::Xamarin.Forms.Platform.Android.Resource.Id.notification_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_background; + global::Xamarin.Forms.Platform.Android.Resource.Id.notification_main_column = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column; + global::Xamarin.Forms.Platform.Android.Resource.Id.notification_main_column_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column_container; + global::Xamarin.Forms.Platform.Android.Resource.Id.parallax = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.parallax; + global::Xamarin.Forms.Platform.Android.Resource.Id.parentPanel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.parentPanel; + global::Xamarin.Forms.Platform.Android.Resource.Id.parent_matrix = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.parent_matrix; + global::Xamarin.Forms.Platform.Android.Resource.Id.pin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.pin; + global::Xamarin.Forms.Platform.Android.Resource.Id.progress_circular = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.progress_circular; + global::Xamarin.Forms.Platform.Android.Resource.Id.progress_horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.progress_horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Id.radio = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.radio; + global::Xamarin.Forms.Platform.Android.Resource.Id.right = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right; + global::Xamarin.Forms.Platform.Android.Resource.Id.right_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_icon; + global::Xamarin.Forms.Platform.Android.Resource.Id.right_side = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_side; + global::Xamarin.Forms.Platform.Android.Resource.Id.save_image_matrix = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.save_image_matrix; + global::Xamarin.Forms.Platform.Android.Resource.Id.save_non_transition_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.save_non_transition_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Id.save_scale_type = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.save_scale_type; + global::Xamarin.Forms.Platform.Android.Resource.Id.screen = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.screen; + global::Xamarin.Forms.Platform.Android.Resource.Id.scroll = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.scroll; + global::Xamarin.Forms.Platform.Android.Resource.Id.scrollIndicatorDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.scrollIndicatorDown; + global::Xamarin.Forms.Platform.Android.Resource.Id.scrollIndicatorUp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.scrollIndicatorUp; + global::Xamarin.Forms.Platform.Android.Resource.Id.scrollView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.scrollView; + global::Xamarin.Forms.Platform.Android.Resource.Id.scrollable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.scrollable; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_badge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_badge; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_bar; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_button; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_close_btn = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_close_btn; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_edit_frame = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_edit_frame; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_go_btn = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_go_btn; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_mag_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_mag_icon; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_plate = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_plate; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_src_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_src_text; + global::Xamarin.Forms.Platform.Android.Resource.Id.search_voice_btn = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.search_voice_btn; + global::Xamarin.Forms.Platform.Android.Resource.Id.select_dialog_listview = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.select_dialog_listview; + global::Xamarin.Forms.Platform.Android.Resource.Id.shellcontent_appbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.shellcontent_appbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.shellcontent_scrollview = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.shellcontent_scrollview; + global::Xamarin.Forms.Platform.Android.Resource.Id.shellcontent_toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.shellcontent_toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Id.shortcut = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.shortcut; + global::Xamarin.Forms.Platform.Android.Resource.Id.showCustom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.showCustom; + global::Xamarin.Forms.Platform.Android.Resource.Id.showHome = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.showHome; + global::Xamarin.Forms.Platform.Android.Resource.Id.showTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.showTitle; + global::Xamarin.Forms.Platform.Android.Resource.Id.smallLabel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.smallLabel; + global::Xamarin.Forms.Platform.Android.Resource.Id.snackbar_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.snackbar_action; + global::Xamarin.Forms.Platform.Android.Resource.Id.snackbar_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.snackbar_text; + global::Xamarin.Forms.Platform.Android.Resource.Id.snap = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.snap; + global::Xamarin.Forms.Platform.Android.Resource.Id.spacer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.spacer; + global::Xamarin.Forms.Platform.Android.Resource.Id.split_action_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.split_action_bar; + global::Xamarin.Forms.Platform.Android.Resource.Id.src_atop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.src_atop; + global::Xamarin.Forms.Platform.Android.Resource.Id.src_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.src_in; + global::Xamarin.Forms.Platform.Android.Resource.Id.src_over = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.src_over; + global::Xamarin.Forms.Platform.Android.Resource.Id.start = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.start; + global::Xamarin.Forms.Platform.Android.Resource.Id.status_bar_latest_event_content = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.status_bar_latest_event_content; + global::Xamarin.Forms.Platform.Android.Resource.Id.submenuarrow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.submenuarrow; + global::Xamarin.Forms.Platform.Android.Resource.Id.submit_area = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.submit_area; + global::Xamarin.Forms.Platform.Android.Resource.Id.tabMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.tabMode; + global::Xamarin.Forms.Platform.Android.Resource.Id.tag_transition_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.tag_transition_group; + global::Xamarin.Forms.Platform.Android.Resource.Id.text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text; + global::Xamarin.Forms.Platform.Android.Resource.Id.text2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text2; + global::Xamarin.Forms.Platform.Android.Resource.Id.textSpacerNoButtons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.textSpacerNoButtons; + global::Xamarin.Forms.Platform.Android.Resource.Id.textSpacerNoTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.textSpacerNoTitle; + global::Xamarin.Forms.Platform.Android.Resource.Id.text_input_password_toggle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text_input_password_toggle; + global::Xamarin.Forms.Platform.Android.Resource.Id.textinput_counter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.textinput_counter; + global::Xamarin.Forms.Platform.Android.Resource.Id.textinput_error = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.textinput_error; + global::Xamarin.Forms.Platform.Android.Resource.Id.time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.time; + global::Xamarin.Forms.Platform.Android.Resource.Id.title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.title; + global::Xamarin.Forms.Platform.Android.Resource.Id.titleDividerNoCustom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.titleDividerNoCustom; + global::Xamarin.Forms.Platform.Android.Resource.Id.title_template = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.title_template; + global::Xamarin.Forms.Platform.Android.Resource.Id.top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.top; + global::Xamarin.Forms.Platform.Android.Resource.Id.topPanel = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.topPanel; + global::Xamarin.Forms.Platform.Android.Resource.Id.touch_outside = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.touch_outside; + global::Xamarin.Forms.Platform.Android.Resource.Id.transition_current_scene = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.transition_current_scene; + global::Xamarin.Forms.Platform.Android.Resource.Id.transition_layout_save = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.transition_layout_save; + global::Xamarin.Forms.Platform.Android.Resource.Id.transition_position = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.transition_position; + global::Xamarin.Forms.Platform.Android.Resource.Id.transition_scene_layoutid_cache = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.transition_scene_layoutid_cache; + global::Xamarin.Forms.Platform.Android.Resource.Id.transition_transform = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.transition_transform; + global::Xamarin.Forms.Platform.Android.Resource.Id.uniform = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.uniform; + global::Xamarin.Forms.Platform.Android.Resource.Id.up = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.up; + global::Xamarin.Forms.Platform.Android.Resource.Id.useLogo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.useLogo; + global::Xamarin.Forms.Platform.Android.Resource.Id.view_offset_helper = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.view_offset_helper; + global::Xamarin.Forms.Platform.Android.Resource.Id.visible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.visible; + global::Xamarin.Forms.Platform.Android.Resource.Id.withText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.withText; + global::Xamarin.Forms.Platform.Android.Resource.Id.wrap_content = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.wrap_content; + global::Xamarin.Forms.Platform.Android.Resource.Integer.abc_config_activityDefaultDur = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.abc_config_activityDefaultDur; + global::Xamarin.Forms.Platform.Android.Resource.Integer.abc_config_activityShortDur = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.abc_config_activityShortDur; + global::Xamarin.Forms.Platform.Android.Resource.Integer.app_bar_elevation_anim_duration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.app_bar_elevation_anim_duration; + global::Xamarin.Forms.Platform.Android.Resource.Integer.bottom_sheet_slide_duration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.bottom_sheet_slide_duration; + global::Xamarin.Forms.Platform.Android.Resource.Integer.cancel_button_image_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.cancel_button_image_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Integer.config_tooltipAnimTime = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.config_tooltipAnimTime; + global::Xamarin.Forms.Platform.Android.Resource.Integer.design_snackbar_text_max_lines = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.design_snackbar_text_max_lines; + global::Xamarin.Forms.Platform.Android.Resource.Integer.hide_password_duration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.hide_password_duration; + global::Xamarin.Forms.Platform.Android.Resource.Integer.show_password_duration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.show_password_duration; + global::Xamarin.Forms.Platform.Android.Resource.Integer.status_bar_notification_info_maxnum = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.status_bar_notification_info_maxnum; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_bar_title_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_bar_title_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_bar_up_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_bar_up_container; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_menu_item_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_menu_item_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_menu_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_menu_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_mode_bar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_mode_bar; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_action_mode_close_item_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_action_mode_close_item_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_activity_chooser_view = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_activity_chooser_view; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_activity_chooser_view_list_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_activity_chooser_view_list_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_alert_dialog_button_bar_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_alert_dialog_button_bar_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_alert_dialog_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_alert_dialog_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_alert_dialog_title_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_alert_dialog_title_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_dialog_title_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_dialog_title_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_expanded_menu_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_expanded_menu_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_list_menu_item_checkbox = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_list_menu_item_checkbox; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_list_menu_item_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_list_menu_item_icon; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_list_menu_item_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_list_menu_item_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_list_menu_item_radio = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_list_menu_item_radio; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_popup_menu_header_item_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_popup_menu_header_item_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_popup_menu_item_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_popup_menu_item_layout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_screen_content_include = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_screen_content_include; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_screen_simple = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_screen_simple; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_screen_simple_overlay_action_mode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_screen_simple_overlay_action_mode; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_screen_toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_screen_toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_search_dropdown_item_icons_2line = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_search_dropdown_item_icons_2line; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_search_view = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_search_view; + global::Xamarin.Forms.Platform.Android.Resource.Layout.abc_select_dialog_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.abc_select_dialog_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.BottomTabLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.BottomTabLayout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_bottom_navigation_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_bottom_navigation_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_bottom_sheet_dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_bottom_sheet_dialog; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_layout_snackbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_layout_snackbar; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_layout_snackbar_include = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_layout_snackbar_include; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_layout_tab_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_layout_tab_icon; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_layout_tab_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_layout_tab_text; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_menu_item_action_area = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_menu_item_action_area; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_item_header = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_item_header; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_item_separator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_item_separator; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_item_subheader = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_item_subheader; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_menu; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_navigation_menu_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_navigation_menu_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.design_text_input_password_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.design_text_input_password_icon; + global::Xamarin.Forms.Platform.Android.Resource.Layout.FlyoutContent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.FlyoutContent; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_action_tombstone = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action_tombstone; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_media_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_media_action; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_media_cancel_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_media_cancel_action; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_big_media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_big_media; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_big_media_custom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_big_media_custom; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_big_media_narrow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_big_media_narrow; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_big_media_narrow_custom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_big_media_narrow_custom; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_custom_big = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_custom_big; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_icon_group; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_lines_media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_lines_media; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_media; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_media_custom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_media_custom; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_part_chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_chronometer; + global::Xamarin.Forms.Platform.Android.Resource.Layout.notification_template_part_time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_time; + global::Xamarin.Forms.Platform.Android.Resource.Layout.RootLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.RootLayout; + global::Xamarin.Forms.Platform.Android.Resource.Layout.select_dialog_item_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.select_dialog_item_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.select_dialog_multichoice_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.select_dialog_multichoice_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.select_dialog_singlechoice_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.select_dialog_singlechoice_material; + global::Xamarin.Forms.Platform.Android.Resource.Layout.ShellContent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.ShellContent; + global::Xamarin.Forms.Platform.Android.Resource.Layout.support_simple_spinner_dropdown_item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.support_simple_spinner_dropdown_item; + global::Xamarin.Forms.Platform.Android.Resource.Layout.tooltip = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.tooltip; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_action_bar_home_description = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_action_bar_home_description; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_action_bar_up_description = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_action_bar_up_description; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_action_menu_overflow_description = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_action_menu_overflow_description; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_action_mode_done = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_action_mode_done; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_activity_chooser_view_see_all = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_activity_chooser_view_see_all; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_activitychooserview_choose_application = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_activitychooserview_choose_application; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_capital_off = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_capital_off; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_capital_on = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_capital_on; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_body_1_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_body_1_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_body_2_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_body_2_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_button_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_button_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_caption_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_caption_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_display_1_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_display_1_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_display_2_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_display_2_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_display_3_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_display_3_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_display_4_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_display_4_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_headline_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_headline_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_menu_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_menu_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_subhead_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_subhead_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_font_family_title_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_font_family_title_material; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_search_hint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_search_hint; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_searchview_description_clear = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_searchview_description_clear; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_searchview_description_query = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_searchview_description_query; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_searchview_description_search = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_searchview_description_search; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_searchview_description_submit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_searchview_description_submit; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_searchview_description_voice = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_searchview_description_voice; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_shareactionprovider_share_with = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_shareactionprovider_share_with; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_shareactionprovider_share_with_application = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_shareactionprovider_share_with_application; + global::Xamarin.Forms.Platform.Android.Resource.String.abc_toolbar_collapse_description = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.abc_toolbar_collapse_description; + global::Xamarin.Forms.Platform.Android.Resource.String.appbar_scrolling_view_behavior = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.appbar_scrolling_view_behavior; + global::Xamarin.Forms.Platform.Android.Resource.String.bottom_sheet_behavior = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.bottom_sheet_behavior; + global::Xamarin.Forms.Platform.Android.Resource.String.character_counter_pattern = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.character_counter_pattern; + global::Xamarin.Forms.Platform.Android.Resource.String.password_toggle_content_description = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.password_toggle_content_description; + global::Xamarin.Forms.Platform.Android.Resource.String.path_password_eye = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.path_password_eye; + global::Xamarin.Forms.Platform.Android.Resource.String.path_password_eye_mask_strike_through = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.path_password_eye_mask_strike_through; + global::Xamarin.Forms.Platform.Android.Resource.String.path_password_eye_mask_visible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.path_password_eye_mask_visible; + global::Xamarin.Forms.Platform.Android.Resource.String.path_password_strike_through = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.path_password_strike_through; + global::Xamarin.Forms.Platform.Android.Resource.String.search_menu_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.search_menu_title; + global::Xamarin.Forms.Platform.Android.Resource.String.status_bar_notification_info_overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.status_bar_notification_info_overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.AlertDialog_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.AlertDialog_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.AlertDialog_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.AlertDialog_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Animation_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Animation_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Animation_AppCompat_DropDownUp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Animation_AppCompat_DropDownUp; + global::Xamarin.Forms.Platform.Android.Resource.Style.Animation_AppCompat_Tooltip = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Animation_AppCompat_Tooltip; + global::Xamarin.Forms.Platform.Android.Resource.Style.Animation_Design_BottomSheetDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Animation_Design_BottomSheetDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_AlertDialog_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_AlertDialog_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_AlertDialog_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_AlertDialog_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Animation_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Animation_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Animation_AppCompat_DropDownUp = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Animation_AppCompat_DropDownUp; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Animation_AppCompat_Tooltip = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Animation_AppCompat_Tooltip; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_CardView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_CardView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_DialogWindowTitle_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_DialogWindowTitle_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_DialogWindowTitleBackground_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_DialogWindowTitleBackground_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Body1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Body1; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Body2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Body2; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Caption = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Caption; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Display1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Display1; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Display2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Display2; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Display3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Display3; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Display4 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Display4; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Headline = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Headline; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Large_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Large_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Medium = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Medium; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Medium_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Medium_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_SearchResult = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_SearchResult; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_SearchResult_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_SearchResult_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_SearchResult_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_SearchResult_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Small_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Small_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Subhead = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Subhead; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Subhead_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Subhead_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Title_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Title_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Tooltip = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Tooltip; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionMode_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionMode_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionMode_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_ActionMode_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Borderless_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Borderless_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_Button_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_DropDownItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_DropDownItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Header = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Header; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_PopupMenu_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_Switch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_Switch; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_AppCompat_Widget_TextView_SpinnerItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_AppCompat_Widget_TextView_SpinnerItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_Widget_AppCompat_ExpandedMenu_Item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_Widget_AppCompat_ExpandedMenu_Item; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_Widget_AppCompat_Toolbar_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_Widget_AppCompat_Toolbar_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_TextAppearance_Widget_AppCompat_Toolbar_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_TextAppearance_Widget_AppCompat_Toolbar_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_CompactMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_CompactMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Dialog_FixedSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Dialog_FixedSize; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Dialog_MinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Dialog_MinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_DialogWhenLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_DialogWhenLarge; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_DarkActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_DarkActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_Dialog_FixedSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_Dialog_FixedSize; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_Dialog_MinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_Dialog_MinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Theme_AppCompat_Light_DialogWhenLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Theme_AppCompat_Light_DialogWhenLarge; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_Dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_Dark; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_Dark_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_Dark_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_ThemeOverlay_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_ThemeOverlay_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V11_Theme_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V11_Theme_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V11_Theme_AppCompat_Light_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V11_Theme_AppCompat_Light_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V11_ThemeOverlay_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V11_ThemeOverlay_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V12_Widget_AppCompat_AutoCompleteTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V12_Widget_AppCompat_AutoCompleteTextView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V12_Widget_AppCompat_EditText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V12_Widget_AppCompat_EditText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V14_Widget_Design_AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V14_Widget_Design_AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_Theme_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_Theme_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_Theme_AppCompat_Light_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_Theme_AppCompat_Light_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_ThemeOverlay_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_ThemeOverlay_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V21_Widget_Design_AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V21_Widget_Design_AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V22_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V22_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V22_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V22_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V23_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V23_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V23_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V23_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V26_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V26_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V26_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V26_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V26_Widget_AppCompat_Toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V26_Widget_AppCompat_Toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V26_Widget_Design_AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V26_Widget_Design_AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Theme_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Theme_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Theme_AppCompat_Light_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Theme_AppCompat_Light_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_ThemeOverlay_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_ThemeOverlay_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Widget_AppCompat_AutoCompleteTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Widget_AppCompat_AutoCompleteTextView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Widget_AppCompat_EditText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Widget_AppCompat_EditText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_V7_Widget_AppCompat_Toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_V7_Widget_AppCompat_Toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionBar_Solid = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionBar_Solid; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionBar_TabBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionBar_TabBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionBar_TabText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionBar_TabText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionBar_TabView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionBar_TabView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionButton_CloseMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionButton_CloseMode; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionButton_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionButton_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActionMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActionMode; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ActivityChooserView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ActivityChooserView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_AutoCompleteTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_AutoCompleteTextView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button_Borderless = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button_Borderless; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button_Borderless_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button_Borderless_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button_ButtonBar_AlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button_ButtonBar_AlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Button_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Button_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ButtonBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ButtonBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ButtonBar_AlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ButtonBar_AlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_CompoundButton_CheckBox = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_CompoundButton_CheckBox; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_CompoundButton_RadioButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_CompoundButton_RadioButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_CompoundButton_Switch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_CompoundButton_Switch; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_DrawerArrowToggle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_DrawerArrowToggle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_DrawerArrowToggle_Common = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_DrawerArrowToggle_Common; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_DropDownItem_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_DropDownItem_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_EditText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_EditText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ImageButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ImageButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_Solid = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_Solid; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabText_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabText_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_ActionBar_TabView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_PopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_PopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Light_PopupMenu_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Light_PopupMenu_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ListMenuView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ListMenuView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ListPopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ListPopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ListView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ListView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ListView_DropDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ListView_DropDown; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ListView_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ListView_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_PopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_PopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_PopupMenu_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_PopupMenu_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_PopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_PopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ProgressBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ProgressBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_ProgressBar_Horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_ProgressBar_Horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_RatingBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_RatingBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_RatingBar_Indicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_RatingBar_Indicator; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_RatingBar_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_RatingBar_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_SearchView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_SearchView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_SearchView_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_SearchView_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_SeekBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_SeekBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_SeekBar_Discrete = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_SeekBar_Discrete; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Spinner_Underlined = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Spinner_Underlined; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_TextView_SpinnerItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_TextView_SpinnerItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_AppCompat_Toolbar_Button_Navigation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_AppCompat_Toolbar_Button_Navigation; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_Design_AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_Design_AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Base_Widget_Design_TabLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Base_Widget_Design_TabLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.CardView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.CardView; + global::Xamarin.Forms.Platform.Android.Resource.Style.CardView_Dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.CardView_Dark; + global::Xamarin.Forms.Platform.Android.Resource.Style.CardView_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.CardView_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_ThemeOverlay_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_ThemeOverlay_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_ThemeOverlay_AppCompat_Dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_ThemeOverlay_AppCompat_Dark; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_ThemeOverlay_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_ThemeOverlay_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V11_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V11_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V11_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V11_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V14_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V14_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V14_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V14_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V21_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V21_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V21_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V21_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V25_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V25_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_V25_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_V25_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Platform_Widget_AppCompat_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Platform_Widget_AppCompat_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_DialogWindowTitle_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_DialogWindowTitle_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_ActionBar_TitleItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_ActionBar_TitleItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_DialogTitle_Icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_DialogTitle_Icon; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem_InternalGroup = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem_InternalGroup; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem_Text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_PopupMenuItem_Text; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Icon1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Icon1; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Icon2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Icon2; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Query = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Query; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_Search_DropDown_Text; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlOverlay_Widget_AppCompat_SearchView_MagIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlOverlay_Widget_AppCompat_SearchView_MagIcon; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlUnderlay_Widget_AppCompat_ActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlUnderlay_Widget_AppCompat_ActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.RtlUnderlay_Widget_AppCompat_ActionButton_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.RtlUnderlay_Widget_AppCompat_ActionButton_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Body1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Body1; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Body2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Body2; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Caption = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Caption; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Display1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Display1; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Display2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Display2; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Display3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Display3; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Display4 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Display4; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Headline = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Headline; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Large_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Large_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Light_SearchResult_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Light_SearchResult_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Light_SearchResult_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Light_SearchResult_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Light_Widget_PopupMenu_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Light_Widget_PopupMenu_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Medium = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Medium; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Medium_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Medium_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_SearchResult_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_SearchResult_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_SearchResult_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_SearchResult_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Small_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Small_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Subhead = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Subhead; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Subhead_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Subhead_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Title_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Title_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Tooltip = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Tooltip; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Subtitle_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Subtitle_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Title_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_ActionMode_Title_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_Button_Borderless_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_Button_Borderless_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_Button_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_Button_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_Button_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_Button_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_DropDownItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_DropDownItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Header = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Header; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Large; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_PopupMenu_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_Switch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_Switch; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_AppCompat_Widget_TextView_SpinnerItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_AppCompat_Widget_TextView_SpinnerItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Info; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Info_Media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Info_Media; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Line2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Line2; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Line2_Media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Line2_Media; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Media; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Time; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Time_Media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Time_Media; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Compat_Notification_Title_Media = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Title_Media; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_CollapsingToolbar_Expanded = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_CollapsingToolbar_Expanded; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Counter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Counter; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Counter_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Counter_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Error = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Error; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Hint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Hint; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Snackbar_Message = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Snackbar_Message; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Design_Tab = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Design_Tab; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Widget_AppCompat_ExpandedMenu_Item = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Widget_AppCompat_ExpandedMenu_Item; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Widget_AppCompat_Toolbar_Subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Widget_AppCompat_Toolbar_Subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Style.TextAppearance_Widget_AppCompat_Toolbar_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Widget_AppCompat_Toolbar_Title; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_CompactMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_CompactMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_DarkActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_DarkActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_Dialog_MinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_Dialog_MinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_DialogWhenLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_DialogWhenLarge; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DayNight_NoActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DayNight_NoActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Dialog_MinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Dialog_MinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_DialogWhenLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_DialogWhenLarge; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_DarkActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_DarkActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_Dialog_MinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_Dialog_MinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_DialogWhenLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_DialogWhenLarge; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_Light_NoActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_Light_NoActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_AppCompat_NoActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_AppCompat_NoActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design_BottomSheetDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design_BottomSheetDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design_Light_BottomSheetDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design_Light_BottomSheetDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design_Light_NoActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design_Light_NoActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Theme_Design_NoActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Theme_Design_NoActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_Dark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_Dark; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_Dark_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_Dark_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_Dialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_Dialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_Dialog_Alert = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_Dialog_Alert; + global::Xamarin.Forms.Platform.Android.Resource.Style.ThemeOverlay_AppCompat_Light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.ThemeOverlay_AppCompat_Light; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionBar_Solid = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionBar_Solid; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionBar_TabBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionBar_TabBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionBar_TabText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionBar_TabText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionBar_TabView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionBar_TabView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionButton_CloseMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionButton_CloseMode; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionButton_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionButton_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActionMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActionMode; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ActivityChooserView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ActivityChooserView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_AutoCompleteTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_AutoCompleteTextView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button_Borderless = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button_Borderless; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button_Borderless_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button_Borderless_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button_ButtonBar_AlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button_ButtonBar_AlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button_Colored = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button_Colored; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Button_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Button_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ButtonBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ButtonBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ButtonBar_AlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ButtonBar_AlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_CompoundButton_CheckBox = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_CompoundButton_CheckBox; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_CompoundButton_RadioButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_CompoundButton_RadioButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_CompoundButton_Switch = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_CompoundButton_Switch; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_DrawerArrowToggle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_DrawerArrowToggle; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_DropDownItem_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_DropDownItem_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_EditText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_EditText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ImageButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ImageButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_Solid = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_Solid; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_Solid_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_Solid_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabBar_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabBar_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabText_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabText_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionBar_TabView_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionBar_TabView_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionButton_CloseMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionButton_CloseMode; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionButton_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionButton_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActionMode_Inverse = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActionMode_Inverse; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ActivityChooserView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ActivityChooserView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_AutoCompleteTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_AutoCompleteTextView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_DropDownItem_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_DropDownItem_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ListPopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ListPopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_ListView_DropDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_ListView_DropDown; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_PopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_PopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_PopupMenu_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_PopupMenu_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_SearchView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_SearchView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Light_Spinner_DropDown_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Light_Spinner_DropDown_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ListMenuView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ListMenuView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ListPopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ListPopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ListView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ListView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ListView_DropDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ListView_DropDown; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ListView_Menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ListView_Menu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_PopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_PopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_PopupMenu_Overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_PopupMenu_Overflow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_PopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_PopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ProgressBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ProgressBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_ProgressBar_Horizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_ProgressBar_Horizontal; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_RatingBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_RatingBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_RatingBar_Indicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_RatingBar_Indicator; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_RatingBar_Small = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_RatingBar_Small; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_SearchView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_SearchView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_SearchView_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_SearchView_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_SeekBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_SeekBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_SeekBar_Discrete = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_SeekBar_Discrete; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Spinner_DropDown = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Spinner_DropDown; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Spinner_DropDown_ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Spinner_DropDown_ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Spinner_Underlined = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Spinner_Underlined; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_TextView_SpinnerItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_TextView_SpinnerItem; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_AppCompat_Toolbar_Button_Navigation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_AppCompat_Toolbar_Button_Navigation; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Compat_NotificationActionContainer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionContainer; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Compat_NotificationActionText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionText; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_BottomNavigationView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_BottomNavigationView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_BottomSheet_Modal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_BottomSheet_Modal; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_CollapsingToolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_CollapsingToolbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_CoordinatorLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_CoordinatorLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_FloatingActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_FloatingActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_NavigationView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_NavigationView; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_ScrimInsetsFrameLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_ScrimInsetsFrameLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_Snackbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_Snackbar; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_TabLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_TabLayout; + global::Xamarin.Forms.Platform.Android.Resource.Style.Widget_Design_TextInputLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Design_TextInputLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_background; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_backgroundSplit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_backgroundSplit; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_backgroundStacked = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_backgroundStacked; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetEndWithActions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetEndWithActions; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetLeft; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetRight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_contentInsetStartWithNavigation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_contentInsetStartWithNavigation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_customNavigationLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_customNavigationLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_displayOptions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_displayOptions; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_divider; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_height; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_hideOnContentScroll = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_hideOnContentScroll; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_homeAsUpIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_homeAsUpIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_homeLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_homeLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_icon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_indeterminateProgressStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_indeterminateProgressStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_itemPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_itemPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_logo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_logo; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_navigationMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_navigationMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_popupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_popupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_progressBarPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_progressBarPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_progressBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_progressBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_subtitleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_subtitleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_title; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBar_titleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBar_titleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionBarLayout_android_layout_gravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionBarLayout_android_layout_gravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMenuItemView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMenuItemView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMenuItemView_android_minWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMenuItemView_android_minWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMenuView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMenuView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_background; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_backgroundSplit = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_backgroundSplit; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_closeItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_closeItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_height; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_subtitleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_subtitleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActionMode_titleTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActionMode_titleTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActivityChooserView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActivityChooserView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActivityChooserView_expandActivityOverflowButtonDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActivityChooserView_expandActivityOverflowButtonDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ActivityChooserView_initialActivityCount = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ActivityChooserView_initialActivityCount; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_android_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_android_layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_buttonPanelSideLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_buttonPanelSideLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_listItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_listItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_listLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_listLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_multiChoiceItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_multiChoiceItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_showTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_showTitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AlertDialog_singleChoiceItemLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AlertDialog_singleChoiceItemLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_android_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_android_background; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_android_keyboardNavigationCluster = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_android_keyboardNavigationCluster; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_android_touchscreenBlocksFocus = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_android_touchscreenBlocksFocus; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_expanded = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_expanded; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayoutStates = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayoutStates; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayoutStates_state_collapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayoutStates_state_collapsed; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayoutStates_state_collapsible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayoutStates_state_collapsible; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_Layout_layout_scrollFlags = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_Layout_layout_scrollFlags; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppBarLayout_Layout_layout_scrollInterpolator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppBarLayout_Layout_layout_scrollInterpolator; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatImageView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatImageView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatImageView_android_src = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatImageView_android_src; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatImageView_srcCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatImageView_srcCompat; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatImageView_tint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatImageView_tint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatImageView_tintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatImageView_tintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatSeekBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatSeekBar; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatSeekBar_android_thumb = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatSeekBar_android_thumb; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatSeekBar_tickMark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatSeekBar_tickMark; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatSeekBar_tickMarkTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatSeekBar_tickMarkTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatSeekBar_tickMarkTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatSeekBar_tickMarkTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableBottom; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableLeft; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableRight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_drawableTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_drawableTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextHelper_android_textAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextHelper_android_textAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_android_textAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_android_textAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_autoSizeMaxTextSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_autoSizeMaxTextSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_autoSizeMinTextSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_autoSizeMinTextSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_autoSizePresetSizes = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_autoSizePresetSizes; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_autoSizeStepGranularity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_autoSizeStepGranularity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_autoSizeTextType = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_autoSizeTextType; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_fontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_fontFamily; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTextView_textAllCaps = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTextView_textAllCaps; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarDivider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarDivider; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarItemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarItemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarPopupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarPopupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarSplitStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarSplitStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarTabBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarTabBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarTabStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarTabStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarTabTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarTabTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionBarWidgetTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionBarWidgetTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionDropDownStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionDropDownStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionMenuTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionMenuTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionMenuTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionMenuTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeCloseButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeCloseButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeCloseDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeCloseDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeCopyDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeCopyDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeCutDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeCutDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeFindDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeFindDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModePasteDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModePasteDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModePopupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModePopupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeSelectAllDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeSelectAllDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeShareDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeShareDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeSplitBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeSplitBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionModeWebSearchDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionModeWebSearchDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionOverflowButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionOverflowButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_actionOverflowMenuStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_actionOverflowMenuStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_activityChooserViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_activityChooserViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_alertDialogButtonGroupStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_alertDialogButtonGroupStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_alertDialogCenterButtons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_alertDialogCenterButtons; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_alertDialogStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_alertDialogStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_alertDialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_alertDialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_android_windowAnimationStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_android_windowAnimationStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_android_windowIsFloating = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_android_windowIsFloating; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_autoCompleteTextViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_autoCompleteTextViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_borderlessButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_borderlessButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonBarButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonBarButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonBarNegativeButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonBarNegativeButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonBarNeutralButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonBarNeutralButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonBarPositiveButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonBarPositiveButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_buttonStyleSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_buttonStyleSmall; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_checkboxStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_checkboxStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_checkedTextViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_checkedTextViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorAccent = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorAccent; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorBackgroundFloating = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorBackgroundFloating; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorButtonNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorButtonNormal; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorControlActivated = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorControlActivated; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorControlHighlight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorControlHighlight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorControlNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorControlNormal; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorError = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorError; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorPrimary = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorPrimary; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorPrimaryDark = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorPrimaryDark; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_colorSwitchThumbNormal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_colorSwitchThumbNormal; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_controlBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_controlBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dialogPreferredPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dialogPreferredPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dividerHorizontal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dividerHorizontal; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dividerVertical = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dividerVertical; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dropDownListViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dropDownListViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_dropdownListPreferredItemHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_dropdownListPreferredItemHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_editTextBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_editTextBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_editTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_editTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_editTextStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_editTextStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_homeAsUpIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_homeAsUpIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_imageButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_imageButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listChoiceBackgroundIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listChoiceBackgroundIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listDividerAlertDialog = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listDividerAlertDialog; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listMenuViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listMenuViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPopupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPopupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPreferredItemHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPreferredItemHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPreferredItemHeightLarge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPreferredItemHeightLarge; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPreferredItemHeightSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPreferredItemHeightSmall; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPreferredItemPaddingLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPreferredItemPaddingLeft; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_listPreferredItemPaddingRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_listPreferredItemPaddingRight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_panelBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_panelBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_panelMenuListTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_panelMenuListTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_panelMenuListWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_panelMenuListWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_popupMenuStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_popupMenuStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_popupWindowStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_popupWindowStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_radioButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_radioButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_ratingBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_ratingBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_ratingBarStyleIndicator = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_ratingBarStyleIndicator; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_ratingBarStyleSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_ratingBarStyleSmall; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_searchViewStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_searchViewStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_seekBarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_seekBarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_selectableItemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_selectableItemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_selectableItemBackgroundBorderless = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_selectableItemBackgroundBorderless; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_spinnerDropDownItemStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_spinnerDropDownItemStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_spinnerStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_spinnerStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_switchStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_switchStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceLargePopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceLargePopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceListItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceListItem; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceListItemSecondary = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceListItemSecondary; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceListItemSmall = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceListItemSmall; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearancePopupMenuHeader = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearancePopupMenuHeader; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceSearchResultSubtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceSearchResultSubtitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceSearchResultTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceSearchResultTitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textAppearanceSmallPopupMenu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textAppearanceSmallPopupMenu; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textColorAlertDialogListItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textColorAlertDialogListItem; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_textColorSearchUrl = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_textColorSearchUrl; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_toolbarNavigationButtonStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_toolbarNavigationButtonStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_toolbarStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_toolbarStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_tooltipForegroundColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_tooltipForegroundColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_tooltipFrameBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_tooltipFrameBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowActionBar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowActionBar; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowActionBarOverlay = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowActionBarOverlay; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowActionModeOverlay = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowActionModeOverlay; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowFixedHeightMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowFixedHeightMajor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowFixedHeightMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowFixedHeightMinor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowFixedWidthMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowFixedWidthMajor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowFixedWidthMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowFixedWidthMinor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowMinWidthMajor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowMinWidthMajor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowMinWidthMinor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowMinWidthMinor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.AppCompatTheme_windowNoTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.AppCompatTheme_windowNoTitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView_itemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView_itemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView_itemIconTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView_itemIconTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView_itemTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView_itemTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomNavigationView_menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomNavigationView_menu; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomSheetBehavior_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomSheetBehavior_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomSheetBehavior_Layout_behavior_hideable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomSheetBehavior_Layout_behavior_hideable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomSheetBehavior_Layout_behavior_peekHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomSheetBehavior_Layout_behavior_peekHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ButtonBarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ButtonBarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ButtonBarLayout_allowStacking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ButtonBarLayout_allowStacking; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_android_minHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_android_minHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_android_minWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_android_minWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardBackgroundColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardBackgroundColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardCornerRadius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardCornerRadius; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardElevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardElevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardMaxElevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardMaxElevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardPreventCornerOverlap = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardPreventCornerOverlap; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_cardUseCompatPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_cardUseCompatPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_contentPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_contentPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_contentPaddingBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_contentPaddingBottom; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_contentPaddingLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_contentPaddingLeft; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_contentPaddingRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_contentPaddingRight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CardView_contentPaddingTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CardView_contentPaddingTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_collapsedTitleGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_collapsedTitleGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_contentScrim = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_contentScrim; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMargin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMargin; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginBottom; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleMarginTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_expandedTitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_expandedTitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_scrimAnimationDuration = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_scrimAnimationDuration; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_statusBarScrim = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_statusBarScrim; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_title; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_titleEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_titleEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_toolbarId = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_toolbarId; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_Layout_layout_collapseMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_Layout_layout_collapseMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ColorStateListItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ColorStateListItem; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ColorStateListItem_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ColorStateListItem_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ColorStateListItem_android_alpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ColorStateListItem_android_alpha; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ColorStateListItem_android_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ColorStateListItem_android_color; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CompoundButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CompoundButton; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CompoundButton_android_button = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CompoundButton_android_button; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CompoundButton_buttonTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CompoundButton_buttonTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CompoundButton_buttonTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CompoundButton_buttonTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_keylines = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_keylines; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_statusBarBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_statusBarBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_android_layout_gravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_android_layout_gravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_anchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_anchor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_anchorGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_anchorGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_behavior = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_behavior; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_insetEdge = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_insetEdge; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.CoordinatorLayout_Layout_layout_keyline = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.CoordinatorLayout_Layout_layout_keyline; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DesignTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DesignTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DesignTheme_bottomSheetDialogTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DesignTheme_bottomSheetDialogTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DesignTheme_bottomSheetStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DesignTheme_bottomSheetStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DesignTheme_textColorError = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DesignTheme_textColorError; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_arrowHeadLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_arrowHeadLength; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_arrowShaftLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_arrowShaftLength; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_barLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_barLength; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_color; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_drawableSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_drawableSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_gapBetweenBars = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_gapBetweenBars; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_spinBars = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_spinBars; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.DrawerArrowToggle_thickness = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.DrawerArrowToggle_thickness; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_backgroundTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_backgroundTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_backgroundTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_backgroundTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_borderWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_borderWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_fabSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_fabSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_pressedTranslationZ = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_pressedTranslationZ; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_rippleColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_rippleColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_useCompatPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_useCompatPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_Behavior_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_Behavior_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderAuthority; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderCerts; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchStrategy; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchTimeout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderPackage; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamily_fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderQuery; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_android_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_font; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_android_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_android_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontWeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_font; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.FontFamilyFont_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontWeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ForegroundLinearLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ForegroundLinearLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ForegroundLinearLayout_android_foreground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ForegroundLinearLayout_android_foreground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ForegroundLinearLayout_android_foregroundGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ForegroundLinearLayout_android_foregroundGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ForegroundLinearLayout_foregroundInsidePadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ForegroundLinearLayout_foregroundInsidePadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_android_baselineAligned = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_android_baselineAligned; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_android_baselineAlignedChildIndex = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_android_baselineAlignedChildIndex; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_android_gravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_android_gravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_android_orientation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_android_orientation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_android_weightSum = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_android_weightSum; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_divider; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_dividerPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_dividerPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_measureWithLargestChild = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_measureWithLargestChild; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_showDividers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_showDividers; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_gravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_gravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_height; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_weight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_weight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.LinearLayoutCompat_Layout_android_layout_width; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ListPopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ListPopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ListPopupWindow_android_dropDownHorizontalOffset = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ListPopupWindow_android_dropDownHorizontalOffset; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ListPopupWindow_android_dropDownVerticalOffset = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ListPopupWindow_android_dropDownVerticalOffset; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_checkableBehavior = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_checkableBehavior; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_enabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_enabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_id = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_id; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_menuCategory = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_menuCategory; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_orderInCategory = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_orderInCategory; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuGroup_android_visible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuGroup_android_visible; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_actionLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_actionLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_actionProviderClass = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_actionProviderClass; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_actionViewClass = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_actionViewClass; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_alphabeticModifiers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_alphabeticModifiers; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_alphabeticShortcut = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_alphabeticShortcut; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_checkable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_checkable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_checked = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_checked; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_enabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_enabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_icon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_id = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_id; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_menuCategory = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_menuCategory; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_numericShortcut = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_numericShortcut; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_onClick = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_onClick; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_orderInCategory = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_orderInCategory; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_title; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_titleCondensed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_titleCondensed; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_android_visible = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_android_visible; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_contentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_contentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_iconTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_iconTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_iconTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_iconTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_numericModifiers = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_numericModifiers; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_showAsAction = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_showAsAction; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuItem_tooltipText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuItem_tooltipText; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_headerBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_headerBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_horizontalDivider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_horizontalDivider; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_itemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_itemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_itemIconDisabledAlpha = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_itemIconDisabledAlpha; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_itemTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_itemTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_verticalDivider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_verticalDivider; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_android_windowAnimationStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_android_windowAnimationStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_preserveIconSpacing = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_preserveIconSpacing; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.MenuView_subMenuArrow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.MenuView_subMenuArrow; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_android_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_android_background; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_android_fitsSystemWindows = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_android_fitsSystemWindows; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_android_maxWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_android_maxWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_headerLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_headerLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_itemBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_itemBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_itemIconTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_itemIconTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_itemTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_itemTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_itemTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_itemTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.NavigationView_menu = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.NavigationView_menu; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindow; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindow_android_popupAnimationStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindow_android_popupAnimationStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindow_android_popupBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindow_android_popupBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindow_overlapAnchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindow_overlapAnchor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindowBackgroundState = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindowBackgroundState; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.PopupWindowBackgroundState_state_above_anchor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.PopupWindowBackgroundState_state_above_anchor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecycleListView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecycleListView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecycleListView_paddingBottomNoButtons = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecycleListView_paddingBottomNoButtons; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecycleListView_paddingTopNoTitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecycleListView_paddingTopNoTitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_android_descendantFocusability = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_android_descendantFocusability; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_android_orientation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_android_orientation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_fastScrollEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_fastScrollEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_fastScrollHorizontalThumbDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_fastScrollHorizontalThumbDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_fastScrollHorizontalTrackDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_fastScrollHorizontalTrackDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_fastScrollVerticalThumbDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_fastScrollVerticalThumbDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_fastScrollVerticalTrackDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_fastScrollVerticalTrackDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_layoutManager = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_layoutManager; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_reverseLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_reverseLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_spanCount = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_spanCount; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.RecyclerView_stackFromEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.RecyclerView_stackFromEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ScrimInsetsFrameLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ScrimInsetsFrameLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ScrimInsetsFrameLayout_insetForeground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ScrimInsetsFrameLayout_insetForeground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ScrollingViewBehavior_Layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ScrollingViewBehavior_Layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ScrollingViewBehavior_Layout_behavior_overlapTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ScrollingViewBehavior_Layout_behavior_overlapTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_android_focusable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_android_focusable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_android_imeOptions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_android_imeOptions; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_android_inputType = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_android_inputType; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_android_maxWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_android_maxWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_closeIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_closeIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_commitIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_commitIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_defaultQueryHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_defaultQueryHint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_goIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_goIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_iconifiedByDefault = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_iconifiedByDefault; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_queryBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_queryBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_queryHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_queryHint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_searchHintIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_searchHintIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_searchIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_searchIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_submitBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_submitBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_suggestionRowLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_suggestionRowLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SearchView_voiceIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SearchView_voiceIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SnackbarLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SnackbarLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SnackbarLayout_android_maxWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SnackbarLayout_android_maxWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SnackbarLayout_elevation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SnackbarLayout_elevation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SnackbarLayout_maxActionInlineWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SnackbarLayout_maxActionInlineWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner_android_dropDownWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner_android_dropDownWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner_android_entries = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner_android_entries; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner_android_popupBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner_android_popupBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner_android_prompt = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner_android_prompt; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Spinner_popupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Spinner_popupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_android_textOff = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_android_textOff; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_android_textOn = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_android_textOn; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_android_thumb = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_android_thumb; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_showText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_showText; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_splitTrack = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_splitTrack; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_switchMinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_switchMinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_switchPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_switchPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_switchTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_switchTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_thumbTextPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_thumbTextPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_thumbTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_thumbTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_thumbTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_thumbTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_track = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_track; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_trackTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_trackTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.SwitchCompat_trackTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.SwitchCompat_trackTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabItem = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabItem; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabItem_android_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabItem_android_icon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabItem_android_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabItem_android_layout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabItem_android_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabItem_android_text; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabBackground = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabBackground; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabContentStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabContentStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabIndicatorColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabIndicatorColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabIndicatorHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabIndicatorHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabMaxWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabMaxWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabMinWidth = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabMinWidth; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabPadding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabPadding; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabPaddingBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabPaddingBottom; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabPaddingEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabPaddingEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabPaddingStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabPaddingStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabPaddingTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabPaddingTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabSelectedTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabSelectedTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TabLayout_tabTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TabLayout_tabTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_fontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_fontFamily; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_shadowColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_shadowColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_shadowDx = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_shadowDx; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_shadowDy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_shadowDy; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_shadowRadius = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_shadowRadius; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_textColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_textColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_textColorHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_textColorHint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_textColorLink = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_textColorLink; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_textSize = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_textSize; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_textStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_textStyle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_android_typeface = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_android_typeface; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_fontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_fontFamily; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextAppearance_textAllCaps = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextAppearance_textAllCaps; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_android_hint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_android_hint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_android_textColorHint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_android_textColorHint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_counterEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_counterEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_counterMaxLength = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_counterMaxLength; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_counterOverflowTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_counterOverflowTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_counterTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_counterTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_errorEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_errorEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_errorTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_errorTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_hintAnimationEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_hintAnimationEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_hintEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_hintEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_hintTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_hintTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_passwordToggleContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_passwordToggleContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_passwordToggleDrawable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_passwordToggleDrawable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_passwordToggleEnabled = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_passwordToggleEnabled; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_passwordToggleTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_passwordToggleTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.TextInputLayout_passwordToggleTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.TextInputLayout_passwordToggleTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_android_gravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_android_gravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_android_minHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_android_minHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_buttonGravity = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_buttonGravity; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_collapseContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_collapseContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_collapseIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_collapseIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetEndWithActions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetEndWithActions; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetLeft = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetLeft; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetRight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetRight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_contentInsetStartWithNavigation = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_contentInsetStartWithNavigation; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_logo = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_logo; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_logoDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_logoDescription; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_maxButtonHeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_maxButtonHeight; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_navigationContentDescription = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_navigationContentDescription; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_navigationIcon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_navigationIcon; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_popupTheme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_popupTheme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_subtitle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_subtitle; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_subtitleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_subtitleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_subtitleTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_subtitleTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_title; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMargin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMargin; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMarginBottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMarginBottom; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMarginEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMarginEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMarginStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMarginStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMarginTop = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMarginTop; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleMargins = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleMargins; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleTextAppearance = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleTextAppearance; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.Toolbar_titleTextColor = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.Toolbar_titleTextColor; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View_android_focusable = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View_android_focusable; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View_android_theme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View_android_theme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View_paddingEnd = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View_paddingEnd; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View_paddingStart = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View_paddingStart; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.View_theme = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.View_theme; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewBackgroundHelper = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewBackgroundHelper; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewBackgroundHelper_android_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewBackgroundHelper_android_background; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewBackgroundHelper_backgroundTint = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewBackgroundHelper_backgroundTint; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewBackgroundHelper_backgroundTintMode = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewBackgroundHelper_backgroundTintMode; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewStubCompat = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewStubCompat; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewStubCompat_android_id = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewStubCompat_android_id; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewStubCompat_android_inflatedId = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewStubCompat_android_inflatedId; + global::Xamarin.Forms.Platform.Android.Resource.Styleable.ViewStubCompat_android_layout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.ViewStubCompat_android_layout; + } + + public partial class Animation + { + + // aapt resource value: 0x7f050000 + public const int abc_fade_in = 2131034112; + + // aapt resource value: 0x7f050001 + public const int abc_fade_out = 2131034113; + + // aapt resource value: 0x7f050002 + public const int abc_grow_fade_in_from_bottom = 2131034114; + + // aapt resource value: 0x7f050003 + public const int abc_popup_enter = 2131034115; + + // aapt resource value: 0x7f050004 + public const int abc_popup_exit = 2131034116; + + // aapt resource value: 0x7f050005 + public const int abc_shrink_fade_out_from_bottom = 2131034117; + + // aapt resource value: 0x7f050006 + public const int abc_slide_in_bottom = 2131034118; + + // aapt resource value: 0x7f050007 + public const int abc_slide_in_top = 2131034119; + + // aapt resource value: 0x7f050008 + public const int abc_slide_out_bottom = 2131034120; + + // aapt resource value: 0x7f050009 + public const int abc_slide_out_top = 2131034121; + + // aapt resource value: 0x7f05000a + public const int design_bottom_sheet_slide_in = 2131034122; + + // aapt resource value: 0x7f05000b + public const int design_bottom_sheet_slide_out = 2131034123; + + // aapt resource value: 0x7f05000c + public const int design_snackbar_in = 2131034124; + + // aapt resource value: 0x7f05000d + public const int design_snackbar_out = 2131034125; + + // aapt resource value: 0x7f05000e + public const int EnterFromLeft = 2131034126; + + // aapt resource value: 0x7f05000f + public const int EnterFromRight = 2131034127; + + // aapt resource value: 0x7f050010 + public const int ExitToLeft = 2131034128; + + // aapt resource value: 0x7f050011 + public const int ExitToRight = 2131034129; + + // aapt resource value: 0x7f050012 + public const int tooltip_enter = 2131034130; + + // aapt resource value: 0x7f050013 + public const int tooltip_exit = 2131034131; + + static Animation() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Animation() + { + } + } + + public partial class Animator + { + + // aapt resource value: 0x7f060000 + public const int design_appbar_state_list_animator = 2131099648; + + static Animator() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Animator() + { + } + } + + public partial class Attribute + { + + // aapt resource value: 0x7f01006b + public const int actionBarDivider = 2130772075; + + // aapt resource value: 0x7f01006c + public const int actionBarItemBackground = 2130772076; + + // aapt resource value: 0x7f010065 + public const int actionBarPopupTheme = 2130772069; + + // aapt resource value: 0x7f01006a + public const int actionBarSize = 2130772074; + + // aapt resource value: 0x7f010067 + public const int actionBarSplitStyle = 2130772071; + + // aapt resource value: 0x7f010066 + public const int actionBarStyle = 2130772070; + + // aapt resource value: 0x7f010061 + public const int actionBarTabBarStyle = 2130772065; + + // aapt resource value: 0x7f010060 + public const int actionBarTabStyle = 2130772064; + + // aapt resource value: 0x7f010062 + public const int actionBarTabTextStyle = 2130772066; + + // aapt resource value: 0x7f010068 + public const int actionBarTheme = 2130772072; + + // aapt resource value: 0x7f010069 + public const int actionBarWidgetTheme = 2130772073; + + // aapt resource value: 0x7f010086 + public const int actionButtonStyle = 2130772102; + + // aapt resource value: 0x7f010082 + public const int actionDropDownStyle = 2130772098; + + // aapt resource value: 0x7f0100dd + public const int actionLayout = 2130772189; + + // aapt resource value: 0x7f01006d + public const int actionMenuTextAppearance = 2130772077; + + // aapt resource value: 0x7f01006e + public const int actionMenuTextColor = 2130772078; + + // aapt resource value: 0x7f010071 + public const int actionModeBackground = 2130772081; + + // aapt resource value: 0x7f010070 + public const int actionModeCloseButtonStyle = 2130772080; + + // aapt resource value: 0x7f010073 + public const int actionModeCloseDrawable = 2130772083; + + // aapt resource value: 0x7f010075 + public const int actionModeCopyDrawable = 2130772085; + + // aapt resource value: 0x7f010074 + public const int actionModeCutDrawable = 2130772084; + + // aapt resource value: 0x7f010079 + public const int actionModeFindDrawable = 2130772089; + + // aapt resource value: 0x7f010076 + public const int actionModePasteDrawable = 2130772086; + + // aapt resource value: 0x7f01007b + public const int actionModePopupWindowStyle = 2130772091; + + // aapt resource value: 0x7f010077 + public const int actionModeSelectAllDrawable = 2130772087; + + // aapt resource value: 0x7f010078 + public const int actionModeShareDrawable = 2130772088; + + // aapt resource value: 0x7f010072 + public const int actionModeSplitBackground = 2130772082; + + // aapt resource value: 0x7f01006f + public const int actionModeStyle = 2130772079; + + // aapt resource value: 0x7f01007a + public const int actionModeWebSearchDrawable = 2130772090; + + // aapt resource value: 0x7f010063 + public const int actionOverflowButtonStyle = 2130772067; + + // aapt resource value: 0x7f010064 + public const int actionOverflowMenuStyle = 2130772068; + + // aapt resource value: 0x7f0100df + public const int actionProviderClass = 2130772191; + + // aapt resource value: 0x7f0100de + public const int actionViewClass = 2130772190; + + // aapt resource value: 0x7f01008e + public const int activityChooserViewStyle = 2130772110; + + // aapt resource value: 0x7f0100b3 + public const int alertDialogButtonGroupStyle = 2130772147; + + // aapt resource value: 0x7f0100b4 + public const int alertDialogCenterButtons = 2130772148; + + // aapt resource value: 0x7f0100b2 + public const int alertDialogStyle = 2130772146; + + // aapt resource value: 0x7f0100b5 + public const int alertDialogTheme = 2130772149; + + // aapt resource value: 0x7f0100cb + public const int allowStacking = 2130772171; + + // aapt resource value: 0x7f0100cc + public const int alpha = 2130772172; + + // aapt resource value: 0x7f0100da + public const int alphabeticModifiers = 2130772186; + + // aapt resource value: 0x7f0100d3 + public const int arrowHeadLength = 2130772179; + + // aapt resource value: 0x7f0100d4 + public const int arrowShaftLength = 2130772180; + + // aapt resource value: 0x7f0100ba + public const int autoCompleteTextViewStyle = 2130772154; + + // aapt resource value: 0x7f010054 + public const int autoSizeMaxTextSize = 2130772052; + + // aapt resource value: 0x7f010053 + public const int autoSizeMinTextSize = 2130772051; + + // aapt resource value: 0x7f010052 + public const int autoSizePresetSizes = 2130772050; + + // aapt resource value: 0x7f010051 + public const int autoSizeStepGranularity = 2130772049; + + // aapt resource value: 0x7f010050 + public const int autoSizeTextType = 2130772048; + + // aapt resource value: 0x7f01002e + public const int background = 2130772014; + + // aapt resource value: 0x7f010030 + public const int backgroundSplit = 2130772016; + + // aapt resource value: 0x7f01002f + public const int backgroundStacked = 2130772015; + + // aapt resource value: 0x7f010116 + public const int backgroundTint = 2130772246; + + // aapt resource value: 0x7f010117 + public const int backgroundTintMode = 2130772247; + + // aapt resource value: 0x7f0100d5 + public const int barLength = 2130772181; + + // aapt resource value: 0x7f010141 + public const int behavior_autoHide = 2130772289; + + // aapt resource value: 0x7f01011e + public const int behavior_hideable = 2130772254; + + // aapt resource value: 0x7f01014a + public const int behavior_overlapTop = 2130772298; + + // aapt resource value: 0x7f01011d + public const int behavior_peekHeight = 2130772253; + + // aapt resource value: 0x7f01011f + public const int behavior_skipCollapsed = 2130772255; + + // aapt resource value: 0x7f01013f + public const int borderWidth = 2130772287; + + // aapt resource value: 0x7f01008b + public const int borderlessButtonStyle = 2130772107; + + // aapt resource value: 0x7f010139 + public const int bottomSheetDialogTheme = 2130772281; + + // aapt resource value: 0x7f01013a + public const int bottomSheetStyle = 2130772282; + + // aapt resource value: 0x7f010088 + public const int buttonBarButtonStyle = 2130772104; + + // aapt resource value: 0x7f0100b8 + public const int buttonBarNegativeButtonStyle = 2130772152; + + // aapt resource value: 0x7f0100b9 + public const int buttonBarNeutralButtonStyle = 2130772153; + + // aapt resource value: 0x7f0100b7 + public const int buttonBarPositiveButtonStyle = 2130772151; + + // aapt resource value: 0x7f010087 + public const int buttonBarStyle = 2130772103; + + // aapt resource value: 0x7f01010b + public const int buttonGravity = 2130772235; + + // aapt resource value: 0x7f010043 + public const int buttonPanelSideLayout = 2130772035; + + // aapt resource value: 0x7f0100bb + public const int buttonStyle = 2130772155; + + // aapt resource value: 0x7f0100bc + public const int buttonStyleSmall = 2130772156; + + // aapt resource value: 0x7f0100cd + public const int buttonTint = 2130772173; + + // aapt resource value: 0x7f0100ce + public const int buttonTintMode = 2130772174; + + // aapt resource value: 0x7f010017 + public const int cardBackgroundColor = 2130771991; + + // aapt resource value: 0x7f010018 + public const int cardCornerRadius = 2130771992; + + // aapt resource value: 0x7f010019 + public const int cardElevation = 2130771993; + + // aapt resource value: 0x7f01001a + public const int cardMaxElevation = 2130771994; + + // aapt resource value: 0x7f01001c + public const int cardPreventCornerOverlap = 2130771996; + + // aapt resource value: 0x7f01001b + public const int cardUseCompatPadding = 2130771995; + + // aapt resource value: 0x7f0100bd + public const int checkboxStyle = 2130772157; + + // aapt resource value: 0x7f0100be + public const int checkedTextViewStyle = 2130772158; + + // aapt resource value: 0x7f0100ee + public const int closeIcon = 2130772206; + + // aapt resource value: 0x7f010040 + public const int closeItemLayout = 2130772032; + + // aapt resource value: 0x7f01010d + public const int collapseContentDescription = 2130772237; + + // aapt resource value: 0x7f01010c + public const int collapseIcon = 2130772236; + + // aapt resource value: 0x7f01012c + public const int collapsedTitleGravity = 2130772268; + + // aapt resource value: 0x7f010126 + public const int collapsedTitleTextAppearance = 2130772262; + + // aapt resource value: 0x7f0100cf + public const int color = 2130772175; + + // aapt resource value: 0x7f0100aa + public const int colorAccent = 2130772138; + + // aapt resource value: 0x7f0100b1 + public const int colorBackgroundFloating = 2130772145; + + // aapt resource value: 0x7f0100ae + public const int colorButtonNormal = 2130772142; + + // aapt resource value: 0x7f0100ac + public const int colorControlActivated = 2130772140; + + // aapt resource value: 0x7f0100ad + public const int colorControlHighlight = 2130772141; + + // aapt resource value: 0x7f0100ab + public const int colorControlNormal = 2130772139; + + // aapt resource value: 0x7f0100ca + public const int colorError = 2130772170; + + // aapt resource value: 0x7f0100a8 + public const int colorPrimary = 2130772136; + + // aapt resource value: 0x7f0100a9 + public const int colorPrimaryDark = 2130772137; + + // aapt resource value: 0x7f0100af + public const int colorSwitchThumbNormal = 2130772143; + + // aapt resource value: 0x7f0100f3 + public const int commitIcon = 2130772211; + + // aapt resource value: 0x7f0100e0 + public const int contentDescription = 2130772192; + + // aapt resource value: 0x7f010039 + public const int contentInsetEnd = 2130772025; + + // aapt resource value: 0x7f01003d + public const int contentInsetEndWithActions = 2130772029; + + // aapt resource value: 0x7f01003a + public const int contentInsetLeft = 2130772026; + + // aapt resource value: 0x7f01003b + public const int contentInsetRight = 2130772027; + + // aapt resource value: 0x7f010038 + public const int contentInsetStart = 2130772024; + + // aapt resource value: 0x7f01003c + public const int contentInsetStartWithNavigation = 2130772028; + + // aapt resource value: 0x7f01001d + public const int contentPadding = 2130771997; + + // aapt resource value: 0x7f010021 + public const int contentPaddingBottom = 2130772001; + + // aapt resource value: 0x7f01001e + public const int contentPaddingLeft = 2130771998; + + // aapt resource value: 0x7f01001f + public const int contentPaddingRight = 2130771999; + + // aapt resource value: 0x7f010020 + public const int contentPaddingTop = 2130772000; + + // aapt resource value: 0x7f010127 + public const int contentScrim = 2130772263; + + // aapt resource value: 0x7f0100b0 + public const int controlBackground = 2130772144; + + // aapt resource value: 0x7f010160 + public const int counterEnabled = 2130772320; + + // aapt resource value: 0x7f010161 + public const int counterMaxLength = 2130772321; + + // aapt resource value: 0x7f010163 + public const int counterOverflowTextAppearance = 2130772323; + + // aapt resource value: 0x7f010162 + public const int counterTextAppearance = 2130772322; + + // aapt resource value: 0x7f010031 + public const int customNavigationLayout = 2130772017; + + // aapt resource value: 0x7f0100ed + public const int defaultQueryHint = 2130772205; + + // aapt resource value: 0x7f010080 + public const int dialogPreferredPadding = 2130772096; + + // aapt resource value: 0x7f01007f + public const int dialogTheme = 2130772095; + + // aapt resource value: 0x7f010027 + public const int displayOptions = 2130772007; + + // aapt resource value: 0x7f01002d + public const int divider = 2130772013; + + // aapt resource value: 0x7f01008d + public const int dividerHorizontal = 2130772109; + + // aapt resource value: 0x7f0100d9 + public const int dividerPadding = 2130772185; + + // aapt resource value: 0x7f01008c + public const int dividerVertical = 2130772108; + + // aapt resource value: 0x7f0100d1 + public const int drawableSize = 2130772177; + + // aapt resource value: 0x7f010022 + public const int drawerArrowStyle = 2130772002; + + // aapt resource value: 0x7f01009f + public const int dropDownListViewStyle = 2130772127; + + // aapt resource value: 0x7f010083 + public const int dropdownListPreferredItemHeight = 2130772099; + + // aapt resource value: 0x7f010094 + public const int editTextBackground = 2130772116; + + // aapt resource value: 0x7f010093 + public const int editTextColor = 2130772115; + + // aapt resource value: 0x7f0100bf + public const int editTextStyle = 2130772159; + + // aapt resource value: 0x7f01003e + public const int elevation = 2130772030; + + // aapt resource value: 0x7f01015e + public const int errorEnabled = 2130772318; + + // aapt resource value: 0x7f01015f + public const int errorTextAppearance = 2130772319; + + // aapt resource value: 0x7f010042 + public const int expandActivityOverflowButtonDrawable = 2130772034; + + // aapt resource value: 0x7f010118 + public const int expanded = 2130772248; + + // aapt resource value: 0x7f01012d + public const int expandedTitleGravity = 2130772269; + + // aapt resource value: 0x7f010120 + public const int expandedTitleMargin = 2130772256; + + // aapt resource value: 0x7f010124 + public const int expandedTitleMarginBottom = 2130772260; + + // aapt resource value: 0x7f010123 + public const int expandedTitleMarginEnd = 2130772259; + + // aapt resource value: 0x7f010121 + public const int expandedTitleMarginStart = 2130772257; + + // aapt resource value: 0x7f010122 + public const int expandedTitleMarginTop = 2130772258; + + // aapt resource value: 0x7f010125 + public const int expandedTitleTextAppearance = 2130772261; + + // aapt resource value: 0x7f010015 + public const int externalRouteEnabledDrawable = 2130771989; + + // aapt resource value: 0x7f01013d + public const int fabSize = 2130772285; + + // aapt resource value: 0x7f010004 + public const int fastScrollEnabled = 2130771972; + + // aapt resource value: 0x7f010007 + public const int fastScrollHorizontalThumbDrawable = 2130771975; + + // aapt resource value: 0x7f010008 + public const int fastScrollHorizontalTrackDrawable = 2130771976; + + // aapt resource value: 0x7f010005 + public const int fastScrollVerticalThumbDrawable = 2130771973; + + // aapt resource value: 0x7f010006 + public const int fastScrollVerticalTrackDrawable = 2130771974; + + // aapt resource value: 0x7f010171 + public const int font = 2130772337; + + // aapt resource value: 0x7f010055 + public const int fontFamily = 2130772053; + + // aapt resource value: 0x7f01016a + public const int fontProviderAuthority = 2130772330; + + // aapt resource value: 0x7f01016d + public const int fontProviderCerts = 2130772333; + + // aapt resource value: 0x7f01016e + public const int fontProviderFetchStrategy = 2130772334; + + // aapt resource value: 0x7f01016f + public const int fontProviderFetchTimeout = 2130772335; + + // aapt resource value: 0x7f01016b + public const int fontProviderPackage = 2130772331; + + // aapt resource value: 0x7f01016c + public const int fontProviderQuery = 2130772332; + + // aapt resource value: 0x7f010170 + public const int fontStyle = 2130772336; + + // aapt resource value: 0x7f010172 + public const int fontWeight = 2130772338; + + // aapt resource value: 0x7f010142 + public const int foregroundInsidePadding = 2130772290; + + // aapt resource value: 0x7f0100d2 + public const int gapBetweenBars = 2130772178; + + // aapt resource value: 0x7f0100ef + public const int goIcon = 2130772207; + + // aapt resource value: 0x7f010148 + public const int headerLayout = 2130772296; + + // aapt resource value: 0x7f010023 + public const int height = 2130772003; + + // aapt resource value: 0x7f010037 + public const int hideOnContentScroll = 2130772023; + + // aapt resource value: 0x7f010164 + public const int hintAnimationEnabled = 2130772324; + + // aapt resource value: 0x7f01015d + public const int hintEnabled = 2130772317; + + // aapt resource value: 0x7f01015c + public const int hintTextAppearance = 2130772316; + + // aapt resource value: 0x7f010085 + public const int homeAsUpIndicator = 2130772101; + + // aapt resource value: 0x7f010032 + public const int homeLayout = 2130772018; + + // aapt resource value: 0x7f01002b + public const int icon = 2130772011; + + // aapt resource value: 0x7f0100e2 + public const int iconTint = 2130772194; + + // aapt resource value: 0x7f0100e3 + public const int iconTintMode = 2130772195; + + // aapt resource value: 0x7f0100eb + public const int iconifiedByDefault = 2130772203; + + // aapt resource value: 0x7f010095 + public const int imageButtonStyle = 2130772117; + + // aapt resource value: 0x7f010034 + public const int indeterminateProgressStyle = 2130772020; + + // aapt resource value: 0x7f010041 + public const int initialActivityCount = 2130772033; + + // aapt resource value: 0x7f010149 + public const int insetForeground = 2130772297; + + // aapt resource value: 0x7f010024 + public const int isLightTheme = 2130772004; + + // aapt resource value: 0x7f010146 + public const int itemBackground = 2130772294; + + // aapt resource value: 0x7f010144 + public const int itemIconTint = 2130772292; + + // aapt resource value: 0x7f010036 + public const int itemPadding = 2130772022; + + // aapt resource value: 0x7f010147 + public const int itemTextAppearance = 2130772295; + + // aapt resource value: 0x7f010145 + public const int itemTextColor = 2130772293; + + // aapt resource value: 0x7f010131 + public const int keylines = 2130772273; + + // aapt resource value: 0x7f0100ea + public const int layout = 2130772202; + + // aapt resource value: 0x7f010000 + public const int layoutManager = 2130771968; + + // aapt resource value: 0x7f010134 + public const int layout_anchor = 2130772276; + + // aapt resource value: 0x7f010136 + public const int layout_anchorGravity = 2130772278; + + // aapt resource value: 0x7f010133 + public const int layout_behavior = 2130772275; + + // aapt resource value: 0x7f01012f + public const int layout_collapseMode = 2130772271; + + // aapt resource value: 0x7f010130 + public const int layout_collapseParallaxMultiplier = 2130772272; + + // aapt resource value: 0x7f010138 + public const int layout_dodgeInsetEdges = 2130772280; + + // aapt resource value: 0x7f010137 + public const int layout_insetEdge = 2130772279; + + // aapt resource value: 0x7f010135 + public const int layout_keyline = 2130772277; + + // aapt resource value: 0x7f01011b + public const int layout_scrollFlags = 2130772251; + + // aapt resource value: 0x7f01011c + public const int layout_scrollInterpolator = 2130772252; + + // aapt resource value: 0x7f0100a7 + public const int listChoiceBackgroundIndicator = 2130772135; + + // aapt resource value: 0x7f010081 + public const int listDividerAlertDialog = 2130772097; + + // aapt resource value: 0x7f010047 + public const int listItemLayout = 2130772039; + + // aapt resource value: 0x7f010044 + public const int listLayout = 2130772036; + + // aapt resource value: 0x7f0100c7 + public const int listMenuViewStyle = 2130772167; + + // aapt resource value: 0x7f0100a0 + public const int listPopupWindowStyle = 2130772128; + + // aapt resource value: 0x7f01009a + public const int listPreferredItemHeight = 2130772122; + + // aapt resource value: 0x7f01009c + public const int listPreferredItemHeightLarge = 2130772124; + + // aapt resource value: 0x7f01009b + public const int listPreferredItemHeightSmall = 2130772123; + + // aapt resource value: 0x7f01009d + public const int listPreferredItemPaddingLeft = 2130772125; + + // aapt resource value: 0x7f01009e + public const int listPreferredItemPaddingRight = 2130772126; + + // aapt resource value: 0x7f01002c + public const int logo = 2130772012; + + // aapt resource value: 0x7f010110 + public const int logoDescription = 2130772240; + + // aapt resource value: 0x7f01014b + public const int maxActionInlineWidth = 2130772299; + + // aapt resource value: 0x7f01010a + public const int maxButtonHeight = 2130772234; + + // aapt resource value: 0x7f0100d7 + public const int measureWithLargestChild = 2130772183; + + // aapt resource value: 0x7f010009 + public const int mediaRouteAudioTrackDrawable = 2130771977; + + // aapt resource value: 0x7f01000a + public const int mediaRouteButtonStyle = 2130771978; + + // aapt resource value: 0x7f010016 + public const int mediaRouteButtonTint = 2130771990; + + // aapt resource value: 0x7f01000b + public const int mediaRouteCloseDrawable = 2130771979; + + // aapt resource value: 0x7f01000c + public const int mediaRouteControlPanelThemeOverlay = 2130771980; + + // aapt resource value: 0x7f01000d + public const int mediaRouteDefaultIconDrawable = 2130771981; + + // aapt resource value: 0x7f01000e + public const int mediaRoutePauseDrawable = 2130771982; + + // aapt resource value: 0x7f01000f + public const int mediaRoutePlayDrawable = 2130771983; + + // aapt resource value: 0x7f010010 + public const int mediaRouteSpeakerGroupIconDrawable = 2130771984; + + // aapt resource value: 0x7f010011 + public const int mediaRouteSpeakerIconDrawable = 2130771985; + + // aapt resource value: 0x7f010012 + public const int mediaRouteStopDrawable = 2130771986; + + // aapt resource value: 0x7f010013 + public const int mediaRouteTheme = 2130771987; + + // aapt resource value: 0x7f010014 + public const int mediaRouteTvIconDrawable = 2130771988; + + // aapt resource value: 0x7f010143 + public const int menu = 2130772291; + + // aapt resource value: 0x7f010045 + public const int multiChoiceItemLayout = 2130772037; + + // aapt resource value: 0x7f01010f + public const int navigationContentDescription = 2130772239; + + // aapt resource value: 0x7f01010e + public const int navigationIcon = 2130772238; + + // aapt resource value: 0x7f010026 + public const int navigationMode = 2130772006; + + // aapt resource value: 0x7f0100db + public const int numericModifiers = 2130772187; + + // aapt resource value: 0x7f0100e6 + public const int overlapAnchor = 2130772198; + + // aapt resource value: 0x7f0100e8 + public const int paddingBottomNoButtons = 2130772200; + + // aapt resource value: 0x7f010114 + public const int paddingEnd = 2130772244; + + // aapt resource value: 0x7f010113 + public const int paddingStart = 2130772243; + + // aapt resource value: 0x7f0100e9 + public const int paddingTopNoTitle = 2130772201; + + // aapt resource value: 0x7f0100a4 + public const int panelBackground = 2130772132; + + // aapt resource value: 0x7f0100a6 + public const int panelMenuListTheme = 2130772134; + + // aapt resource value: 0x7f0100a5 + public const int panelMenuListWidth = 2130772133; + + // aapt resource value: 0x7f010167 + public const int passwordToggleContentDescription = 2130772327; + + // aapt resource value: 0x7f010166 + public const int passwordToggleDrawable = 2130772326; + + // aapt resource value: 0x7f010165 + public const int passwordToggleEnabled = 2130772325; + + // aapt resource value: 0x7f010168 + public const int passwordToggleTint = 2130772328; + + // aapt resource value: 0x7f010169 + public const int passwordToggleTintMode = 2130772329; + + // aapt resource value: 0x7f010091 + public const int popupMenuStyle = 2130772113; + + // aapt resource value: 0x7f01003f + public const int popupTheme = 2130772031; + + // aapt resource value: 0x7f010092 + public const int popupWindowStyle = 2130772114; + + // aapt resource value: 0x7f0100e4 + public const int preserveIconSpacing = 2130772196; + + // aapt resource value: 0x7f01013e + public const int pressedTranslationZ = 2130772286; + + // aapt resource value: 0x7f010035 + public const int progressBarPadding = 2130772021; + + // aapt resource value: 0x7f010033 + public const int progressBarStyle = 2130772019; + + // aapt resource value: 0x7f0100f5 + public const int queryBackground = 2130772213; + + // aapt resource value: 0x7f0100ec + public const int queryHint = 2130772204; + + // aapt resource value: 0x7f0100c0 + public const int radioButtonStyle = 2130772160; + + // aapt resource value: 0x7f0100c1 + public const int ratingBarStyle = 2130772161; + + // aapt resource value: 0x7f0100c2 + public const int ratingBarStyleIndicator = 2130772162; + + // aapt resource value: 0x7f0100c3 + public const int ratingBarStyleSmall = 2130772163; + + // aapt resource value: 0x7f010002 + public const int reverseLayout = 2130771970; + + // aapt resource value: 0x7f01013c + public const int rippleColor = 2130772284; + + // aapt resource value: 0x7f01012b + public const int scrimAnimationDuration = 2130772267; + + // aapt resource value: 0x7f01012a + public const int scrimVisibleHeightTrigger = 2130772266; + + // aapt resource value: 0x7f0100f1 + public const int searchHintIcon = 2130772209; + + // aapt resource value: 0x7f0100f0 + public const int searchIcon = 2130772208; + + // aapt resource value: 0x7f010099 + public const int searchViewStyle = 2130772121; + + // aapt resource value: 0x7f0100c4 + public const int seekBarStyle = 2130772164; + + // aapt resource value: 0x7f010089 + public const int selectableItemBackground = 2130772105; + + // aapt resource value: 0x7f01008a + public const int selectableItemBackgroundBorderless = 2130772106; + + // aapt resource value: 0x7f0100dc + public const int showAsAction = 2130772188; + + // aapt resource value: 0x7f0100d8 + public const int showDividers = 2130772184; + + // aapt resource value: 0x7f010101 + public const int showText = 2130772225; + + // aapt resource value: 0x7f010048 + public const int showTitle = 2130772040; + + // aapt resource value: 0x7f010046 + public const int singleChoiceItemLayout = 2130772038; + + // aapt resource value: 0x7f010001 + public const int spanCount = 2130771969; + + // aapt resource value: 0x7f0100d0 + public const int spinBars = 2130772176; + + // aapt resource value: 0x7f010084 + public const int spinnerDropDownItemStyle = 2130772100; + + // aapt resource value: 0x7f0100c5 + public const int spinnerStyle = 2130772165; + + // aapt resource value: 0x7f010100 + public const int splitTrack = 2130772224; + + // aapt resource value: 0x7f010049 + public const int srcCompat = 2130772041; + + // aapt resource value: 0x7f010003 + public const int stackFromEnd = 2130771971; + + // aapt resource value: 0x7f0100e7 + public const int state_above_anchor = 2130772199; + + // aapt resource value: 0x7f010119 + public const int state_collapsed = 2130772249; + + // aapt resource value: 0x7f01011a + public const int state_collapsible = 2130772250; + + // aapt resource value: 0x7f010132 + public const int statusBarBackground = 2130772274; + + // aapt resource value: 0x7f010128 + public const int statusBarScrim = 2130772264; + + // aapt resource value: 0x7f0100e5 + public const int subMenuArrow = 2130772197; + + // aapt resource value: 0x7f0100f6 + public const int submitBackground = 2130772214; + + // aapt resource value: 0x7f010028 + public const int subtitle = 2130772008; + + // aapt resource value: 0x7f010103 + public const int subtitleTextAppearance = 2130772227; + + // aapt resource value: 0x7f010112 + public const int subtitleTextColor = 2130772242; + + // aapt resource value: 0x7f01002a + public const int subtitleTextStyle = 2130772010; + + // aapt resource value: 0x7f0100f4 + public const int suggestionRowLayout = 2130772212; + + // aapt resource value: 0x7f0100fe + public const int switchMinWidth = 2130772222; + + // aapt resource value: 0x7f0100ff + public const int switchPadding = 2130772223; + + // aapt resource value: 0x7f0100c6 + public const int switchStyle = 2130772166; + + // aapt resource value: 0x7f0100fd + public const int switchTextAppearance = 2130772221; + + // aapt resource value: 0x7f01014f + public const int tabBackground = 2130772303; + + // aapt resource value: 0x7f01014e + public const int tabContentStart = 2130772302; + + // aapt resource value: 0x7f010151 + public const int tabGravity = 2130772305; + + // aapt resource value: 0x7f01014c + public const int tabIndicatorColor = 2130772300; + + // aapt resource value: 0x7f01014d + public const int tabIndicatorHeight = 2130772301; + + // aapt resource value: 0x7f010153 + public const int tabMaxWidth = 2130772307; + + // aapt resource value: 0x7f010152 + public const int tabMinWidth = 2130772306; + + // aapt resource value: 0x7f010150 + public const int tabMode = 2130772304; + + // aapt resource value: 0x7f01015b + public const int tabPadding = 2130772315; + + // aapt resource value: 0x7f01015a + public const int tabPaddingBottom = 2130772314; + + // aapt resource value: 0x7f010159 + public const int tabPaddingEnd = 2130772313; + + // aapt resource value: 0x7f010157 + public const int tabPaddingStart = 2130772311; + + // aapt resource value: 0x7f010158 + public const int tabPaddingTop = 2130772312; + + // aapt resource value: 0x7f010156 + public const int tabSelectedTextColor = 2130772310; + + // aapt resource value: 0x7f010154 + public const int tabTextAppearance = 2130772308; + + // aapt resource value: 0x7f010155 + public const int tabTextColor = 2130772309; + + // aapt resource value: 0x7f01004f + public const int textAllCaps = 2130772047; + + // aapt resource value: 0x7f01007c + public const int textAppearanceLargePopupMenu = 2130772092; + + // aapt resource value: 0x7f0100a1 + public const int textAppearanceListItem = 2130772129; + + // aapt resource value: 0x7f0100a2 + public const int textAppearanceListItemSecondary = 2130772130; + + // aapt resource value: 0x7f0100a3 + public const int textAppearanceListItemSmall = 2130772131; + + // aapt resource value: 0x7f01007e + public const int textAppearancePopupMenuHeader = 2130772094; + + // aapt resource value: 0x7f010097 + public const int textAppearanceSearchResultSubtitle = 2130772119; + + // aapt resource value: 0x7f010096 + public const int textAppearanceSearchResultTitle = 2130772118; + + // aapt resource value: 0x7f01007d + public const int textAppearanceSmallPopupMenu = 2130772093; + + // aapt resource value: 0x7f0100b6 + public const int textColorAlertDialogListItem = 2130772150; + + // aapt resource value: 0x7f01013b + public const int textColorError = 2130772283; + + // aapt resource value: 0x7f010098 + public const int textColorSearchUrl = 2130772120; + + // aapt resource value: 0x7f010115 + public const int theme = 2130772245; + + // aapt resource value: 0x7f0100d6 + public const int thickness = 2130772182; + + // aapt resource value: 0x7f0100fc + public const int thumbTextPadding = 2130772220; + + // aapt resource value: 0x7f0100f7 + public const int thumbTint = 2130772215; + + // aapt resource value: 0x7f0100f8 + public const int thumbTintMode = 2130772216; + + // aapt resource value: 0x7f01004c + public const int tickMark = 2130772044; + + // aapt resource value: 0x7f01004d + public const int tickMarkTint = 2130772045; + + // aapt resource value: 0x7f01004e + public const int tickMarkTintMode = 2130772046; + + // aapt resource value: 0x7f01004a + public const int tint = 2130772042; + + // aapt resource value: 0x7f01004b + public const int tintMode = 2130772043; + + // aapt resource value: 0x7f010025 + public const int title = 2130772005; + + // aapt resource value: 0x7f01012e + public const int titleEnabled = 2130772270; + + // aapt resource value: 0x7f010104 + public const int titleMargin = 2130772228; + + // aapt resource value: 0x7f010108 + public const int titleMarginBottom = 2130772232; + + // aapt resource value: 0x7f010106 + public const int titleMarginEnd = 2130772230; + + // aapt resource value: 0x7f010105 + public const int titleMarginStart = 2130772229; + + // aapt resource value: 0x7f010107 + public const int titleMarginTop = 2130772231; + + // aapt resource value: 0x7f010109 + public const int titleMargins = 2130772233; + + // aapt resource value: 0x7f010102 + public const int titleTextAppearance = 2130772226; + + // aapt resource value: 0x7f010111 + public const int titleTextColor = 2130772241; + + // aapt resource value: 0x7f010029 + public const int titleTextStyle = 2130772009; + + // aapt resource value: 0x7f010129 + public const int toolbarId = 2130772265; + + // aapt resource value: 0x7f010090 + public const int toolbarNavigationButtonStyle = 2130772112; + + // aapt resource value: 0x7f01008f + public const int toolbarStyle = 2130772111; + + // aapt resource value: 0x7f0100c9 + public const int tooltipForegroundColor = 2130772169; + + // aapt resource value: 0x7f0100c8 + public const int tooltipFrameBackground = 2130772168; + + // aapt resource value: 0x7f0100e1 + public const int tooltipText = 2130772193; + + // aapt resource value: 0x7f0100f9 + public const int track = 2130772217; + + // aapt resource value: 0x7f0100fa + public const int trackTint = 2130772218; + + // aapt resource value: 0x7f0100fb + public const int trackTintMode = 2130772219; + + // aapt resource value: 0x7f010140 + public const int useCompatPadding = 2130772288; + + // aapt resource value: 0x7f0100f2 + public const int voiceIcon = 2130772210; + + // aapt resource value: 0x7f010056 + public const int windowActionBar = 2130772054; + + // aapt resource value: 0x7f010058 + public const int windowActionBarOverlay = 2130772056; + + // aapt resource value: 0x7f010059 + public const int windowActionModeOverlay = 2130772057; + + // aapt resource value: 0x7f01005d + public const int windowFixedHeightMajor = 2130772061; + + // aapt resource value: 0x7f01005b + public const int windowFixedHeightMinor = 2130772059; + + // aapt resource value: 0x7f01005a + public const int windowFixedWidthMajor = 2130772058; + + // aapt resource value: 0x7f01005c + public const int windowFixedWidthMinor = 2130772060; + + // aapt resource value: 0x7f01005e + public const int windowMinWidthMajor = 2130772062; + + // aapt resource value: 0x7f01005f + public const int windowMinWidthMinor = 2130772063; + + // aapt resource value: 0x7f010057 + public const int windowNoTitle = 2130772055; + + static Attribute() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Attribute() + { + } + } + + public partial class Boolean + { + + // aapt resource value: 0x7f0e0000 + public const int abc_action_bar_embed_tabs = 2131623936; + + // aapt resource value: 0x7f0e0001 + public const int abc_allow_stacked_button_bar = 2131623937; + + // aapt resource value: 0x7f0e0002 + public const int abc_config_actionMenuItemAllCaps = 2131623938; + + // aapt resource value: 0x7f0e0003 + public const int abc_config_closeDialogWhenTouchOutside = 2131623939; + + // aapt resource value: 0x7f0e0004 + public const int abc_config_showMenuShortcutsWhenKeyboardPresent = 2131623940; + + static Boolean() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Boolean() + { + } + } + + public partial class Color + { + + // aapt resource value: 0x7f0d004f + public const int abc_background_cache_hint_selector_material_dark = 2131558479; + + // aapt resource value: 0x7f0d0050 + public const int abc_background_cache_hint_selector_material_light = 2131558480; + + // aapt resource value: 0x7f0d0051 + public const int abc_btn_colored_borderless_text_material = 2131558481; + + // aapt resource value: 0x7f0d0052 + public const int abc_btn_colored_text_material = 2131558482; + + // aapt resource value: 0x7f0d0053 + public const int abc_color_highlight_material = 2131558483; + + // aapt resource value: 0x7f0d0054 + public const int abc_hint_foreground_material_dark = 2131558484; + + // aapt resource value: 0x7f0d0055 + public const int abc_hint_foreground_material_light = 2131558485; + + // aapt resource value: 0x7f0d0004 + public const int abc_input_method_navigation_guard = 2131558404; + + // aapt resource value: 0x7f0d0056 + public const int abc_primary_text_disable_only_material_dark = 2131558486; + + // aapt resource value: 0x7f0d0057 + public const int abc_primary_text_disable_only_material_light = 2131558487; + + // aapt resource value: 0x7f0d0058 + public const int abc_primary_text_material_dark = 2131558488; + + // aapt resource value: 0x7f0d0059 + public const int abc_primary_text_material_light = 2131558489; + + // aapt resource value: 0x7f0d005a + public const int abc_search_url_text = 2131558490; + + // aapt resource value: 0x7f0d0005 + public const int abc_search_url_text_normal = 2131558405; + + // aapt resource value: 0x7f0d0006 + public const int abc_search_url_text_pressed = 2131558406; + + // aapt resource value: 0x7f0d0007 + public const int abc_search_url_text_selected = 2131558407; + + // aapt resource value: 0x7f0d005b + public const int abc_secondary_text_material_dark = 2131558491; + + // aapt resource value: 0x7f0d005c + public const int abc_secondary_text_material_light = 2131558492; + + // aapt resource value: 0x7f0d005d + public const int abc_tint_btn_checkable = 2131558493; + + // aapt resource value: 0x7f0d005e + public const int abc_tint_default = 2131558494; + + // aapt resource value: 0x7f0d005f + public const int abc_tint_edittext = 2131558495; + + // aapt resource value: 0x7f0d0060 + public const int abc_tint_seek_thumb = 2131558496; + + // aapt resource value: 0x7f0d0061 + public const int abc_tint_spinner = 2131558497; + + // aapt resource value: 0x7f0d0062 + public const int abc_tint_switch_track = 2131558498; + + // aapt resource value: 0x7f0d0008 + public const int accent_material_dark = 2131558408; + + // aapt resource value: 0x7f0d0009 + public const int accent_material_light = 2131558409; + + // aapt resource value: 0x7f0d000a + public const int background_floating_material_dark = 2131558410; + + // aapt resource value: 0x7f0d000b + public const int background_floating_material_light = 2131558411; + + // aapt resource value: 0x7f0d000c + public const int background_material_dark = 2131558412; + + // aapt resource value: 0x7f0d000d + public const int background_material_light = 2131558413; + + // aapt resource value: 0x7f0d000e + public const int bright_foreground_disabled_material_dark = 2131558414; + + // aapt resource value: 0x7f0d000f + public const int bright_foreground_disabled_material_light = 2131558415; + + // aapt resource value: 0x7f0d0010 + public const int bright_foreground_inverse_material_dark = 2131558416; + + // aapt resource value: 0x7f0d0011 + public const int bright_foreground_inverse_material_light = 2131558417; + + // aapt resource value: 0x7f0d0012 + public const int bright_foreground_material_dark = 2131558418; + + // aapt resource value: 0x7f0d0013 + public const int bright_foreground_material_light = 2131558419; + + // aapt resource value: 0x7f0d0014 + public const int button_material_dark = 2131558420; + + // aapt resource value: 0x7f0d0015 + public const int button_material_light = 2131558421; + + // aapt resource value: 0x7f0d0000 + public const int cardview_dark_background = 2131558400; + + // aapt resource value: 0x7f0d0001 + public const int cardview_light_background = 2131558401; + + // aapt resource value: 0x7f0d0002 + public const int cardview_shadow_end_color = 2131558402; + + // aapt resource value: 0x7f0d0003 + public const int cardview_shadow_start_color = 2131558403; + + // aapt resource value: 0x7f0d004d + public const int colorAccent = 2131558477; + + // aapt resource value: 0x7f0d004b + public const int colorPrimary = 2131558475; + + // aapt resource value: 0x7f0d004c + public const int colorPrimaryDark = 2131558476; + + // aapt resource value: 0x7f0d0040 + public const int design_bottom_navigation_shadow_color = 2131558464; + + // aapt resource value: 0x7f0d0063 + public const int design_error = 2131558499; + + // aapt resource value: 0x7f0d0041 + public const int design_fab_shadow_end_color = 2131558465; + + // aapt resource value: 0x7f0d0042 + public const int design_fab_shadow_mid_color = 2131558466; + + // aapt resource value: 0x7f0d0043 + public const int design_fab_shadow_start_color = 2131558467; + + // aapt resource value: 0x7f0d0044 + public const int design_fab_stroke_end_inner_color = 2131558468; + + // aapt resource value: 0x7f0d0045 + public const int design_fab_stroke_end_outer_color = 2131558469; + + // aapt resource value: 0x7f0d0046 + public const int design_fab_stroke_top_inner_color = 2131558470; + + // aapt resource value: 0x7f0d0047 + public const int design_fab_stroke_top_outer_color = 2131558471; + + // aapt resource value: 0x7f0d0048 + public const int design_snackbar_background_color = 2131558472; + + // aapt resource value: 0x7f0d0064 + public const int design_tint_password_toggle = 2131558500; + + // aapt resource value: 0x7f0d0016 + public const int dim_foreground_disabled_material_dark = 2131558422; + + // aapt resource value: 0x7f0d0017 + public const int dim_foreground_disabled_material_light = 2131558423; + + // aapt resource value: 0x7f0d0018 + public const int dim_foreground_material_dark = 2131558424; + + // aapt resource value: 0x7f0d0019 + public const int dim_foreground_material_light = 2131558425; + + // aapt resource value: 0x7f0d001a + public const int error_color_material = 2131558426; + + // aapt resource value: 0x7f0d001b + public const int foreground_material_dark = 2131558427; + + // aapt resource value: 0x7f0d001c + public const int foreground_material_light = 2131558428; + + // aapt resource value: 0x7f0d001d + public const int highlighted_text_material_dark = 2131558429; + + // aapt resource value: 0x7f0d001e + public const int highlighted_text_material_light = 2131558430; + + // aapt resource value: 0x7f0d004e + public const int ic_launcher_background = 2131558478; + + // aapt resource value: 0x7f0d001f + public const int material_blue_grey_800 = 2131558431; + + // aapt resource value: 0x7f0d0020 + public const int material_blue_grey_900 = 2131558432; + + // aapt resource value: 0x7f0d0021 + public const int material_blue_grey_950 = 2131558433; + + // aapt resource value: 0x7f0d0022 + public const int material_deep_teal_200 = 2131558434; + + // aapt resource value: 0x7f0d0023 + public const int material_deep_teal_500 = 2131558435; + + // aapt resource value: 0x7f0d0024 + public const int material_grey_100 = 2131558436; + + // aapt resource value: 0x7f0d0025 + public const int material_grey_300 = 2131558437; + + // aapt resource value: 0x7f0d0026 + public const int material_grey_50 = 2131558438; + + // aapt resource value: 0x7f0d0027 + public const int material_grey_600 = 2131558439; + + // aapt resource value: 0x7f0d0028 + public const int material_grey_800 = 2131558440; + + // aapt resource value: 0x7f0d0029 + public const int material_grey_850 = 2131558441; + + // aapt resource value: 0x7f0d002a + public const int material_grey_900 = 2131558442; + + // aapt resource value: 0x7f0d0049 + public const int notification_action_color_filter = 2131558473; + + // aapt resource value: 0x7f0d004a + public const int notification_icon_bg_color = 2131558474; + + // aapt resource value: 0x7f0d003f + public const int notification_material_background_media_default_color = 2131558463; + + // aapt resource value: 0x7f0d002b + public const int primary_dark_material_dark = 2131558443; + + // aapt resource value: 0x7f0d002c + public const int primary_dark_material_light = 2131558444; + + // aapt resource value: 0x7f0d002d + public const int primary_material_dark = 2131558445; + + // aapt resource value: 0x7f0d002e + public const int primary_material_light = 2131558446; + + // aapt resource value: 0x7f0d002f + public const int primary_text_default_material_dark = 2131558447; + + // aapt resource value: 0x7f0d0030 + public const int primary_text_default_material_light = 2131558448; + + // aapt resource value: 0x7f0d0031 + public const int primary_text_disabled_material_dark = 2131558449; + + // aapt resource value: 0x7f0d0032 + public const int primary_text_disabled_material_light = 2131558450; + + // aapt resource value: 0x7f0d0033 + public const int ripple_material_dark = 2131558451; + + // aapt resource value: 0x7f0d0034 + public const int ripple_material_light = 2131558452; + + // aapt resource value: 0x7f0d0035 + public const int secondary_text_default_material_dark = 2131558453; + + // aapt resource value: 0x7f0d0036 + public const int secondary_text_default_material_light = 2131558454; + + // aapt resource value: 0x7f0d0037 + public const int secondary_text_disabled_material_dark = 2131558455; + + // aapt resource value: 0x7f0d0038 + public const int secondary_text_disabled_material_light = 2131558456; + + // aapt resource value: 0x7f0d0039 + public const int switch_thumb_disabled_material_dark = 2131558457; + + // aapt resource value: 0x7f0d003a + public const int switch_thumb_disabled_material_light = 2131558458; + + // aapt resource value: 0x7f0d0065 + public const int switch_thumb_material_dark = 2131558501; + + // aapt resource value: 0x7f0d0066 + public const int switch_thumb_material_light = 2131558502; + + // aapt resource value: 0x7f0d003b + public const int switch_thumb_normal_material_dark = 2131558459; + + // aapt resource value: 0x7f0d003c + public const int switch_thumb_normal_material_light = 2131558460; + + // aapt resource value: 0x7f0d003d + public const int tooltip_background_dark = 2131558461; + + // aapt resource value: 0x7f0d003e + public const int tooltip_background_light = 2131558462; + + static Color() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Color() + { + } + } + + public partial class Dimension + { + + // aapt resource value: 0x7f08001b + public const int abc_action_bar_content_inset_material = 2131230747; + + // aapt resource value: 0x7f08001c + public const int abc_action_bar_content_inset_with_nav = 2131230748; + + // aapt resource value: 0x7f080010 + public const int abc_action_bar_default_height_material = 2131230736; + + // aapt resource value: 0x7f08001d + public const int abc_action_bar_default_padding_end_material = 2131230749; + + // aapt resource value: 0x7f08001e + public const int abc_action_bar_default_padding_start_material = 2131230750; + + // aapt resource value: 0x7f080020 + public const int abc_action_bar_elevation_material = 2131230752; + + // aapt resource value: 0x7f080021 + public const int abc_action_bar_icon_vertical_padding_material = 2131230753; + + // aapt resource value: 0x7f080022 + public const int abc_action_bar_overflow_padding_end_material = 2131230754; + + // aapt resource value: 0x7f080023 + public const int abc_action_bar_overflow_padding_start_material = 2131230755; + + // aapt resource value: 0x7f080011 + public const int abc_action_bar_progress_bar_size = 2131230737; + + // aapt resource value: 0x7f080024 + public const int abc_action_bar_stacked_max_height = 2131230756; + + // aapt resource value: 0x7f080025 + public const int abc_action_bar_stacked_tab_max_width = 2131230757; + + // aapt resource value: 0x7f080026 + public const int abc_action_bar_subtitle_bottom_margin_material = 2131230758; + + // aapt resource value: 0x7f080027 + public const int abc_action_bar_subtitle_top_margin_material = 2131230759; + + // aapt resource value: 0x7f080028 + public const int abc_action_button_min_height_material = 2131230760; + + // aapt resource value: 0x7f080029 + public const int abc_action_button_min_width_material = 2131230761; + + // aapt resource value: 0x7f08002a + public const int abc_action_button_min_width_overflow_material = 2131230762; + + // aapt resource value: 0x7f08000f + public const int abc_alert_dialog_button_bar_height = 2131230735; + + // aapt resource value: 0x7f08002b + public const int abc_button_inset_horizontal_material = 2131230763; + + // aapt resource value: 0x7f08002c + public const int abc_button_inset_vertical_material = 2131230764; + + // aapt resource value: 0x7f08002d + public const int abc_button_padding_horizontal_material = 2131230765; + + // aapt resource value: 0x7f08002e + public const int abc_button_padding_vertical_material = 2131230766; + + // aapt resource value: 0x7f08002f + public const int abc_cascading_menus_min_smallest_width = 2131230767; + + // aapt resource value: 0x7f080014 + public const int abc_config_prefDialogWidth = 2131230740; + + // aapt resource value: 0x7f080030 + public const int abc_control_corner_material = 2131230768; + + // aapt resource value: 0x7f080031 + public const int abc_control_inset_material = 2131230769; + + // aapt resource value: 0x7f080032 + public const int abc_control_padding_material = 2131230770; + + // aapt resource value: 0x7f080015 + public const int abc_dialog_fixed_height_major = 2131230741; + + // aapt resource value: 0x7f080016 + public const int abc_dialog_fixed_height_minor = 2131230742; + + // aapt resource value: 0x7f080017 + public const int abc_dialog_fixed_width_major = 2131230743; + + // aapt resource value: 0x7f080018 + public const int abc_dialog_fixed_width_minor = 2131230744; + + // aapt resource value: 0x7f080033 + public const int abc_dialog_list_padding_bottom_no_buttons = 2131230771; + + // aapt resource value: 0x7f080034 + public const int abc_dialog_list_padding_top_no_title = 2131230772; + + // aapt resource value: 0x7f080019 + public const int abc_dialog_min_width_major = 2131230745; + + // aapt resource value: 0x7f08001a + public const int abc_dialog_min_width_minor = 2131230746; + + // aapt resource value: 0x7f080035 + public const int abc_dialog_padding_material = 2131230773; + + // aapt resource value: 0x7f080036 + public const int abc_dialog_padding_top_material = 2131230774; + + // aapt resource value: 0x7f080037 + public const int abc_dialog_title_divider_material = 2131230775; + + // aapt resource value: 0x7f080038 + public const int abc_disabled_alpha_material_dark = 2131230776; + + // aapt resource value: 0x7f080039 + public const int abc_disabled_alpha_material_light = 2131230777; + + // aapt resource value: 0x7f08003a + public const int abc_dropdownitem_icon_width = 2131230778; + + // aapt resource value: 0x7f08003b + public const int abc_dropdownitem_text_padding_left = 2131230779; + + // aapt resource value: 0x7f08003c + public const int abc_dropdownitem_text_padding_right = 2131230780; + + // aapt resource value: 0x7f08003d + public const int abc_edit_text_inset_bottom_material = 2131230781; + + // aapt resource value: 0x7f08003e + public const int abc_edit_text_inset_horizontal_material = 2131230782; + + // aapt resource value: 0x7f08003f + public const int abc_edit_text_inset_top_material = 2131230783; + + // aapt resource value: 0x7f080040 + public const int abc_floating_window_z = 2131230784; + + // aapt resource value: 0x7f080041 + public const int abc_list_item_padding_horizontal_material = 2131230785; + + // aapt resource value: 0x7f080042 + public const int abc_panel_menu_list_width = 2131230786; + + // aapt resource value: 0x7f080043 + public const int abc_progress_bar_height_material = 2131230787; + + // aapt resource value: 0x7f080044 + public const int abc_search_view_preferred_height = 2131230788; + + // aapt resource value: 0x7f080045 + public const int abc_search_view_preferred_width = 2131230789; + + // aapt resource value: 0x7f080046 + public const int abc_seekbar_track_background_height_material = 2131230790; + + // aapt resource value: 0x7f080047 + public const int abc_seekbar_track_progress_height_material = 2131230791; + + // aapt resource value: 0x7f080048 + public const int abc_select_dialog_padding_start_material = 2131230792; + + // aapt resource value: 0x7f08001f + public const int abc_switch_padding = 2131230751; + + // aapt resource value: 0x7f080049 + public const int abc_text_size_body_1_material = 2131230793; + + // aapt resource value: 0x7f08004a + public const int abc_text_size_body_2_material = 2131230794; + + // aapt resource value: 0x7f08004b + public const int abc_text_size_button_material = 2131230795; + + // aapt resource value: 0x7f08004c + public const int abc_text_size_caption_material = 2131230796; + + // aapt resource value: 0x7f08004d + public const int abc_text_size_display_1_material = 2131230797; + + // aapt resource value: 0x7f08004e + public const int abc_text_size_display_2_material = 2131230798; + + // aapt resource value: 0x7f08004f + public const int abc_text_size_display_3_material = 2131230799; + + // aapt resource value: 0x7f080050 + public const int abc_text_size_display_4_material = 2131230800; + + // aapt resource value: 0x7f080051 + public const int abc_text_size_headline_material = 2131230801; + + // aapt resource value: 0x7f080052 + public const int abc_text_size_large_material = 2131230802; + + // aapt resource value: 0x7f080053 + public const int abc_text_size_medium_material = 2131230803; + + // aapt resource value: 0x7f080054 + public const int abc_text_size_menu_header_material = 2131230804; + + // aapt resource value: 0x7f080055 + public const int abc_text_size_menu_material = 2131230805; + + // aapt resource value: 0x7f080056 + public const int abc_text_size_small_material = 2131230806; + + // aapt resource value: 0x7f080057 + public const int abc_text_size_subhead_material = 2131230807; + + // aapt resource value: 0x7f080012 + public const int abc_text_size_subtitle_material_toolbar = 2131230738; + + // aapt resource value: 0x7f080058 + public const int abc_text_size_title_material = 2131230808; + + // aapt resource value: 0x7f080013 + public const int abc_text_size_title_material_toolbar = 2131230739; + + // aapt resource value: 0x7f08000c + public const int cardview_compat_inset_shadow = 2131230732; + + // aapt resource value: 0x7f08000d + public const int cardview_default_elevation = 2131230733; + + // aapt resource value: 0x7f08000e + public const int cardview_default_radius = 2131230734; + + // aapt resource value: 0x7f080094 + public const int compat_button_inset_horizontal_material = 2131230868; + + // aapt resource value: 0x7f080095 + public const int compat_button_inset_vertical_material = 2131230869; + + // aapt resource value: 0x7f080096 + public const int compat_button_padding_horizontal_material = 2131230870; + + // aapt resource value: 0x7f080097 + public const int compat_button_padding_vertical_material = 2131230871; + + // aapt resource value: 0x7f080098 + public const int compat_control_corner_material = 2131230872; + + // aapt resource value: 0x7f080072 + public const int design_appbar_elevation = 2131230834; + + // aapt resource value: 0x7f080073 + public const int design_bottom_navigation_active_item_max_width = 2131230835; + + // aapt resource value: 0x7f080074 + public const int design_bottom_navigation_active_text_size = 2131230836; + + // aapt resource value: 0x7f080075 + public const int design_bottom_navigation_elevation = 2131230837; + + // aapt resource value: 0x7f080076 + public const int design_bottom_navigation_height = 2131230838; + + // aapt resource value: 0x7f080077 + public const int design_bottom_navigation_item_max_width = 2131230839; + + // aapt resource value: 0x7f080078 + public const int design_bottom_navigation_item_min_width = 2131230840; + + // aapt resource value: 0x7f080079 + public const int design_bottom_navigation_margin = 2131230841; + + // aapt resource value: 0x7f08007a + public const int design_bottom_navigation_shadow_height = 2131230842; + + // aapt resource value: 0x7f08007b + public const int design_bottom_navigation_text_size = 2131230843; + + // aapt resource value: 0x7f08007c + public const int design_bottom_sheet_modal_elevation = 2131230844; + + // aapt resource value: 0x7f08007d + public const int design_bottom_sheet_peek_height_min = 2131230845; + + // aapt resource value: 0x7f08007e + public const int design_fab_border_width = 2131230846; + + // aapt resource value: 0x7f08007f + public const int design_fab_elevation = 2131230847; + + // aapt resource value: 0x7f080080 + public const int design_fab_image_size = 2131230848; + + // aapt resource value: 0x7f080081 + public const int design_fab_size_mini = 2131230849; + + // aapt resource value: 0x7f080082 + public const int design_fab_size_normal = 2131230850; + + // aapt resource value: 0x7f080083 + public const int design_fab_translation_z_pressed = 2131230851; + + // aapt resource value: 0x7f080084 + public const int design_navigation_elevation = 2131230852; + + // aapt resource value: 0x7f080085 + public const int design_navigation_icon_padding = 2131230853; + + // aapt resource value: 0x7f080086 + public const int design_navigation_icon_size = 2131230854; + + // aapt resource value: 0x7f08006a + public const int design_navigation_max_width = 2131230826; + + // aapt resource value: 0x7f080087 + public const int design_navigation_padding_bottom = 2131230855; + + // aapt resource value: 0x7f080088 + public const int design_navigation_separator_vertical_padding = 2131230856; + + // aapt resource value: 0x7f08006b + public const int design_snackbar_action_inline_max_width = 2131230827; + + // aapt resource value: 0x7f08006c + public const int design_snackbar_background_corner_radius = 2131230828; + + // aapt resource value: 0x7f080089 + public const int design_snackbar_elevation = 2131230857; + + // aapt resource value: 0x7f08006d + public const int design_snackbar_extra_spacing_horizontal = 2131230829; + + // aapt resource value: 0x7f08006e + public const int design_snackbar_max_width = 2131230830; + + // aapt resource value: 0x7f08006f + public const int design_snackbar_min_width = 2131230831; + + // aapt resource value: 0x7f08008a + public const int design_snackbar_padding_horizontal = 2131230858; + + // aapt resource value: 0x7f08008b + public const int design_snackbar_padding_vertical = 2131230859; + + // aapt resource value: 0x7f080070 + public const int design_snackbar_padding_vertical_2lines = 2131230832; + + // aapt resource value: 0x7f08008c + public const int design_snackbar_text_size = 2131230860; + + // aapt resource value: 0x7f08008d + public const int design_tab_max_width = 2131230861; + + // aapt resource value: 0x7f080071 + public const int design_tab_scrollable_min_width = 2131230833; + + // aapt resource value: 0x7f08008e + public const int design_tab_text_size = 2131230862; + + // aapt resource value: 0x7f08008f + public const int design_tab_text_size_2line = 2131230863; + + // aapt resource value: 0x7f080059 + public const int disabled_alpha_material_dark = 2131230809; + + // aapt resource value: 0x7f08005a + public const int disabled_alpha_material_light = 2131230810; + + // aapt resource value: 0x7f080000 + public const int fastscroll_default_thickness = 2131230720; + + // aapt resource value: 0x7f080001 + public const int fastscroll_margin = 2131230721; + + // aapt resource value: 0x7f080002 + public const int fastscroll_minimum_range = 2131230722; + + // aapt resource value: 0x7f08005b + public const int highlight_alpha_material_colored = 2131230811; + + // aapt resource value: 0x7f08005c + public const int highlight_alpha_material_dark = 2131230812; + + // aapt resource value: 0x7f08005d + public const int highlight_alpha_material_light = 2131230813; + + // aapt resource value: 0x7f08005e + public const int hint_alpha_material_dark = 2131230814; + + // aapt resource value: 0x7f08005f + public const int hint_alpha_material_light = 2131230815; + + // aapt resource value: 0x7f080060 + public const int hint_pressed_alpha_material_dark = 2131230816; + + // aapt resource value: 0x7f080061 + public const int hint_pressed_alpha_material_light = 2131230817; + + // aapt resource value: 0x7f080003 + public const int item_touch_helper_max_drag_scroll_per_frame = 2131230723; + + // aapt resource value: 0x7f080004 + public const int item_touch_helper_swipe_escape_max_velocity = 2131230724; + + // aapt resource value: 0x7f080005 + public const int item_touch_helper_swipe_escape_velocity = 2131230725; + + // aapt resource value: 0x7f080006 + public const int mr_controller_volume_group_list_item_height = 2131230726; + + // aapt resource value: 0x7f080007 + public const int mr_controller_volume_group_list_item_icon_size = 2131230727; + + // aapt resource value: 0x7f080008 + public const int mr_controller_volume_group_list_max_height = 2131230728; + + // aapt resource value: 0x7f08000b + public const int mr_controller_volume_group_list_padding_top = 2131230731; + + // aapt resource value: 0x7f080009 + public const int mr_dialog_fixed_width_major = 2131230729; + + // aapt resource value: 0x7f08000a + public const int mr_dialog_fixed_width_minor = 2131230730; + + // aapt resource value: 0x7f080099 + public const int notification_action_icon_size = 2131230873; + + // aapt resource value: 0x7f08009a + public const int notification_action_text_size = 2131230874; + + // aapt resource value: 0x7f08009b + public const int notification_big_circle_margin = 2131230875; + + // aapt resource value: 0x7f080091 + public const int notification_content_margin_start = 2131230865; + + // aapt resource value: 0x7f08009c + public const int notification_large_icon_height = 2131230876; + + // aapt resource value: 0x7f08009d + public const int notification_large_icon_width = 2131230877; + + // aapt resource value: 0x7f080092 + public const int notification_main_column_padding_top = 2131230866; + + // aapt resource value: 0x7f080093 + public const int notification_media_narrow_margin = 2131230867; + + // aapt resource value: 0x7f08009e + public const int notification_right_icon_size = 2131230878; + + // aapt resource value: 0x7f080090 + public const int notification_right_side_padding_top = 2131230864; + + // aapt resource value: 0x7f08009f + public const int notification_small_icon_background_padding = 2131230879; + + // aapt resource value: 0x7f0800a0 + public const int notification_small_icon_size_as_large = 2131230880; + + // aapt resource value: 0x7f0800a1 + public const int notification_subtext_size = 2131230881; + + // aapt resource value: 0x7f0800a2 + public const int notification_top_pad = 2131230882; + + // aapt resource value: 0x7f0800a3 + public const int notification_top_pad_large_text = 2131230883; + + // aapt resource value: 0x7f080062 + public const int tooltip_corner_radius = 2131230818; + + // aapt resource value: 0x7f080063 + public const int tooltip_horizontal_padding = 2131230819; + + // aapt resource value: 0x7f080064 + public const int tooltip_margin = 2131230820; + + // aapt resource value: 0x7f080065 + public const int tooltip_precise_anchor_extra_offset = 2131230821; + + // aapt resource value: 0x7f080066 + public const int tooltip_precise_anchor_threshold = 2131230822; + + // aapt resource value: 0x7f080067 + public const int tooltip_vertical_padding = 2131230823; + + // aapt resource value: 0x7f080068 + public const int tooltip_y_offset_non_touch = 2131230824; + + // aapt resource value: 0x7f080069 + public const int tooltip_y_offset_touch = 2131230825; + + static Dimension() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Dimension() + { + } + } + + public partial class Drawable + { + + // aapt resource value: 0x7f020000 + public const int abc_ab_share_pack_mtrl_alpha = 2130837504; + + // aapt resource value: 0x7f020001 + public const int abc_action_bar_item_background_material = 2130837505; + + // aapt resource value: 0x7f020002 + public const int abc_btn_borderless_material = 2130837506; + + // aapt resource value: 0x7f020003 + public const int abc_btn_check_material = 2130837507; + + // aapt resource value: 0x7f020004 + public const int abc_btn_check_to_on_mtrl_000 = 2130837508; + + // aapt resource value: 0x7f020005 + public const int abc_btn_check_to_on_mtrl_015 = 2130837509; + + // aapt resource value: 0x7f020006 + public const int abc_btn_colored_material = 2130837510; + + // aapt resource value: 0x7f020007 + public const int abc_btn_default_mtrl_shape = 2130837511; + + // aapt resource value: 0x7f020008 + public const int abc_btn_radio_material = 2130837512; + + // aapt resource value: 0x7f020009 + public const int abc_btn_radio_to_on_mtrl_000 = 2130837513; + + // aapt resource value: 0x7f02000a + public const int abc_btn_radio_to_on_mtrl_015 = 2130837514; + + // aapt resource value: 0x7f02000b + public const int abc_btn_switch_to_on_mtrl_00001 = 2130837515; + + // aapt resource value: 0x7f02000c + public const int abc_btn_switch_to_on_mtrl_00012 = 2130837516; + + // aapt resource value: 0x7f02000d + public const int abc_cab_background_internal_bg = 2130837517; + + // aapt resource value: 0x7f02000e + public const int abc_cab_background_top_material = 2130837518; + + // aapt resource value: 0x7f02000f + public const int abc_cab_background_top_mtrl_alpha = 2130837519; + + // aapt resource value: 0x7f020010 + public const int abc_control_background_material = 2130837520; + + // aapt resource value: 0x7f020011 + public const int abc_dialog_material_background = 2130837521; + + // aapt resource value: 0x7f020012 + public const int abc_edit_text_material = 2130837522; + + // aapt resource value: 0x7f020013 + public const int abc_ic_ab_back_material = 2130837523; + + // aapt resource value: 0x7f020014 + public const int abc_ic_arrow_drop_right_black_24dp = 2130837524; + + // aapt resource value: 0x7f020015 + public const int abc_ic_clear_material = 2130837525; + + // aapt resource value: 0x7f020016 + public const int abc_ic_commit_search_api_mtrl_alpha = 2130837526; + + // aapt resource value: 0x7f020017 + public const int abc_ic_go_search_api_material = 2130837527; + + // aapt resource value: 0x7f020018 + public const int abc_ic_menu_copy_mtrl_am_alpha = 2130837528; + + // aapt resource value: 0x7f020019 + public const int abc_ic_menu_cut_mtrl_alpha = 2130837529; + + // aapt resource value: 0x7f02001a + public const int abc_ic_menu_overflow_material = 2130837530; + + // aapt resource value: 0x7f02001b + public const int abc_ic_menu_paste_mtrl_am_alpha = 2130837531; + + // aapt resource value: 0x7f02001c + public const int abc_ic_menu_selectall_mtrl_alpha = 2130837532; + + // aapt resource value: 0x7f02001d + public const int abc_ic_menu_share_mtrl_alpha = 2130837533; + + // aapt resource value: 0x7f02001e + public const int abc_ic_search_api_material = 2130837534; + + // aapt resource value: 0x7f02001f + public const int abc_ic_star_black_16dp = 2130837535; + + // aapt resource value: 0x7f020020 + public const int abc_ic_star_black_36dp = 2130837536; + + // aapt resource value: 0x7f020021 + public const int abc_ic_star_black_48dp = 2130837537; + + // aapt resource value: 0x7f020022 + public const int abc_ic_star_half_black_16dp = 2130837538; + + // aapt resource value: 0x7f020023 + public const int abc_ic_star_half_black_36dp = 2130837539; + + // aapt resource value: 0x7f020024 + public const int abc_ic_star_half_black_48dp = 2130837540; + + // aapt resource value: 0x7f020025 + public const int abc_ic_voice_search_api_material = 2130837541; + + // aapt resource value: 0x7f020026 + public const int abc_item_background_holo_dark = 2130837542; + + // aapt resource value: 0x7f020027 + public const int abc_item_background_holo_light = 2130837543; + + // aapt resource value: 0x7f020028 + public const int abc_list_divider_mtrl_alpha = 2130837544; + + // aapt resource value: 0x7f020029 + public const int abc_list_focused_holo = 2130837545; + + // aapt resource value: 0x7f02002a + public const int abc_list_longpressed_holo = 2130837546; + + // aapt resource value: 0x7f02002b + public const int abc_list_pressed_holo_dark = 2130837547; + + // aapt resource value: 0x7f02002c + public const int abc_list_pressed_holo_light = 2130837548; + + // aapt resource value: 0x7f02002d + public const int abc_list_selector_background_transition_holo_dark = 2130837549; + + // aapt resource value: 0x7f02002e + public const int abc_list_selector_background_transition_holo_light = 2130837550; + + // aapt resource value: 0x7f02002f + public const int abc_list_selector_disabled_holo_dark = 2130837551; + + // aapt resource value: 0x7f020030 + public const int abc_list_selector_disabled_holo_light = 2130837552; + + // aapt resource value: 0x7f020031 + public const int abc_list_selector_holo_dark = 2130837553; + + // aapt resource value: 0x7f020032 + public const int abc_list_selector_holo_light = 2130837554; + + // aapt resource value: 0x7f020033 + public const int abc_menu_hardkey_panel_mtrl_mult = 2130837555; + + // aapt resource value: 0x7f020034 + public const int abc_popup_background_mtrl_mult = 2130837556; + + // aapt resource value: 0x7f020035 + public const int abc_ratingbar_indicator_material = 2130837557; + + // aapt resource value: 0x7f020036 + public const int abc_ratingbar_material = 2130837558; + + // aapt resource value: 0x7f020037 + public const int abc_ratingbar_small_material = 2130837559; + + // aapt resource value: 0x7f020038 + public const int abc_scrubber_control_off_mtrl_alpha = 2130837560; + + // aapt resource value: 0x7f020039 + public const int abc_scrubber_control_to_pressed_mtrl_000 = 2130837561; + + // aapt resource value: 0x7f02003a + public const int abc_scrubber_control_to_pressed_mtrl_005 = 2130837562; + + // aapt resource value: 0x7f02003b + public const int abc_scrubber_primary_mtrl_alpha = 2130837563; + + // aapt resource value: 0x7f02003c + public const int abc_scrubber_track_mtrl_alpha = 2130837564; + + // aapt resource value: 0x7f02003d + public const int abc_seekbar_thumb_material = 2130837565; + + // aapt resource value: 0x7f02003e + public const int abc_seekbar_tick_mark_material = 2130837566; + + // aapt resource value: 0x7f02003f + public const int abc_seekbar_track_material = 2130837567; + + // aapt resource value: 0x7f020040 + public const int abc_spinner_mtrl_am_alpha = 2130837568; + + // aapt resource value: 0x7f020041 + public const int abc_spinner_textfield_background_material = 2130837569; + + // aapt resource value: 0x7f020042 + public const int abc_switch_thumb_material = 2130837570; + + // aapt resource value: 0x7f020043 + public const int abc_switch_track_mtrl_alpha = 2130837571; + + // aapt resource value: 0x7f020044 + public const int abc_tab_indicator_material = 2130837572; + + // aapt resource value: 0x7f020045 + public const int abc_tab_indicator_mtrl_alpha = 2130837573; + + // aapt resource value: 0x7f020046 + public const int abc_text_cursor_material = 2130837574; + + // aapt resource value: 0x7f020047 + public const int abc_text_select_handle_left_mtrl_dark = 2130837575; + + // aapt resource value: 0x7f020048 + public const int abc_text_select_handle_left_mtrl_light = 2130837576; + + // aapt resource value: 0x7f020049 + public const int abc_text_select_handle_middle_mtrl_dark = 2130837577; + + // aapt resource value: 0x7f02004a + public const int abc_text_select_handle_middle_mtrl_light = 2130837578; + + // aapt resource value: 0x7f02004b + public const int abc_text_select_handle_right_mtrl_dark = 2130837579; + + // aapt resource value: 0x7f02004c + public const int abc_text_select_handle_right_mtrl_light = 2130837580; + + // aapt resource value: 0x7f02004d + public const int abc_textfield_activated_mtrl_alpha = 2130837581; + + // aapt resource value: 0x7f02004e + public const int abc_textfield_default_mtrl_alpha = 2130837582; + + // aapt resource value: 0x7f02004f + public const int abc_textfield_search_activated_mtrl_alpha = 2130837583; + + // aapt resource value: 0x7f020050 + public const int abc_textfield_search_default_mtrl_alpha = 2130837584; + + // aapt resource value: 0x7f020051 + public const int abc_textfield_search_material = 2130837585; + + // aapt resource value: 0x7f020052 + public const int abc_vector_test = 2130837586; + + // aapt resource value: 0x7f020053 + public const int avd_hide_password = 2130837587; + + // aapt resource value: 0x7f02012f + public const int avd_hide_password_1 = 2130837807; + + // aapt resource value: 0x7f020130 + public const int avd_hide_password_2 = 2130837808; + + // aapt resource value: 0x7f020131 + public const int avd_hide_password_3 = 2130837809; + + // aapt resource value: 0x7f020054 + public const int avd_show_password = 2130837588; + + // aapt resource value: 0x7f020132 + public const int avd_show_password_1 = 2130837810; + + // aapt resource value: 0x7f020133 + public const int avd_show_password_2 = 2130837811; + + // aapt resource value: 0x7f020134 + public const int avd_show_password_3 = 2130837812; + + // aapt resource value: 0x7f020055 + public const int design_bottom_navigation_item_background = 2130837589; + + // aapt resource value: 0x7f020056 + public const int design_fab_background = 2130837590; + + // aapt resource value: 0x7f020057 + public const int design_ic_visibility = 2130837591; + + // aapt resource value: 0x7f020058 + public const int design_ic_visibility_off = 2130837592; + + // aapt resource value: 0x7f020059 + public const int design_password_eye = 2130837593; + + // aapt resource value: 0x7f02005a + public const int design_snackbar_background = 2130837594; + + // aapt resource value: 0x7f02005b + public const int ic_audiotrack_dark = 2130837595; + + // aapt resource value: 0x7f02005c + public const int ic_audiotrack_light = 2130837596; + + // aapt resource value: 0x7f02005d + public const int ic_dialog_close_dark = 2130837597; + + // aapt resource value: 0x7f02005e + public const int ic_dialog_close_light = 2130837598; + + // aapt resource value: 0x7f02005f + public const int ic_group_collapse_00 = 2130837599; + + // aapt resource value: 0x7f020060 + public const int ic_group_collapse_01 = 2130837600; + + // aapt resource value: 0x7f020061 + public const int ic_group_collapse_02 = 2130837601; + + // aapt resource value: 0x7f020062 + public const int ic_group_collapse_03 = 2130837602; + + // aapt resource value: 0x7f020063 + public const int ic_group_collapse_04 = 2130837603; + + // aapt resource value: 0x7f020064 + public const int ic_group_collapse_05 = 2130837604; + + // aapt resource value: 0x7f020065 + public const int ic_group_collapse_06 = 2130837605; + + // aapt resource value: 0x7f020066 + public const int ic_group_collapse_07 = 2130837606; + + // aapt resource value: 0x7f020067 + public const int ic_group_collapse_08 = 2130837607; + + // aapt resource value: 0x7f020068 + public const int ic_group_collapse_09 = 2130837608; + + // aapt resource value: 0x7f020069 + public const int ic_group_collapse_10 = 2130837609; + + // aapt resource value: 0x7f02006a + public const int ic_group_collapse_11 = 2130837610; + + // aapt resource value: 0x7f02006b + public const int ic_group_collapse_12 = 2130837611; + + // aapt resource value: 0x7f02006c + public const int ic_group_collapse_13 = 2130837612; + + // aapt resource value: 0x7f02006d + public const int ic_group_collapse_14 = 2130837613; + + // aapt resource value: 0x7f02006e + public const int ic_group_collapse_15 = 2130837614; + + // aapt resource value: 0x7f02006f + public const int ic_group_expand_00 = 2130837615; + + // aapt resource value: 0x7f020070 + public const int ic_group_expand_01 = 2130837616; + + // aapt resource value: 0x7f020071 + public const int ic_group_expand_02 = 2130837617; + + // aapt resource value: 0x7f020072 + public const int ic_group_expand_03 = 2130837618; + + // aapt resource value: 0x7f020073 + public const int ic_group_expand_04 = 2130837619; + + // aapt resource value: 0x7f020074 + public const int ic_group_expand_05 = 2130837620; + + // aapt resource value: 0x7f020075 + public const int ic_group_expand_06 = 2130837621; + + // aapt resource value: 0x7f020076 + public const int ic_group_expand_07 = 2130837622; + + // aapt resource value: 0x7f020077 + public const int ic_group_expand_08 = 2130837623; + + // aapt resource value: 0x7f020078 + public const int ic_group_expand_09 = 2130837624; + + // aapt resource value: 0x7f020079 + public const int ic_group_expand_10 = 2130837625; + + // aapt resource value: 0x7f02007a + public const int ic_group_expand_11 = 2130837626; + + // aapt resource value: 0x7f02007b + public const int ic_group_expand_12 = 2130837627; + + // aapt resource value: 0x7f02007c + public const int ic_group_expand_13 = 2130837628; + + // aapt resource value: 0x7f02007d + public const int ic_group_expand_14 = 2130837629; + + // aapt resource value: 0x7f02007e + public const int ic_group_expand_15 = 2130837630; + + // aapt resource value: 0x7f02007f + public const int ic_media_pause_dark = 2130837631; + + // aapt resource value: 0x7f020080 + public const int ic_media_pause_light = 2130837632; + + // aapt resource value: 0x7f020081 + public const int ic_media_play_dark = 2130837633; + + // aapt resource value: 0x7f020082 + public const int ic_media_play_light = 2130837634; + + // aapt resource value: 0x7f020083 + public const int ic_media_stop_dark = 2130837635; + + // aapt resource value: 0x7f020084 + public const int ic_media_stop_light = 2130837636; + + // aapt resource value: 0x7f020085 + public const int ic_mr_button_connected_00_dark = 2130837637; + + // aapt resource value: 0x7f020086 + public const int ic_mr_button_connected_00_light = 2130837638; + + // aapt resource value: 0x7f020087 + public const int ic_mr_button_connected_01_dark = 2130837639; + + // aapt resource value: 0x7f020088 + public const int ic_mr_button_connected_01_light = 2130837640; + + // aapt resource value: 0x7f020089 + public const int ic_mr_button_connected_02_dark = 2130837641; + + // aapt resource value: 0x7f02008a + public const int ic_mr_button_connected_02_light = 2130837642; + + // aapt resource value: 0x7f02008b + public const int ic_mr_button_connected_03_dark = 2130837643; + + // aapt resource value: 0x7f02008c + public const int ic_mr_button_connected_03_light = 2130837644; + + // aapt resource value: 0x7f02008d + public const int ic_mr_button_connected_04_dark = 2130837645; + + // aapt resource value: 0x7f02008e + public const int ic_mr_button_connected_04_light = 2130837646; + + // aapt resource value: 0x7f02008f + public const int ic_mr_button_connected_05_dark = 2130837647; + + // aapt resource value: 0x7f020090 + public const int ic_mr_button_connected_05_light = 2130837648; + + // aapt resource value: 0x7f020091 + public const int ic_mr_button_connected_06_dark = 2130837649; + + // aapt resource value: 0x7f020092 + public const int ic_mr_button_connected_06_light = 2130837650; + + // aapt resource value: 0x7f020093 + public const int ic_mr_button_connected_07_dark = 2130837651; + + // aapt resource value: 0x7f020094 + public const int ic_mr_button_connected_07_light = 2130837652; + + // aapt resource value: 0x7f020095 + public const int ic_mr_button_connected_08_dark = 2130837653; + + // aapt resource value: 0x7f020096 + public const int ic_mr_button_connected_08_light = 2130837654; + + // aapt resource value: 0x7f020097 + public const int ic_mr_button_connected_09_dark = 2130837655; + + // aapt resource value: 0x7f020098 + public const int ic_mr_button_connected_09_light = 2130837656; + + // aapt resource value: 0x7f020099 + public const int ic_mr_button_connected_10_dark = 2130837657; + + // aapt resource value: 0x7f02009a + public const int ic_mr_button_connected_10_light = 2130837658; + + // aapt resource value: 0x7f02009b + public const int ic_mr_button_connected_11_dark = 2130837659; + + // aapt resource value: 0x7f02009c + public const int ic_mr_button_connected_11_light = 2130837660; + + // aapt resource value: 0x7f02009d + public const int ic_mr_button_connected_12_dark = 2130837661; + + // aapt resource value: 0x7f02009e + public const int ic_mr_button_connected_12_light = 2130837662; + + // aapt resource value: 0x7f02009f + public const int ic_mr_button_connected_13_dark = 2130837663; + + // aapt resource value: 0x7f0200a0 + public const int ic_mr_button_connected_13_light = 2130837664; + + // aapt resource value: 0x7f0200a1 + public const int ic_mr_button_connected_14_dark = 2130837665; + + // aapt resource value: 0x7f0200a2 + public const int ic_mr_button_connected_14_light = 2130837666; + + // aapt resource value: 0x7f0200a3 + public const int ic_mr_button_connected_15_dark = 2130837667; + + // aapt resource value: 0x7f0200a4 + public const int ic_mr_button_connected_15_light = 2130837668; + + // aapt resource value: 0x7f0200a5 + public const int ic_mr_button_connected_16_dark = 2130837669; + + // aapt resource value: 0x7f0200a6 + public const int ic_mr_button_connected_16_light = 2130837670; + + // aapt resource value: 0x7f0200a7 + public const int ic_mr_button_connected_17_dark = 2130837671; + + // aapt resource value: 0x7f0200a8 + public const int ic_mr_button_connected_17_light = 2130837672; + + // aapt resource value: 0x7f0200a9 + public const int ic_mr_button_connected_18_dark = 2130837673; + + // aapt resource value: 0x7f0200aa + public const int ic_mr_button_connected_18_light = 2130837674; + + // aapt resource value: 0x7f0200ab + public const int ic_mr_button_connected_19_dark = 2130837675; + + // aapt resource value: 0x7f0200ac + public const int ic_mr_button_connected_19_light = 2130837676; + + // aapt resource value: 0x7f0200ad + public const int ic_mr_button_connected_20_dark = 2130837677; + + // aapt resource value: 0x7f0200ae + public const int ic_mr_button_connected_20_light = 2130837678; + + // aapt resource value: 0x7f0200af + public const int ic_mr_button_connected_21_dark = 2130837679; + + // aapt resource value: 0x7f0200b0 + public const int ic_mr_button_connected_21_light = 2130837680; + + // aapt resource value: 0x7f0200b1 + public const int ic_mr_button_connected_22_dark = 2130837681; + + // aapt resource value: 0x7f0200b2 + public const int ic_mr_button_connected_22_light = 2130837682; + + // aapt resource value: 0x7f0200b3 + public const int ic_mr_button_connected_23_dark = 2130837683; + + // aapt resource value: 0x7f0200b4 + public const int ic_mr_button_connected_23_light = 2130837684; + + // aapt resource value: 0x7f0200b5 + public const int ic_mr_button_connected_24_dark = 2130837685; + + // aapt resource value: 0x7f0200b6 + public const int ic_mr_button_connected_24_light = 2130837686; + + // aapt resource value: 0x7f0200b7 + public const int ic_mr_button_connected_25_dark = 2130837687; + + // aapt resource value: 0x7f0200b8 + public const int ic_mr_button_connected_25_light = 2130837688; + + // aapt resource value: 0x7f0200b9 + public const int ic_mr_button_connected_26_dark = 2130837689; + + // aapt resource value: 0x7f0200ba + public const int ic_mr_button_connected_26_light = 2130837690; + + // aapt resource value: 0x7f0200bb + public const int ic_mr_button_connected_27_dark = 2130837691; + + // aapt resource value: 0x7f0200bc + public const int ic_mr_button_connected_27_light = 2130837692; + + // aapt resource value: 0x7f0200bd + public const int ic_mr_button_connected_28_dark = 2130837693; + + // aapt resource value: 0x7f0200be + public const int ic_mr_button_connected_28_light = 2130837694; + + // aapt resource value: 0x7f0200bf + public const int ic_mr_button_connected_29_dark = 2130837695; + + // aapt resource value: 0x7f0200c0 + public const int ic_mr_button_connected_29_light = 2130837696; + + // aapt resource value: 0x7f0200c1 + public const int ic_mr_button_connected_30_dark = 2130837697; + + // aapt resource value: 0x7f0200c2 + public const int ic_mr_button_connected_30_light = 2130837698; + + // aapt resource value: 0x7f0200c3 + public const int ic_mr_button_connecting_00_dark = 2130837699; + + // aapt resource value: 0x7f0200c4 + public const int ic_mr_button_connecting_00_light = 2130837700; + + // aapt resource value: 0x7f0200c5 + public const int ic_mr_button_connecting_01_dark = 2130837701; + + // aapt resource value: 0x7f0200c6 + public const int ic_mr_button_connecting_01_light = 2130837702; + + // aapt resource value: 0x7f0200c7 + public const int ic_mr_button_connecting_02_dark = 2130837703; + + // aapt resource value: 0x7f0200c8 + public const int ic_mr_button_connecting_02_light = 2130837704; + + // aapt resource value: 0x7f0200c9 + public const int ic_mr_button_connecting_03_dark = 2130837705; + + // aapt resource value: 0x7f0200ca + public const int ic_mr_button_connecting_03_light = 2130837706; + + // aapt resource value: 0x7f0200cb + public const int ic_mr_button_connecting_04_dark = 2130837707; + + // aapt resource value: 0x7f0200cc + public const int ic_mr_button_connecting_04_light = 2130837708; + + // aapt resource value: 0x7f0200cd + public const int ic_mr_button_connecting_05_dark = 2130837709; + + // aapt resource value: 0x7f0200ce + public const int ic_mr_button_connecting_05_light = 2130837710; + + // aapt resource value: 0x7f0200cf + public const int ic_mr_button_connecting_06_dark = 2130837711; + + // aapt resource value: 0x7f0200d0 + public const int ic_mr_button_connecting_06_light = 2130837712; + + // aapt resource value: 0x7f0200d1 + public const int ic_mr_button_connecting_07_dark = 2130837713; + + // aapt resource value: 0x7f0200d2 + public const int ic_mr_button_connecting_07_light = 2130837714; + + // aapt resource value: 0x7f0200d3 + public const int ic_mr_button_connecting_08_dark = 2130837715; + + // aapt resource value: 0x7f0200d4 + public const int ic_mr_button_connecting_08_light = 2130837716; + + // aapt resource value: 0x7f0200d5 + public const int ic_mr_button_connecting_09_dark = 2130837717; + + // aapt resource value: 0x7f0200d6 + public const int ic_mr_button_connecting_09_light = 2130837718; + + // aapt resource value: 0x7f0200d7 + public const int ic_mr_button_connecting_10_dark = 2130837719; + + // aapt resource value: 0x7f0200d8 + public const int ic_mr_button_connecting_10_light = 2130837720; + + // aapt resource value: 0x7f0200d9 + public const int ic_mr_button_connecting_11_dark = 2130837721; + + // aapt resource value: 0x7f0200da + public const int ic_mr_button_connecting_11_light = 2130837722; + + // aapt resource value: 0x7f0200db + public const int ic_mr_button_connecting_12_dark = 2130837723; + + // aapt resource value: 0x7f0200dc + public const int ic_mr_button_connecting_12_light = 2130837724; + + // aapt resource value: 0x7f0200dd + public const int ic_mr_button_connecting_13_dark = 2130837725; + + // aapt resource value: 0x7f0200de + public const int ic_mr_button_connecting_13_light = 2130837726; + + // aapt resource value: 0x7f0200df + public const int ic_mr_button_connecting_14_dark = 2130837727; + + // aapt resource value: 0x7f0200e0 + public const int ic_mr_button_connecting_14_light = 2130837728; + + // aapt resource value: 0x7f0200e1 + public const int ic_mr_button_connecting_15_dark = 2130837729; + + // aapt resource value: 0x7f0200e2 + public const int ic_mr_button_connecting_15_light = 2130837730; + + // aapt resource value: 0x7f0200e3 + public const int ic_mr_button_connecting_16_dark = 2130837731; + + // aapt resource value: 0x7f0200e4 + public const int ic_mr_button_connecting_16_light = 2130837732; + + // aapt resource value: 0x7f0200e5 + public const int ic_mr_button_connecting_17_dark = 2130837733; + + // aapt resource value: 0x7f0200e6 + public const int ic_mr_button_connecting_17_light = 2130837734; + + // aapt resource value: 0x7f0200e7 + public const int ic_mr_button_connecting_18_dark = 2130837735; + + // aapt resource value: 0x7f0200e8 + public const int ic_mr_button_connecting_18_light = 2130837736; + + // aapt resource value: 0x7f0200e9 + public const int ic_mr_button_connecting_19_dark = 2130837737; + + // aapt resource value: 0x7f0200ea + public const int ic_mr_button_connecting_19_light = 2130837738; + + // aapt resource value: 0x7f0200eb + public const int ic_mr_button_connecting_20_dark = 2130837739; + + // aapt resource value: 0x7f0200ec + public const int ic_mr_button_connecting_20_light = 2130837740; + + // aapt resource value: 0x7f0200ed + public const int ic_mr_button_connecting_21_dark = 2130837741; + + // aapt resource value: 0x7f0200ee + public const int ic_mr_button_connecting_21_light = 2130837742; + + // aapt resource value: 0x7f0200ef + public const int ic_mr_button_connecting_22_dark = 2130837743; + + // aapt resource value: 0x7f0200f0 + public const int ic_mr_button_connecting_22_light = 2130837744; + + // aapt resource value: 0x7f0200f1 + public const int ic_mr_button_connecting_23_dark = 2130837745; + + // aapt resource value: 0x7f0200f2 + public const int ic_mr_button_connecting_23_light = 2130837746; + + // aapt resource value: 0x7f0200f3 + public const int ic_mr_button_connecting_24_dark = 2130837747; + + // aapt resource value: 0x7f0200f4 + public const int ic_mr_button_connecting_24_light = 2130837748; + + // aapt resource value: 0x7f0200f5 + public const int ic_mr_button_connecting_25_dark = 2130837749; + + // aapt resource value: 0x7f0200f6 + public const int ic_mr_button_connecting_25_light = 2130837750; + + // aapt resource value: 0x7f0200f7 + public const int ic_mr_button_connecting_26_dark = 2130837751; + + // aapt resource value: 0x7f0200f8 + public const int ic_mr_button_connecting_26_light = 2130837752; + + // aapt resource value: 0x7f0200f9 + public const int ic_mr_button_connecting_27_dark = 2130837753; + + // aapt resource value: 0x7f0200fa + public const int ic_mr_button_connecting_27_light = 2130837754; + + // aapt resource value: 0x7f0200fb + public const int ic_mr_button_connecting_28_dark = 2130837755; + + // aapt resource value: 0x7f0200fc + public const int ic_mr_button_connecting_28_light = 2130837756; + + // aapt resource value: 0x7f0200fd + public const int ic_mr_button_connecting_29_dark = 2130837757; + + // aapt resource value: 0x7f0200fe + public const int ic_mr_button_connecting_29_light = 2130837758; + + // aapt resource value: 0x7f0200ff + public const int ic_mr_button_connecting_30_dark = 2130837759; + + // aapt resource value: 0x7f020100 + public const int ic_mr_button_connecting_30_light = 2130837760; + + // aapt resource value: 0x7f020101 + public const int ic_mr_button_disabled_dark = 2130837761; + + // aapt resource value: 0x7f020102 + public const int ic_mr_button_disabled_light = 2130837762; + + // aapt resource value: 0x7f020103 + public const int ic_mr_button_disconnected_dark = 2130837763; + + // aapt resource value: 0x7f020104 + public const int ic_mr_button_disconnected_light = 2130837764; + + // aapt resource value: 0x7f020105 + public const int ic_mr_button_grey = 2130837765; + + // aapt resource value: 0x7f020106 + public const int ic_vol_type_speaker_dark = 2130837766; + + // aapt resource value: 0x7f020107 + public const int ic_vol_type_speaker_group_dark = 2130837767; + + // aapt resource value: 0x7f020108 + public const int ic_vol_type_speaker_group_light = 2130837768; + + // aapt resource value: 0x7f020109 + public const int ic_vol_type_speaker_light = 2130837769; + + // aapt resource value: 0x7f02010a + public const int ic_vol_type_tv_dark = 2130837770; + + // aapt resource value: 0x7f02010b + public const int ic_vol_type_tv_light = 2130837771; + + // aapt resource value: 0x7f02010c + public const int mr_button_connected_dark = 2130837772; + + // aapt resource value: 0x7f02010d + public const int mr_button_connected_light = 2130837773; + + // aapt resource value: 0x7f02010e + public const int mr_button_connecting_dark = 2130837774; + + // aapt resource value: 0x7f02010f + public const int mr_button_connecting_light = 2130837775; + + // aapt resource value: 0x7f020110 + public const int mr_button_dark = 2130837776; + + // aapt resource value: 0x7f020111 + public const int mr_button_light = 2130837777; + + // aapt resource value: 0x7f020112 + public const int mr_dialog_close_dark = 2130837778; + + // aapt resource value: 0x7f020113 + public const int mr_dialog_close_light = 2130837779; + + // aapt resource value: 0x7f020114 + public const int mr_dialog_material_background_dark = 2130837780; + + // aapt resource value: 0x7f020115 + public const int mr_dialog_material_background_light = 2130837781; + + // aapt resource value: 0x7f020116 + public const int mr_group_collapse = 2130837782; + + // aapt resource value: 0x7f020117 + public const int mr_group_expand = 2130837783; + + // aapt resource value: 0x7f020118 + public const int mr_media_pause_dark = 2130837784; + + // aapt resource value: 0x7f020119 + public const int mr_media_pause_light = 2130837785; + + // aapt resource value: 0x7f02011a + public const int mr_media_play_dark = 2130837786; + + // aapt resource value: 0x7f02011b + public const int mr_media_play_light = 2130837787; + + // aapt resource value: 0x7f02011c + public const int mr_media_stop_dark = 2130837788; + + // aapt resource value: 0x7f02011d + public const int mr_media_stop_light = 2130837789; + + // aapt resource value: 0x7f02011e + public const int mr_vol_type_audiotrack_dark = 2130837790; + + // aapt resource value: 0x7f02011f + public const int mr_vol_type_audiotrack_light = 2130837791; + + // aapt resource value: 0x7f020120 + public const int navigation_empty_icon = 2130837792; + + // aapt resource value: 0x7f020121 + public const int notification_action_background = 2130837793; + + // aapt resource value: 0x7f020122 + public const int notification_bg = 2130837794; + + // aapt resource value: 0x7f020123 + public const int notification_bg_low = 2130837795; + + // aapt resource value: 0x7f020124 + public const int notification_bg_low_normal = 2130837796; + + // aapt resource value: 0x7f020125 + public const int notification_bg_low_pressed = 2130837797; + + // aapt resource value: 0x7f020126 + public const int notification_bg_normal = 2130837798; + + // aapt resource value: 0x7f020127 + public const int notification_bg_normal_pressed = 2130837799; + + // aapt resource value: 0x7f020128 + public const int notification_icon_background = 2130837800; + + // aapt resource value: 0x7f02012d + public const int notification_template_icon_bg = 2130837805; + + // aapt resource value: 0x7f02012e + public const int notification_template_icon_low_bg = 2130837806; + + // aapt resource value: 0x7f020129 + public const int notification_tile_bg = 2130837801; + + // aapt resource value: 0x7f02012a + public const int notify_panel_notification_icon_bg = 2130837802; + + // aapt resource value: 0x7f02012b + public const int tooltip_frame_dark = 2130837803; + + // aapt resource value: 0x7f02012c + public const int tooltip_frame_light = 2130837804; + + static Drawable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Drawable() + { + } + } + + public partial class Id + { + + // aapt resource value: 0x7f090032 + public const int ALT = 2131296306; + + // aapt resource value: 0x7f090033 + public const int CTRL = 2131296307; + + // aapt resource value: 0x7f090034 + public const int FUNCTION = 2131296308; + + // aapt resource value: 0x7f090035 + public const int META = 2131296309; + + // aapt resource value: 0x7f090036 + public const int SHIFT = 2131296310; + + // aapt resource value: 0x7f090037 + public const int SYM = 2131296311; + + // aapt resource value: 0x7f0900ba + public const int action0 = 2131296442; + + // aapt resource value: 0x7f09007c + public const int action_bar = 2131296380; + + // aapt resource value: 0x7f090001 + public const int action_bar_activity_content = 2131296257; + + // aapt resource value: 0x7f09007b + public const int action_bar_container = 2131296379; + + // aapt resource value: 0x7f090077 + public const int action_bar_root = 2131296375; + + // aapt resource value: 0x7f090002 + public const int action_bar_spinner = 2131296258; + + // aapt resource value: 0x7f09005b + public const int action_bar_subtitle = 2131296347; + + // aapt resource value: 0x7f09005a + public const int action_bar_title = 2131296346; + + // aapt resource value: 0x7f0900b7 + public const int action_container = 2131296439; + + // aapt resource value: 0x7f09007d + public const int action_context_bar = 2131296381; + + // aapt resource value: 0x7f0900be + public const int action_divider = 2131296446; + + // aapt resource value: 0x7f0900b8 + public const int action_image = 2131296440; + + // aapt resource value: 0x7f090003 + public const int action_menu_divider = 2131296259; + + // aapt resource value: 0x7f090004 + public const int action_menu_presenter = 2131296260; + + // aapt resource value: 0x7f090079 + public const int action_mode_bar = 2131296377; + + // aapt resource value: 0x7f090078 + public const int action_mode_bar_stub = 2131296376; + + // aapt resource value: 0x7f09005c + public const int action_mode_close_button = 2131296348; + + // aapt resource value: 0x7f0900b9 + public const int action_text = 2131296441; + + // aapt resource value: 0x7f0900c7 + public const int actions = 2131296455; + + // aapt resource value: 0x7f09005d + public const int activity_chooser_view_content = 2131296349; + + // aapt resource value: 0x7f090027 + public const int add = 2131296295; + + // aapt resource value: 0x7f090070 + public const int alertTitle = 2131296368; + + // aapt resource value: 0x7f090052 + public const int all = 2131296338; + + // aapt resource value: 0x7f090038 + public const int always = 2131296312; + + // aapt resource value: 0x7f090056 + public const int async = 2131296342; + + // aapt resource value: 0x7f090044 + public const int auto = 2131296324; + + // aapt resource value: 0x7f09002f + public const int beginning = 2131296303; + + // aapt resource value: 0x7f090057 + public const int blocking = 2131296343; + + // aapt resource value: 0x7f09003d + public const int bottom = 2131296317; + + // aapt resource value: 0x7f09008b + public const int bottomtab_navarea = 2131296395; + + // aapt resource value: 0x7f09008c + public const int bottomtab_tabbar = 2131296396; + + // aapt resource value: 0x7f090063 + public const int buttonPanel = 2131296355; + + // aapt resource value: 0x7f0900bb + public const int cancel_action = 2131296443; + + // aapt resource value: 0x7f090045 + public const int center = 2131296325; + + // aapt resource value: 0x7f090046 + public const int center_horizontal = 2131296326; + + // aapt resource value: 0x7f090047 + public const int center_vertical = 2131296327; + + // aapt resource value: 0x7f090073 + public const int checkbox = 2131296371; + + // aapt resource value: 0x7f0900c3 + public const int chronometer = 2131296451; + + // aapt resource value: 0x7f09004e + public const int clip_horizontal = 2131296334; + + // aapt resource value: 0x7f09004f + public const int clip_vertical = 2131296335; + + // aapt resource value: 0x7f090039 + public const int collapseActionView = 2131296313; + + // aapt resource value: 0x7f09008f + public const int container = 2131296399; + + // aapt resource value: 0x7f090066 + public const int contentPanel = 2131296358; + + // aapt resource value: 0x7f090090 + public const int coordinator = 2131296400; + + // aapt resource value: 0x7f09006d + public const int custom = 2131296365; + + // aapt resource value: 0x7f09006c + public const int customPanel = 2131296364; + + // aapt resource value: 0x7f09007a + public const int decor_content_parent = 2131296378; + + // aapt resource value: 0x7f090060 + public const int default_activity_button = 2131296352; + + // aapt resource value: 0x7f090092 + public const int design_bottom_sheet = 2131296402; + + // aapt resource value: 0x7f090099 + public const int design_menu_item_action_area = 2131296409; + + // aapt resource value: 0x7f090098 + public const int design_menu_item_action_area_stub = 2131296408; + + // aapt resource value: 0x7f090097 + public const int design_menu_item_text = 2131296407; + + // aapt resource value: 0x7f090096 + public const int design_navigation_view = 2131296406; + + // aapt resource value: 0x7f090020 + public const int disableHome = 2131296288; + + // aapt resource value: 0x7f09007e + public const int edit_query = 2131296382; + + // aapt resource value: 0x7f090030 + public const int end = 2131296304; + + // aapt resource value: 0x7f0900c9 + public const int end_padder = 2131296457; + + // aapt resource value: 0x7f09003f + public const int enterAlways = 2131296319; + + // aapt resource value: 0x7f090040 + public const int enterAlwaysCollapsed = 2131296320; + + // aapt resource value: 0x7f090041 + public const int exitUntilCollapsed = 2131296321; + + // aapt resource value: 0x7f09005e + public const int expand_activities_button = 2131296350; + + // aapt resource value: 0x7f090072 + public const int expanded_menu = 2131296370; + + // aapt resource value: 0x7f090050 + public const int fill = 2131296336; + + // aapt resource value: 0x7f090051 + public const int fill_horizontal = 2131296337; + + // aapt resource value: 0x7f090048 + public const int fill_vertical = 2131296328; + + // aapt resource value: 0x7f090054 + public const int @fixed = 2131296340; + + // aapt resource value: 0x7f09009b + public const int flyoutcontent_appbar = 2131296411; + + // aapt resource value: 0x7f09009c + public const int flyoutcontent_recycler = 2131296412; + + // aapt resource value: 0x7f090058 + public const int forever = 2131296344; + + // aapt resource value: 0x7f09000a + public const int ghost_view = 2131296266; + + // aapt resource value: 0x7f090005 + public const int home = 2131296261; + + // aapt resource value: 0x7f090021 + public const int homeAsUp = 2131296289; + + // aapt resource value: 0x7f090062 + public const int icon = 2131296354; + + // aapt resource value: 0x7f0900c8 + public const int icon_group = 2131296456; + + // aapt resource value: 0x7f09003a + public const int ifRoom = 2131296314; + + // aapt resource value: 0x7f09005f + public const int image = 2131296351; + + // aapt resource value: 0x7f0900c4 + public const int info = 2131296452; + + // aapt resource value: 0x7f090059 + public const int italic = 2131296345; + + // aapt resource value: 0x7f090000 + public const int item_touch_helper_previous_elevation = 2131296256; + + // aapt resource value: 0x7f09008e + public const int largeLabel = 2131296398; + + // aapt resource value: 0x7f090049 + public const int left = 2131296329; + + // aapt resource value: 0x7f090017 + public const int line1 = 2131296279; + + // aapt resource value: 0x7f090018 + public const int line3 = 2131296280; + + // aapt resource value: 0x7f09001d + public const int listMode = 2131296285; + + // aapt resource value: 0x7f090061 + public const int list_item = 2131296353; + + // aapt resource value: 0x7f0900ca + public const int main_appbar = 2131296458; + + // aapt resource value: 0x7f0900cd + public const int main_scrollview = 2131296461; + + // aapt resource value: 0x7f0900cc + public const int main_tablayout = 2131296460; + + // aapt resource value: 0x7f0900cb + public const int main_toolbar = 2131296459; + + // aapt resource value: 0x7f0900d3 + public const int masked = 2131296467; + + // aapt resource value: 0x7f0900bd + public const int media_actions = 2131296445; + + // aapt resource value: 0x7f0900d1 + public const int message = 2131296465; + + // aapt resource value: 0x7f090031 + public const int middle = 2131296305; + + // aapt resource value: 0x7f090053 + public const int mini = 2131296339; + + // aapt resource value: 0x7f0900a9 + public const int mr_art = 2131296425; + + // aapt resource value: 0x7f09009e + public const int mr_chooser_list = 2131296414; + + // aapt resource value: 0x7f0900a1 + public const int mr_chooser_route_desc = 2131296417; + + // aapt resource value: 0x7f09009f + public const int mr_chooser_route_icon = 2131296415; + + // aapt resource value: 0x7f0900a0 + public const int mr_chooser_route_name = 2131296416; + + // aapt resource value: 0x7f09009d + public const int mr_chooser_title = 2131296413; + + // aapt resource value: 0x7f0900a6 + public const int mr_close = 2131296422; + + // aapt resource value: 0x7f0900ac + public const int mr_control_divider = 2131296428; + + // aapt resource value: 0x7f0900b2 + public const int mr_control_playback_ctrl = 2131296434; + + // aapt resource value: 0x7f0900b5 + public const int mr_control_subtitle = 2131296437; + + // aapt resource value: 0x7f0900b4 + public const int mr_control_title = 2131296436; + + // aapt resource value: 0x7f0900b3 + public const int mr_control_title_container = 2131296435; + + // aapt resource value: 0x7f0900a7 + public const int mr_custom_control = 2131296423; + + // aapt resource value: 0x7f0900a8 + public const int mr_default_control = 2131296424; + + // aapt resource value: 0x7f0900a3 + public const int mr_dialog_area = 2131296419; + + // aapt resource value: 0x7f0900a2 + public const int mr_expandable_area = 2131296418; + + // aapt resource value: 0x7f0900b6 + public const int mr_group_expand_collapse = 2131296438; + + // aapt resource value: 0x7f0900aa + public const int mr_media_main_control = 2131296426; + + // aapt resource value: 0x7f0900a5 + public const int mr_name = 2131296421; + + // aapt resource value: 0x7f0900ab + public const int mr_playback_control = 2131296427; + + // aapt resource value: 0x7f0900a4 + public const int mr_title_bar = 2131296420; + + // aapt resource value: 0x7f0900ad + public const int mr_volume_control = 2131296429; + + // aapt resource value: 0x7f0900ae + public const int mr_volume_group_list = 2131296430; + + // aapt resource value: 0x7f0900b0 + public const int mr_volume_item_icon = 2131296432; + + // aapt resource value: 0x7f0900b1 + public const int mr_volume_slider = 2131296433; + + // aapt resource value: 0x7f090028 + public const int multiply = 2131296296; + + // aapt resource value: 0x7f090095 + public const int navigation_header_container = 2131296405; + + // aapt resource value: 0x7f09003b + public const int never = 2131296315; + + // aapt resource value: 0x7f090022 + public const int none = 2131296290; + + // aapt resource value: 0x7f09001e + public const int normal = 2131296286; + + // aapt resource value: 0x7f0900c6 + public const int notification_background = 2131296454; + + // aapt resource value: 0x7f0900c0 + public const int notification_main_column = 2131296448; + + // aapt resource value: 0x7f0900bf + public const int notification_main_column_container = 2131296447; + + // aapt resource value: 0x7f09004c + public const int parallax = 2131296332; + + // aapt resource value: 0x7f090065 + public const int parentPanel = 2131296357; + + // aapt resource value: 0x7f09000b + public const int parent_matrix = 2131296267; + + // aapt resource value: 0x7f09004d + public const int pin = 2131296333; + + // aapt resource value: 0x7f090006 + public const int progress_circular = 2131296262; + + // aapt resource value: 0x7f090007 + public const int progress_horizontal = 2131296263; + + // aapt resource value: 0x7f090075 + public const int radio = 2131296373; + + // aapt resource value: 0x7f09004a + public const int right = 2131296330; + + // aapt resource value: 0x7f0900c5 + public const int right_icon = 2131296453; + + // aapt resource value: 0x7f0900c1 + public const int right_side = 2131296449; + + // aapt resource value: 0x7f09000c + public const int save_image_matrix = 2131296268; + + // aapt resource value: 0x7f09000d + public const int save_non_transition_alpha = 2131296269; + + // aapt resource value: 0x7f09000e + public const int save_scale_type = 2131296270; + + // aapt resource value: 0x7f090029 + public const int screen = 2131296297; + + // aapt resource value: 0x7f090042 + public const int scroll = 2131296322; + + // aapt resource value: 0x7f09006b + public const int scrollIndicatorDown = 2131296363; + + // aapt resource value: 0x7f090067 + public const int scrollIndicatorUp = 2131296359; + + // aapt resource value: 0x7f090068 + public const int scrollView = 2131296360; + + // aapt resource value: 0x7f090055 + public const int scrollable = 2131296341; + + // aapt resource value: 0x7f090080 + public const int search_badge = 2131296384; + + // aapt resource value: 0x7f09007f + public const int search_bar = 2131296383; + + // aapt resource value: 0x7f090081 + public const int search_button = 2131296385; + + // aapt resource value: 0x7f090086 + public const int search_close_btn = 2131296390; + + // aapt resource value: 0x7f090082 + public const int search_edit_frame = 2131296386; + + // aapt resource value: 0x7f090088 + public const int search_go_btn = 2131296392; + + // aapt resource value: 0x7f090083 + public const int search_mag_icon = 2131296387; + + // aapt resource value: 0x7f090084 + public const int search_plate = 2131296388; + + // aapt resource value: 0x7f090085 + public const int search_src_text = 2131296389; + + // aapt resource value: 0x7f090089 + public const int search_voice_btn = 2131296393; + + // aapt resource value: 0x7f09008a + public const int select_dialog_listview = 2131296394; + + // aapt resource value: 0x7f0900ce + public const int shellcontent_appbar = 2131296462; + + // aapt resource value: 0x7f0900d0 + public const int shellcontent_scrollview = 2131296464; + + // aapt resource value: 0x7f0900cf + public const int shellcontent_toolbar = 2131296463; + + // aapt resource value: 0x7f090074 + public const int shortcut = 2131296372; + + // aapt resource value: 0x7f090023 + public const int showCustom = 2131296291; + + // aapt resource value: 0x7f090024 + public const int showHome = 2131296292; + + // aapt resource value: 0x7f090025 + public const int showTitle = 2131296293; + + // aapt resource value: 0x7f09008d + public const int smallLabel = 2131296397; + + // aapt resource value: 0x7f090094 + public const int snackbar_action = 2131296404; + + // aapt resource value: 0x7f090093 + public const int snackbar_text = 2131296403; + + // aapt resource value: 0x7f090043 + public const int snap = 2131296323; + + // aapt resource value: 0x7f090064 + public const int spacer = 2131296356; + + // aapt resource value: 0x7f090008 + public const int split_action_bar = 2131296264; + + // aapt resource value: 0x7f09002a + public const int src_atop = 2131296298; + + // aapt resource value: 0x7f09002b + public const int src_in = 2131296299; + + // aapt resource value: 0x7f09002c + public const int src_over = 2131296300; + + // aapt resource value: 0x7f09004b + public const int start = 2131296331; + + // aapt resource value: 0x7f0900bc + public const int status_bar_latest_event_content = 2131296444; + + // aapt resource value: 0x7f090076 + public const int submenuarrow = 2131296374; + + // aapt resource value: 0x7f090087 + public const int submit_area = 2131296391; + + // aapt resource value: 0x7f09001f + public const int tabMode = 2131296287; + + // aapt resource value: 0x7f090019 + public const int tag_transition_group = 2131296281; + + // aapt resource value: 0x7f09001a + public const int text = 2131296282; + + // aapt resource value: 0x7f09001b + public const int text2 = 2131296283; + + // aapt resource value: 0x7f09006a + public const int textSpacerNoButtons = 2131296362; + + // aapt resource value: 0x7f090069 + public const int textSpacerNoTitle = 2131296361; + + // aapt resource value: 0x7f09009a + public const int text_input_password_toggle = 2131296410; + + // aapt resource value: 0x7f090014 + public const int textinput_counter = 2131296276; + + // aapt resource value: 0x7f090015 + public const int textinput_error = 2131296277; + + // aapt resource value: 0x7f0900c2 + public const int time = 2131296450; + + // aapt resource value: 0x7f09001c + public const int title = 2131296284; + + // aapt resource value: 0x7f090071 + public const int titleDividerNoCustom = 2131296369; + + // aapt resource value: 0x7f09006f + public const int title_template = 2131296367; + + // aapt resource value: 0x7f09003e + public const int top = 2131296318; + + // aapt resource value: 0x7f09006e + public const int topPanel = 2131296366; + + // aapt resource value: 0x7f090091 + public const int touch_outside = 2131296401; + + // aapt resource value: 0x7f09000f + public const int transition_current_scene = 2131296271; + + // aapt resource value: 0x7f090010 + public const int transition_layout_save = 2131296272; + + // aapt resource value: 0x7f090011 + public const int transition_position = 2131296273; + + // aapt resource value: 0x7f090012 + public const int transition_scene_layoutid_cache = 2131296274; + + // aapt resource value: 0x7f090013 + public const int transition_transform = 2131296275; + + // aapt resource value: 0x7f09002d + public const int uniform = 2131296301; + + // aapt resource value: 0x7f090009 + public const int up = 2131296265; + + // aapt resource value: 0x7f090026 + public const int useLogo = 2131296294; + + // aapt resource value: 0x7f090016 + public const int view_offset_helper = 2131296278; + + // aapt resource value: 0x7f0900d2 + public const int visible = 2131296466; + + // aapt resource value: 0x7f0900af + public const int volume_item_container = 2131296431; + + // aapt resource value: 0x7f09003c + public const int withText = 2131296316; + + // aapt resource value: 0x7f09002e + public const int wrap_content = 2131296302; + + static Id() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Id() + { + } + } + + public partial class Integer + { + + // aapt resource value: 0x7f0b0003 + public const int abc_config_activityDefaultDur = 2131427331; + + // aapt resource value: 0x7f0b0004 + public const int abc_config_activityShortDur = 2131427332; + + // aapt resource value: 0x7f0b0008 + public const int app_bar_elevation_anim_duration = 2131427336; + + // aapt resource value: 0x7f0b0009 + public const int bottom_sheet_slide_duration = 2131427337; + + // aapt resource value: 0x7f0b0005 + public const int cancel_button_image_alpha = 2131427333; + + // aapt resource value: 0x7f0b0006 + public const int config_tooltipAnimTime = 2131427334; + + // aapt resource value: 0x7f0b0007 + public const int design_snackbar_text_max_lines = 2131427335; + + // aapt resource value: 0x7f0b000a + public const int hide_password_duration = 2131427338; + + // aapt resource value: 0x7f0b0000 + public const int mr_controller_volume_group_list_animation_duration_ms = 2131427328; + + // aapt resource value: 0x7f0b0001 + public const int mr_controller_volume_group_list_fade_in_duration_ms = 2131427329; + + // aapt resource value: 0x7f0b0002 + public const int mr_controller_volume_group_list_fade_out_duration_ms = 2131427330; + + // aapt resource value: 0x7f0b000b + public const int show_password_duration = 2131427339; + + // aapt resource value: 0x7f0b000c + public const int status_bar_notification_info_maxnum = 2131427340; + + static Integer() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Integer() + { + } + } + + public partial class Interpolator + { + + // aapt resource value: 0x7f070000 + public const int mr_fast_out_slow_in = 2131165184; + + // aapt resource value: 0x7f070001 + public const int mr_linear_out_slow_in = 2131165185; + + static Interpolator() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Interpolator() + { + } + } + + public partial class Layout + { + + // aapt resource value: 0x7f040000 + public const int abc_action_bar_title_item = 2130968576; + + // aapt resource value: 0x7f040001 + public const int abc_action_bar_up_container = 2130968577; + + // aapt resource value: 0x7f040002 + public const int abc_action_menu_item_layout = 2130968578; + + // aapt resource value: 0x7f040003 + public const int abc_action_menu_layout = 2130968579; + + // aapt resource value: 0x7f040004 + public const int abc_action_mode_bar = 2130968580; + + // aapt resource value: 0x7f040005 + public const int abc_action_mode_close_item_material = 2130968581; + + // aapt resource value: 0x7f040006 + public const int abc_activity_chooser_view = 2130968582; + + // aapt resource value: 0x7f040007 + public const int abc_activity_chooser_view_list_item = 2130968583; + + // aapt resource value: 0x7f040008 + public const int abc_alert_dialog_button_bar_material = 2130968584; + + // aapt resource value: 0x7f040009 + public const int abc_alert_dialog_material = 2130968585; + + // aapt resource value: 0x7f04000a + public const int abc_alert_dialog_title_material = 2130968586; + + // aapt resource value: 0x7f04000b + public const int abc_dialog_title_material = 2130968587; + + // aapt resource value: 0x7f04000c + public const int abc_expanded_menu_layout = 2130968588; + + // aapt resource value: 0x7f04000d + public const int abc_list_menu_item_checkbox = 2130968589; + + // aapt resource value: 0x7f04000e + public const int abc_list_menu_item_icon = 2130968590; + + // aapt resource value: 0x7f04000f + public const int abc_list_menu_item_layout = 2130968591; + + // aapt resource value: 0x7f040010 + public const int abc_list_menu_item_radio = 2130968592; + + // aapt resource value: 0x7f040011 + public const int abc_popup_menu_header_item_layout = 2130968593; + + // aapt resource value: 0x7f040012 + public const int abc_popup_menu_item_layout = 2130968594; + + // aapt resource value: 0x7f040013 + public const int abc_screen_content_include = 2130968595; + + // aapt resource value: 0x7f040014 + public const int abc_screen_simple = 2130968596; + + // aapt resource value: 0x7f040015 + public const int abc_screen_simple_overlay_action_mode = 2130968597; + + // aapt resource value: 0x7f040016 + public const int abc_screen_toolbar = 2130968598; + + // aapt resource value: 0x7f040017 + public const int abc_search_dropdown_item_icons_2line = 2130968599; + + // aapt resource value: 0x7f040018 + public const int abc_search_view = 2130968600; + + // aapt resource value: 0x7f040019 + public const int abc_select_dialog_material = 2130968601; + + // aapt resource value: 0x7f04001a + public const int activity_main = 2130968602; + + // aapt resource value: 0x7f04001b + public const int BottomTabLayout = 2130968603; + + // aapt resource value: 0x7f04001c + public const int design_bottom_navigation_item = 2130968604; + + // aapt resource value: 0x7f04001d + public const int design_bottom_sheet_dialog = 2130968605; + + // aapt resource value: 0x7f04001e + public const int design_layout_snackbar = 2130968606; + + // aapt resource value: 0x7f04001f + public const int design_layout_snackbar_include = 2130968607; + + // aapt resource value: 0x7f040020 + public const int design_layout_tab_icon = 2130968608; + + // aapt resource value: 0x7f040021 + public const int design_layout_tab_text = 2130968609; + + // aapt resource value: 0x7f040022 + public const int design_menu_item_action_area = 2130968610; + + // aapt resource value: 0x7f040023 + public const int design_navigation_item = 2130968611; + + // aapt resource value: 0x7f040024 + public const int design_navigation_item_header = 2130968612; + + // aapt resource value: 0x7f040025 + public const int design_navigation_item_separator = 2130968613; + + // aapt resource value: 0x7f040026 + public const int design_navigation_item_subheader = 2130968614; + + // aapt resource value: 0x7f040027 + public const int design_navigation_menu = 2130968615; + + // aapt resource value: 0x7f040028 + public const int design_navigation_menu_item = 2130968616; + + // aapt resource value: 0x7f040029 + public const int design_text_input_password_icon = 2130968617; + + // aapt resource value: 0x7f04002a + public const int FlyoutContent = 2130968618; + + // aapt resource value: 0x7f04002b + public const int mr_chooser_dialog = 2130968619; + + // aapt resource value: 0x7f04002c + public const int mr_chooser_list_item = 2130968620; + + // aapt resource value: 0x7f04002d + public const int mr_controller_material_dialog_b = 2130968621; + + // aapt resource value: 0x7f04002e + public const int mr_controller_volume_item = 2130968622; + + // aapt resource value: 0x7f04002f + public const int mr_playback_control = 2130968623; + + // aapt resource value: 0x7f040030 + public const int mr_volume_control = 2130968624; + + // aapt resource value: 0x7f040031 + public const int notification_action = 2130968625; + + // aapt resource value: 0x7f040032 + public const int notification_action_tombstone = 2130968626; + + // aapt resource value: 0x7f040033 + public const int notification_media_action = 2130968627; + + // aapt resource value: 0x7f040034 + public const int notification_media_cancel_action = 2130968628; + + // aapt resource value: 0x7f040035 + public const int notification_template_big_media = 2130968629; + + // aapt resource value: 0x7f040036 + public const int notification_template_big_media_custom = 2130968630; + + // aapt resource value: 0x7f040037 + public const int notification_template_big_media_narrow = 2130968631; + + // aapt resource value: 0x7f040038 + public const int notification_template_big_media_narrow_custom = 2130968632; + + // aapt resource value: 0x7f040039 + public const int notification_template_custom_big = 2130968633; + + // aapt resource value: 0x7f04003a + public const int notification_template_icon_group = 2130968634; + + // aapt resource value: 0x7f04003b + public const int notification_template_lines_media = 2130968635; + + // aapt resource value: 0x7f04003c + public const int notification_template_media = 2130968636; + + // aapt resource value: 0x7f04003d + public const int notification_template_media_custom = 2130968637; + + // aapt resource value: 0x7f04003e + public const int notification_template_part_chronometer = 2130968638; + + // aapt resource value: 0x7f04003f + public const int notification_template_part_time = 2130968639; + + // aapt resource value: 0x7f040040 + public const int RootLayout = 2130968640; + + // aapt resource value: 0x7f040041 + public const int select_dialog_item_material = 2130968641; + + // aapt resource value: 0x7f040042 + public const int select_dialog_multichoice_material = 2130968642; + + // aapt resource value: 0x7f040043 + public const int select_dialog_singlechoice_material = 2130968643; + + // aapt resource value: 0x7f040044 + public const int ShellContent = 2130968644; + + // aapt resource value: 0x7f040045 + public const int support_simple_spinner_dropdown_item = 2130968645; + + // aapt resource value: 0x7f040046 + public const int tooltip = 2130968646; + + static Layout() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Layout() + { + } + } + + public partial class Mipmap + { + + // aapt resource value: 0x7f030000 + public const int ic_launcher = 2130903040; + + // aapt resource value: 0x7f030001 + public const int ic_launcher_foreground = 2130903041; + + // aapt resource value: 0x7f030002 + public const int ic_launcher_round = 2130903042; + + static Mipmap() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Mipmap() + { + } + } + + public partial class String + { + + // aapt resource value: 0x7f0a0015 + public const int abc_action_bar_home_description = 2131361813; + + // aapt resource value: 0x7f0a0016 + public const int abc_action_bar_up_description = 2131361814; + + // aapt resource value: 0x7f0a0017 + public const int abc_action_menu_overflow_description = 2131361815; + + // aapt resource value: 0x7f0a0018 + public const int abc_action_mode_done = 2131361816; + + // aapt resource value: 0x7f0a0019 + public const int abc_activity_chooser_view_see_all = 2131361817; + + // aapt resource value: 0x7f0a001a + public const int abc_activitychooserview_choose_application = 2131361818; + + // aapt resource value: 0x7f0a001b + public const int abc_capital_off = 2131361819; + + // aapt resource value: 0x7f0a001c + public const int abc_capital_on = 2131361820; + + // aapt resource value: 0x7f0a0027 + public const int abc_font_family_body_1_material = 2131361831; + + // aapt resource value: 0x7f0a0028 + public const int abc_font_family_body_2_material = 2131361832; + + // aapt resource value: 0x7f0a0029 + public const int abc_font_family_button_material = 2131361833; + + // aapt resource value: 0x7f0a002a + public const int abc_font_family_caption_material = 2131361834; + + // aapt resource value: 0x7f0a002b + public const int abc_font_family_display_1_material = 2131361835; + + // aapt resource value: 0x7f0a002c + public const int abc_font_family_display_2_material = 2131361836; + + // aapt resource value: 0x7f0a002d + public const int abc_font_family_display_3_material = 2131361837; + + // aapt resource value: 0x7f0a002e + public const int abc_font_family_display_4_material = 2131361838; + + // aapt resource value: 0x7f0a002f + public const int abc_font_family_headline_material = 2131361839; + + // aapt resource value: 0x7f0a0030 + public const int abc_font_family_menu_material = 2131361840; + + // aapt resource value: 0x7f0a0031 + public const int abc_font_family_subhead_material = 2131361841; + + // aapt resource value: 0x7f0a0032 + public const int abc_font_family_title_material = 2131361842; + + // aapt resource value: 0x7f0a001d + public const int abc_search_hint = 2131361821; + + // aapt resource value: 0x7f0a001e + public const int abc_searchview_description_clear = 2131361822; + + // aapt resource value: 0x7f0a001f + public const int abc_searchview_description_query = 2131361823; + + // aapt resource value: 0x7f0a0020 + public const int abc_searchview_description_search = 2131361824; + + // aapt resource value: 0x7f0a0021 + public const int abc_searchview_description_submit = 2131361825; + + // aapt resource value: 0x7f0a0022 + public const int abc_searchview_description_voice = 2131361826; + + // aapt resource value: 0x7f0a0023 + public const int abc_shareactionprovider_share_with = 2131361827; + + // aapt resource value: 0x7f0a0024 + public const int abc_shareactionprovider_share_with_application = 2131361828; + + // aapt resource value: 0x7f0a0025 + public const int abc_toolbar_collapse_description = 2131361829; + + // aapt resource value: 0x7f0a003d + public const int action_settings = 2131361853; + + // aapt resource value: 0x7f0a003c + public const int app_name = 2131361852; + + // aapt resource value: 0x7f0a0033 + public const int appbar_scrolling_view_behavior = 2131361843; + + // aapt resource value: 0x7f0a0034 + public const int bottom_sheet_behavior = 2131361844; + + // aapt resource value: 0x7f0a0035 + public const int character_counter_pattern = 2131361845; + + // aapt resource value: 0x7f0a0000 + public const int mr_button_content_description = 2131361792; + + // aapt resource value: 0x7f0a0001 + public const int mr_cast_button_connected = 2131361793; + + // aapt resource value: 0x7f0a0002 + public const int mr_cast_button_connecting = 2131361794; + + // aapt resource value: 0x7f0a0003 + public const int mr_cast_button_disconnected = 2131361795; + + // aapt resource value: 0x7f0a0004 + public const int mr_chooser_searching = 2131361796; + + // aapt resource value: 0x7f0a0005 + public const int mr_chooser_title = 2131361797; + + // aapt resource value: 0x7f0a0006 + public const int mr_controller_album_art = 2131361798; + + // aapt resource value: 0x7f0a0007 + public const int mr_controller_casting_screen = 2131361799; + + // aapt resource value: 0x7f0a0008 + public const int mr_controller_close_description = 2131361800; + + // aapt resource value: 0x7f0a0009 + public const int mr_controller_collapse_group = 2131361801; + + // aapt resource value: 0x7f0a000a + public const int mr_controller_disconnect = 2131361802; + + // aapt resource value: 0x7f0a000b + public const int mr_controller_expand_group = 2131361803; + + // aapt resource value: 0x7f0a000c + public const int mr_controller_no_info_available = 2131361804; + + // aapt resource value: 0x7f0a000d + public const int mr_controller_no_media_selected = 2131361805; + + // aapt resource value: 0x7f0a000e + public const int mr_controller_pause = 2131361806; + + // aapt resource value: 0x7f0a000f + public const int mr_controller_play = 2131361807; + + // aapt resource value: 0x7f0a0010 + public const int mr_controller_stop = 2131361808; + + // aapt resource value: 0x7f0a0011 + public const int mr_controller_stop_casting = 2131361809; + + // aapt resource value: 0x7f0a0012 + public const int mr_controller_volume_slider = 2131361810; + + // aapt resource value: 0x7f0a0013 + public const int mr_system_route_name = 2131361811; + + // aapt resource value: 0x7f0a0014 + public const int mr_user_route_category_name = 2131361812; + + // aapt resource value: 0x7f0a0036 + public const int password_toggle_content_description = 2131361846; + + // aapt resource value: 0x7f0a0037 + public const int path_password_eye = 2131361847; + + // aapt resource value: 0x7f0a0038 + public const int path_password_eye_mask_strike_through = 2131361848; + + // aapt resource value: 0x7f0a0039 + public const int path_password_eye_mask_visible = 2131361849; + + // aapt resource value: 0x7f0a003a + public const int path_password_strike_through = 2131361850; + + // aapt resource value: 0x7f0a0026 + public const int search_menu_title = 2131361830; + + // aapt resource value: 0x7f0a003b + public const int status_bar_notification_info_overflow = 2131361851; + + static String() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private String() + { + } + } + + public partial class Style + { + + // aapt resource value: 0x7f0c00a4 + public const int AlertDialog_AppCompat = 2131493028; + + // aapt resource value: 0x7f0c00a5 + public const int AlertDialog_AppCompat_Light = 2131493029; + + // aapt resource value: 0x7f0c00a6 + public const int Animation_AppCompat_Dialog = 2131493030; + + // aapt resource value: 0x7f0c00a7 + public const int Animation_AppCompat_DropDownUp = 2131493031; + + // aapt resource value: 0x7f0c00a8 + public const int Animation_AppCompat_Tooltip = 2131493032; + + // aapt resource value: 0x7f0c016e + public const int Animation_Design_BottomSheetDialog = 2131493230; + + // aapt resource value: 0x7f0c018f + public const int AppTheme = 2131493263; + + // aapt resource value: 0x7f0c00a9 + public const int Base_AlertDialog_AppCompat = 2131493033; + + // aapt resource value: 0x7f0c00aa + public const int Base_AlertDialog_AppCompat_Light = 2131493034; + + // aapt resource value: 0x7f0c00ab + public const int Base_Animation_AppCompat_Dialog = 2131493035; + + // aapt resource value: 0x7f0c00ac + public const int Base_Animation_AppCompat_DropDownUp = 2131493036; + + // aapt resource value: 0x7f0c00ad + public const int Base_Animation_AppCompat_Tooltip = 2131493037; + + // aapt resource value: 0x7f0c000c + public const int Base_CardView = 2131492876; + + // aapt resource value: 0x7f0c00ae + public const int Base_DialogWindowTitle_AppCompat = 2131493038; + + // aapt resource value: 0x7f0c00af + public const int Base_DialogWindowTitleBackground_AppCompat = 2131493039; + + // aapt resource value: 0x7f0c0048 + public const int Base_TextAppearance_AppCompat = 2131492936; + + // aapt resource value: 0x7f0c0049 + public const int Base_TextAppearance_AppCompat_Body1 = 2131492937; + + // aapt resource value: 0x7f0c004a + public const int Base_TextAppearance_AppCompat_Body2 = 2131492938; + + // aapt resource value: 0x7f0c0036 + public const int Base_TextAppearance_AppCompat_Button = 2131492918; + + // aapt resource value: 0x7f0c004b + public const int Base_TextAppearance_AppCompat_Caption = 2131492939; + + // aapt resource value: 0x7f0c004c + public const int Base_TextAppearance_AppCompat_Display1 = 2131492940; + + // aapt resource value: 0x7f0c004d + public const int Base_TextAppearance_AppCompat_Display2 = 2131492941; + + // aapt resource value: 0x7f0c004e + public const int Base_TextAppearance_AppCompat_Display3 = 2131492942; + + // aapt resource value: 0x7f0c004f + public const int Base_TextAppearance_AppCompat_Display4 = 2131492943; + + // aapt resource value: 0x7f0c0050 + public const int Base_TextAppearance_AppCompat_Headline = 2131492944; + + // aapt resource value: 0x7f0c001a + public const int Base_TextAppearance_AppCompat_Inverse = 2131492890; + + // aapt resource value: 0x7f0c0051 + public const int Base_TextAppearance_AppCompat_Large = 2131492945; + + // aapt resource value: 0x7f0c001b + public const int Base_TextAppearance_AppCompat_Large_Inverse = 2131492891; + + // aapt resource value: 0x7f0c0052 + public const int Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = 2131492946; + + // aapt resource value: 0x7f0c0053 + public const int Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = 2131492947; + + // aapt resource value: 0x7f0c0054 + public const int Base_TextAppearance_AppCompat_Medium = 2131492948; + + // aapt resource value: 0x7f0c001c + public const int Base_TextAppearance_AppCompat_Medium_Inverse = 2131492892; + + // aapt resource value: 0x7f0c0055 + public const int Base_TextAppearance_AppCompat_Menu = 2131492949; + + // aapt resource value: 0x7f0c00b0 + public const int Base_TextAppearance_AppCompat_SearchResult = 2131493040; + + // aapt resource value: 0x7f0c0056 + public const int Base_TextAppearance_AppCompat_SearchResult_Subtitle = 2131492950; + + // aapt resource value: 0x7f0c0057 + public const int Base_TextAppearance_AppCompat_SearchResult_Title = 2131492951; + + // aapt resource value: 0x7f0c0058 + public const int Base_TextAppearance_AppCompat_Small = 2131492952; + + // aapt resource value: 0x7f0c001d + public const int Base_TextAppearance_AppCompat_Small_Inverse = 2131492893; + + // aapt resource value: 0x7f0c0059 + public const int Base_TextAppearance_AppCompat_Subhead = 2131492953; + + // aapt resource value: 0x7f0c001e + public const int Base_TextAppearance_AppCompat_Subhead_Inverse = 2131492894; + + // aapt resource value: 0x7f0c005a + public const int Base_TextAppearance_AppCompat_Title = 2131492954; + + // aapt resource value: 0x7f0c001f + public const int Base_TextAppearance_AppCompat_Title_Inverse = 2131492895; + + // aapt resource value: 0x7f0c00b1 + public const int Base_TextAppearance_AppCompat_Tooltip = 2131493041; + + // aapt resource value: 0x7f0c0095 + public const int Base_TextAppearance_AppCompat_Widget_ActionBar_Menu = 2131493013; + + // aapt resource value: 0x7f0c005b + public const int Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle = 2131492955; + + // aapt resource value: 0x7f0c005c + public const int Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = 2131492956; + + // aapt resource value: 0x7f0c005d + public const int Base_TextAppearance_AppCompat_Widget_ActionBar_Title = 2131492957; + + // aapt resource value: 0x7f0c005e + public const int Base_TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = 2131492958; + + // aapt resource value: 0x7f0c005f + public const int Base_TextAppearance_AppCompat_Widget_ActionMode_Subtitle = 2131492959; + + // aapt resource value: 0x7f0c0060 + public const int Base_TextAppearance_AppCompat_Widget_ActionMode_Title = 2131492960; + + // aapt resource value: 0x7f0c0061 + public const int Base_TextAppearance_AppCompat_Widget_Button = 2131492961; + + // aapt resource value: 0x7f0c009c + public const int Base_TextAppearance_AppCompat_Widget_Button_Borderless_Colored = 2131493020; + + // aapt resource value: 0x7f0c009d + public const int Base_TextAppearance_AppCompat_Widget_Button_Colored = 2131493021; + + // aapt resource value: 0x7f0c0096 + public const int Base_TextAppearance_AppCompat_Widget_Button_Inverse = 2131493014; + + // aapt resource value: 0x7f0c00b2 + public const int Base_TextAppearance_AppCompat_Widget_DropDownItem = 2131493042; + + // aapt resource value: 0x7f0c0062 + public const int Base_TextAppearance_AppCompat_Widget_PopupMenu_Header = 2131492962; + + // aapt resource value: 0x7f0c0063 + public const int Base_TextAppearance_AppCompat_Widget_PopupMenu_Large = 2131492963; + + // aapt resource value: 0x7f0c0064 + public const int Base_TextAppearance_AppCompat_Widget_PopupMenu_Small = 2131492964; + + // aapt resource value: 0x7f0c0065 + public const int Base_TextAppearance_AppCompat_Widget_Switch = 2131492965; + + // aapt resource value: 0x7f0c0066 + public const int Base_TextAppearance_AppCompat_Widget_TextView_SpinnerItem = 2131492966; + + // aapt resource value: 0x7f0c00b3 + public const int Base_TextAppearance_Widget_AppCompat_ExpandedMenu_Item = 2131493043; + + // aapt resource value: 0x7f0c0067 + public const int Base_TextAppearance_Widget_AppCompat_Toolbar_Subtitle = 2131492967; + + // aapt resource value: 0x7f0c0068 + public const int Base_TextAppearance_Widget_AppCompat_Toolbar_Title = 2131492968; + + // aapt resource value: 0x7f0c0069 + public const int Base_Theme_AppCompat = 2131492969; + + // aapt resource value: 0x7f0c00b4 + public const int Base_Theme_AppCompat_CompactMenu = 2131493044; + + // aapt resource value: 0x7f0c0020 + public const int Base_Theme_AppCompat_Dialog = 2131492896; + + // aapt resource value: 0x7f0c0021 + public const int Base_Theme_AppCompat_Dialog_Alert = 2131492897; + + // aapt resource value: 0x7f0c00b5 + public const int Base_Theme_AppCompat_Dialog_FixedSize = 2131493045; + + // aapt resource value: 0x7f0c0022 + public const int Base_Theme_AppCompat_Dialog_MinWidth = 2131492898; + + // aapt resource value: 0x7f0c0010 + public const int Base_Theme_AppCompat_DialogWhenLarge = 2131492880; + + // aapt resource value: 0x7f0c006a + public const int Base_Theme_AppCompat_Light = 2131492970; + + // aapt resource value: 0x7f0c00b6 + public const int Base_Theme_AppCompat_Light_DarkActionBar = 2131493046; + + // aapt resource value: 0x7f0c0023 + public const int Base_Theme_AppCompat_Light_Dialog = 2131492899; + + // aapt resource value: 0x7f0c0024 + public const int Base_Theme_AppCompat_Light_Dialog_Alert = 2131492900; + + // aapt resource value: 0x7f0c00b7 + public const int Base_Theme_AppCompat_Light_Dialog_FixedSize = 2131493047; + + // aapt resource value: 0x7f0c0025 + public const int Base_Theme_AppCompat_Light_Dialog_MinWidth = 2131492901; + + // aapt resource value: 0x7f0c0011 + public const int Base_Theme_AppCompat_Light_DialogWhenLarge = 2131492881; + + // aapt resource value: 0x7f0c00b8 + public const int Base_ThemeOverlay_AppCompat = 2131493048; + + // aapt resource value: 0x7f0c00b9 + public const int Base_ThemeOverlay_AppCompat_ActionBar = 2131493049; + + // aapt resource value: 0x7f0c00ba + public const int Base_ThemeOverlay_AppCompat_Dark = 2131493050; + + // aapt resource value: 0x7f0c00bb + public const int Base_ThemeOverlay_AppCompat_Dark_ActionBar = 2131493051; + + // aapt resource value: 0x7f0c0026 + public const int Base_ThemeOverlay_AppCompat_Dialog = 2131492902; + + // aapt resource value: 0x7f0c0027 + public const int Base_ThemeOverlay_AppCompat_Dialog_Alert = 2131492903; + + // aapt resource value: 0x7f0c00bc + public const int Base_ThemeOverlay_AppCompat_Light = 2131493052; + + // aapt resource value: 0x7f0c0028 + public const int Base_V11_Theme_AppCompat_Dialog = 2131492904; + + // aapt resource value: 0x7f0c0029 + public const int Base_V11_Theme_AppCompat_Light_Dialog = 2131492905; + + // aapt resource value: 0x7f0c002a + public const int Base_V11_ThemeOverlay_AppCompat_Dialog = 2131492906; + + // aapt resource value: 0x7f0c0032 + public const int Base_V12_Widget_AppCompat_AutoCompleteTextView = 2131492914; + + // aapt resource value: 0x7f0c0033 + public const int Base_V12_Widget_AppCompat_EditText = 2131492915; + + // aapt resource value: 0x7f0c016f + public const int Base_V14_Widget_Design_AppBarLayout = 2131493231; + + // aapt resource value: 0x7f0c006b + public const int Base_V21_Theme_AppCompat = 2131492971; + + // aapt resource value: 0x7f0c006c + public const int Base_V21_Theme_AppCompat_Dialog = 2131492972; + + // aapt resource value: 0x7f0c006d + public const int Base_V21_Theme_AppCompat_Light = 2131492973; + + // aapt resource value: 0x7f0c006e + public const int Base_V21_Theme_AppCompat_Light_Dialog = 2131492974; + + // aapt resource value: 0x7f0c006f + public const int Base_V21_ThemeOverlay_AppCompat_Dialog = 2131492975; + + // aapt resource value: 0x7f0c016b + public const int Base_V21_Widget_Design_AppBarLayout = 2131493227; + + // aapt resource value: 0x7f0c0093 + public const int Base_V22_Theme_AppCompat = 2131493011; + + // aapt resource value: 0x7f0c0094 + public const int Base_V22_Theme_AppCompat_Light = 2131493012; + + // aapt resource value: 0x7f0c0097 + public const int Base_V23_Theme_AppCompat = 2131493015; + + // aapt resource value: 0x7f0c0098 + public const int Base_V23_Theme_AppCompat_Light = 2131493016; + + // aapt resource value: 0x7f0c00a0 + public const int Base_V26_Theme_AppCompat = 2131493024; + + // aapt resource value: 0x7f0c00a1 + public const int Base_V26_Theme_AppCompat_Light = 2131493025; + + // aapt resource value: 0x7f0c00a2 + public const int Base_V26_Widget_AppCompat_Toolbar = 2131493026; + + // aapt resource value: 0x7f0c016d + public const int Base_V26_Widget_Design_AppBarLayout = 2131493229; + + // aapt resource value: 0x7f0c00bd + public const int Base_V7_Theme_AppCompat = 2131493053; + + // aapt resource value: 0x7f0c00be + public const int Base_V7_Theme_AppCompat_Dialog = 2131493054; + + // aapt resource value: 0x7f0c00bf + public const int Base_V7_Theme_AppCompat_Light = 2131493055; + + // aapt resource value: 0x7f0c00c0 + public const int Base_V7_Theme_AppCompat_Light_Dialog = 2131493056; + + // aapt resource value: 0x7f0c00c1 + public const int Base_V7_ThemeOverlay_AppCompat_Dialog = 2131493057; + + // aapt resource value: 0x7f0c00c2 + public const int Base_V7_Widget_AppCompat_AutoCompleteTextView = 2131493058; + + // aapt resource value: 0x7f0c00c3 + public const int Base_V7_Widget_AppCompat_EditText = 2131493059; + + // aapt resource value: 0x7f0c00c4 + public const int Base_V7_Widget_AppCompat_Toolbar = 2131493060; + + // aapt resource value: 0x7f0c00c5 + public const int Base_Widget_AppCompat_ActionBar = 2131493061; + + // aapt resource value: 0x7f0c00c6 + public const int Base_Widget_AppCompat_ActionBar_Solid = 2131493062; + + // aapt resource value: 0x7f0c00c7 + public const int Base_Widget_AppCompat_ActionBar_TabBar = 2131493063; + + // aapt resource value: 0x7f0c0070 + public const int Base_Widget_AppCompat_ActionBar_TabText = 2131492976; + + // aapt resource value: 0x7f0c0071 + public const int Base_Widget_AppCompat_ActionBar_TabView = 2131492977; + + // aapt resource value: 0x7f0c0072 + public const int Base_Widget_AppCompat_ActionButton = 2131492978; + + // aapt resource value: 0x7f0c0073 + public const int Base_Widget_AppCompat_ActionButton_CloseMode = 2131492979; + + // aapt resource value: 0x7f0c0074 + public const int Base_Widget_AppCompat_ActionButton_Overflow = 2131492980; + + // aapt resource value: 0x7f0c00c8 + public const int Base_Widget_AppCompat_ActionMode = 2131493064; + + // aapt resource value: 0x7f0c00c9 + public const int Base_Widget_AppCompat_ActivityChooserView = 2131493065; + + // aapt resource value: 0x7f0c0034 + public const int Base_Widget_AppCompat_AutoCompleteTextView = 2131492916; + + // aapt resource value: 0x7f0c0075 + public const int Base_Widget_AppCompat_Button = 2131492981; + + // aapt resource value: 0x7f0c0076 + public const int Base_Widget_AppCompat_Button_Borderless = 2131492982; + + // aapt resource value: 0x7f0c0077 + public const int Base_Widget_AppCompat_Button_Borderless_Colored = 2131492983; + + // aapt resource value: 0x7f0c00ca + public const int Base_Widget_AppCompat_Button_ButtonBar_AlertDialog = 2131493066; + + // aapt resource value: 0x7f0c0099 + public const int Base_Widget_AppCompat_Button_Colored = 2131493017; + + // aapt resource value: 0x7f0c0078 + public const int Base_Widget_AppCompat_Button_Small = 2131492984; + + // aapt resource value: 0x7f0c0079 + public const int Base_Widget_AppCompat_ButtonBar = 2131492985; + + // aapt resource value: 0x7f0c00cb + public const int Base_Widget_AppCompat_ButtonBar_AlertDialog = 2131493067; + + // aapt resource value: 0x7f0c007a + public const int Base_Widget_AppCompat_CompoundButton_CheckBox = 2131492986; + + // aapt resource value: 0x7f0c007b + public const int Base_Widget_AppCompat_CompoundButton_RadioButton = 2131492987; + + // aapt resource value: 0x7f0c00cc + public const int Base_Widget_AppCompat_CompoundButton_Switch = 2131493068; + + // aapt resource value: 0x7f0c000f + public const int Base_Widget_AppCompat_DrawerArrowToggle = 2131492879; + + // aapt resource value: 0x7f0c00cd + public const int Base_Widget_AppCompat_DrawerArrowToggle_Common = 2131493069; + + // aapt resource value: 0x7f0c007c + public const int Base_Widget_AppCompat_DropDownItem_Spinner = 2131492988; + + // aapt resource value: 0x7f0c0035 + public const int Base_Widget_AppCompat_EditText = 2131492917; + + // aapt resource value: 0x7f0c007d + public const int Base_Widget_AppCompat_ImageButton = 2131492989; + + // aapt resource value: 0x7f0c00ce + public const int Base_Widget_AppCompat_Light_ActionBar = 2131493070; + + // aapt resource value: 0x7f0c00cf + public const int Base_Widget_AppCompat_Light_ActionBar_Solid = 2131493071; + + // aapt resource value: 0x7f0c00d0 + public const int Base_Widget_AppCompat_Light_ActionBar_TabBar = 2131493072; + + // aapt resource value: 0x7f0c007e + public const int Base_Widget_AppCompat_Light_ActionBar_TabText = 2131492990; + + // aapt resource value: 0x7f0c007f + public const int Base_Widget_AppCompat_Light_ActionBar_TabText_Inverse = 2131492991; + + // aapt resource value: 0x7f0c0080 + public const int Base_Widget_AppCompat_Light_ActionBar_TabView = 2131492992; + + // aapt resource value: 0x7f0c0081 + public const int Base_Widget_AppCompat_Light_PopupMenu = 2131492993; + + // aapt resource value: 0x7f0c0082 + public const int Base_Widget_AppCompat_Light_PopupMenu_Overflow = 2131492994; + + // aapt resource value: 0x7f0c00d1 + public const int Base_Widget_AppCompat_ListMenuView = 2131493073; + + // aapt resource value: 0x7f0c0083 + public const int Base_Widget_AppCompat_ListPopupWindow = 2131492995; + + // aapt resource value: 0x7f0c0084 + public const int Base_Widget_AppCompat_ListView = 2131492996; + + // aapt resource value: 0x7f0c0085 + public const int Base_Widget_AppCompat_ListView_DropDown = 2131492997; + + // aapt resource value: 0x7f0c0086 + public const int Base_Widget_AppCompat_ListView_Menu = 2131492998; + + // aapt resource value: 0x7f0c0087 + public const int Base_Widget_AppCompat_PopupMenu = 2131492999; + + // aapt resource value: 0x7f0c0088 + public const int Base_Widget_AppCompat_PopupMenu_Overflow = 2131493000; + + // aapt resource value: 0x7f0c00d2 + public const int Base_Widget_AppCompat_PopupWindow = 2131493074; + + // aapt resource value: 0x7f0c002b + public const int Base_Widget_AppCompat_ProgressBar = 2131492907; + + // aapt resource value: 0x7f0c002c + public const int Base_Widget_AppCompat_ProgressBar_Horizontal = 2131492908; + + // aapt resource value: 0x7f0c0089 + public const int Base_Widget_AppCompat_RatingBar = 2131493001; + + // aapt resource value: 0x7f0c009a + public const int Base_Widget_AppCompat_RatingBar_Indicator = 2131493018; + + // aapt resource value: 0x7f0c009b + public const int Base_Widget_AppCompat_RatingBar_Small = 2131493019; + + // aapt resource value: 0x7f0c00d3 + public const int Base_Widget_AppCompat_SearchView = 2131493075; + + // aapt resource value: 0x7f0c00d4 + public const int Base_Widget_AppCompat_SearchView_ActionBar = 2131493076; + + // aapt resource value: 0x7f0c008a + public const int Base_Widget_AppCompat_SeekBar = 2131493002; + + // aapt resource value: 0x7f0c00d5 + public const int Base_Widget_AppCompat_SeekBar_Discrete = 2131493077; + + // aapt resource value: 0x7f0c008b + public const int Base_Widget_AppCompat_Spinner = 2131493003; + + // aapt resource value: 0x7f0c0012 + public const int Base_Widget_AppCompat_Spinner_Underlined = 2131492882; + + // aapt resource value: 0x7f0c008c + public const int Base_Widget_AppCompat_TextView_SpinnerItem = 2131493004; + + // aapt resource value: 0x7f0c00a3 + public const int Base_Widget_AppCompat_Toolbar = 2131493027; + + // aapt resource value: 0x7f0c008d + public const int Base_Widget_AppCompat_Toolbar_Button_Navigation = 2131493005; + + // aapt resource value: 0x7f0c016c + public const int Base_Widget_Design_AppBarLayout = 2131493228; + + // aapt resource value: 0x7f0c0170 + public const int Base_Widget_Design_TabLayout = 2131493232; + + // aapt resource value: 0x7f0c000b + public const int CardView = 2131492875; + + // aapt resource value: 0x7f0c000d + public const int CardView_Dark = 2131492877; + + // aapt resource value: 0x7f0c000e + public const int CardView_Light = 2131492878; + + // aapt resource value: 0x7f0c002d + public const int Platform_AppCompat = 2131492909; + + // aapt resource value: 0x7f0c002e + public const int Platform_AppCompat_Light = 2131492910; + + // aapt resource value: 0x7f0c008e + public const int Platform_ThemeOverlay_AppCompat = 2131493006; + + // aapt resource value: 0x7f0c008f + public const int Platform_ThemeOverlay_AppCompat_Dark = 2131493007; + + // aapt resource value: 0x7f0c0090 + public const int Platform_ThemeOverlay_AppCompat_Light = 2131493008; + + // aapt resource value: 0x7f0c002f + public const int Platform_V11_AppCompat = 2131492911; + + // aapt resource value: 0x7f0c0030 + public const int Platform_V11_AppCompat_Light = 2131492912; + + // aapt resource value: 0x7f0c0037 + public const int Platform_V14_AppCompat = 2131492919; + + // aapt resource value: 0x7f0c0038 + public const int Platform_V14_AppCompat_Light = 2131492920; + + // aapt resource value: 0x7f0c0091 + public const int Platform_V21_AppCompat = 2131493009; + + // aapt resource value: 0x7f0c0092 + public const int Platform_V21_AppCompat_Light = 2131493010; + + // aapt resource value: 0x7f0c009e + public const int Platform_V25_AppCompat = 2131493022; + + // aapt resource value: 0x7f0c009f + public const int Platform_V25_AppCompat_Light = 2131493023; + + // aapt resource value: 0x7f0c0031 + public const int Platform_Widget_AppCompat_Spinner = 2131492913; + + // aapt resource value: 0x7f0c003a + public const int RtlOverlay_DialogWindowTitle_AppCompat = 2131492922; + + // aapt resource value: 0x7f0c003b + public const int RtlOverlay_Widget_AppCompat_ActionBar_TitleItem = 2131492923; + + // aapt resource value: 0x7f0c003c + public const int RtlOverlay_Widget_AppCompat_DialogTitle_Icon = 2131492924; + + // aapt resource value: 0x7f0c003d + public const int RtlOverlay_Widget_AppCompat_PopupMenuItem = 2131492925; + + // aapt resource value: 0x7f0c003e + public const int RtlOverlay_Widget_AppCompat_PopupMenuItem_InternalGroup = 2131492926; + + // aapt resource value: 0x7f0c003f + public const int RtlOverlay_Widget_AppCompat_PopupMenuItem_Text = 2131492927; + + // aapt resource value: 0x7f0c0040 + public const int RtlOverlay_Widget_AppCompat_Search_DropDown = 2131492928; + + // aapt resource value: 0x7f0c0041 + public const int RtlOverlay_Widget_AppCompat_Search_DropDown_Icon1 = 2131492929; + + // aapt resource value: 0x7f0c0042 + public const int RtlOverlay_Widget_AppCompat_Search_DropDown_Icon2 = 2131492930; + + // aapt resource value: 0x7f0c0043 + public const int RtlOverlay_Widget_AppCompat_Search_DropDown_Query = 2131492931; + + // aapt resource value: 0x7f0c0044 + public const int RtlOverlay_Widget_AppCompat_Search_DropDown_Text = 2131492932; + + // aapt resource value: 0x7f0c0045 + public const int RtlOverlay_Widget_AppCompat_SearchView_MagIcon = 2131492933; + + // aapt resource value: 0x7f0c0046 + public const int RtlUnderlay_Widget_AppCompat_ActionButton = 2131492934; + + // aapt resource value: 0x7f0c0047 + public const int RtlUnderlay_Widget_AppCompat_ActionButton_Overflow = 2131492935; + + // aapt resource value: 0x7f0c00d6 + public const int TextAppearance_AppCompat = 2131493078; + + // aapt resource value: 0x7f0c00d7 + public const int TextAppearance_AppCompat_Body1 = 2131493079; + + // aapt resource value: 0x7f0c00d8 + public const int TextAppearance_AppCompat_Body2 = 2131493080; + + // aapt resource value: 0x7f0c00d9 + public const int TextAppearance_AppCompat_Button = 2131493081; + + // aapt resource value: 0x7f0c00da + public const int TextAppearance_AppCompat_Caption = 2131493082; + + // aapt resource value: 0x7f0c00db + public const int TextAppearance_AppCompat_Display1 = 2131493083; + + // aapt resource value: 0x7f0c00dc + public const int TextAppearance_AppCompat_Display2 = 2131493084; + + // aapt resource value: 0x7f0c00dd + public const int TextAppearance_AppCompat_Display3 = 2131493085; + + // aapt resource value: 0x7f0c00de + public const int TextAppearance_AppCompat_Display4 = 2131493086; + + // aapt resource value: 0x7f0c00df + public const int TextAppearance_AppCompat_Headline = 2131493087; + + // aapt resource value: 0x7f0c00e0 + public const int TextAppearance_AppCompat_Inverse = 2131493088; + + // aapt resource value: 0x7f0c00e1 + public const int TextAppearance_AppCompat_Large = 2131493089; + + // aapt resource value: 0x7f0c00e2 + public const int TextAppearance_AppCompat_Large_Inverse = 2131493090; + + // aapt resource value: 0x7f0c00e3 + public const int TextAppearance_AppCompat_Light_SearchResult_Subtitle = 2131493091; + + // aapt resource value: 0x7f0c00e4 + public const int TextAppearance_AppCompat_Light_SearchResult_Title = 2131493092; + + // aapt resource value: 0x7f0c00e5 + public const int TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = 2131493093; + + // aapt resource value: 0x7f0c00e6 + public const int TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = 2131493094; + + // aapt resource value: 0x7f0c00e7 + public const int TextAppearance_AppCompat_Medium = 2131493095; + + // aapt resource value: 0x7f0c00e8 + public const int TextAppearance_AppCompat_Medium_Inverse = 2131493096; + + // aapt resource value: 0x7f0c00e9 + public const int TextAppearance_AppCompat_Menu = 2131493097; + + // aapt resource value: 0x7f0c00ea + public const int TextAppearance_AppCompat_SearchResult_Subtitle = 2131493098; + + // aapt resource value: 0x7f0c00eb + public const int TextAppearance_AppCompat_SearchResult_Title = 2131493099; + + // aapt resource value: 0x7f0c00ec + public const int TextAppearance_AppCompat_Small = 2131493100; + + // aapt resource value: 0x7f0c00ed + public const int TextAppearance_AppCompat_Small_Inverse = 2131493101; + + // aapt resource value: 0x7f0c00ee + public const int TextAppearance_AppCompat_Subhead = 2131493102; + + // aapt resource value: 0x7f0c00ef + public const int TextAppearance_AppCompat_Subhead_Inverse = 2131493103; + + // aapt resource value: 0x7f0c00f0 + public const int TextAppearance_AppCompat_Title = 2131493104; + + // aapt resource value: 0x7f0c00f1 + public const int TextAppearance_AppCompat_Title_Inverse = 2131493105; + + // aapt resource value: 0x7f0c0039 + public const int TextAppearance_AppCompat_Tooltip = 2131492921; + + // aapt resource value: 0x7f0c00f2 + public const int TextAppearance_AppCompat_Widget_ActionBar_Menu = 2131493106; + + // aapt resource value: 0x7f0c00f3 + public const int TextAppearance_AppCompat_Widget_ActionBar_Subtitle = 2131493107; + + // aapt resource value: 0x7f0c00f4 + public const int TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = 2131493108; + + // aapt resource value: 0x7f0c00f5 + public const int TextAppearance_AppCompat_Widget_ActionBar_Title = 2131493109; + + // aapt resource value: 0x7f0c00f6 + public const int TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = 2131493110; + + // aapt resource value: 0x7f0c00f7 + public const int TextAppearance_AppCompat_Widget_ActionMode_Subtitle = 2131493111; + + // aapt resource value: 0x7f0c00f8 + public const int TextAppearance_AppCompat_Widget_ActionMode_Subtitle_Inverse = 2131493112; + + // aapt resource value: 0x7f0c00f9 + public const int TextAppearance_AppCompat_Widget_ActionMode_Title = 2131493113; + + // aapt resource value: 0x7f0c00fa + public const int TextAppearance_AppCompat_Widget_ActionMode_Title_Inverse = 2131493114; + + // aapt resource value: 0x7f0c00fb + public const int TextAppearance_AppCompat_Widget_Button = 2131493115; + + // aapt resource value: 0x7f0c00fc + public const int TextAppearance_AppCompat_Widget_Button_Borderless_Colored = 2131493116; + + // aapt resource value: 0x7f0c00fd + public const int TextAppearance_AppCompat_Widget_Button_Colored = 2131493117; + + // aapt resource value: 0x7f0c00fe + public const int TextAppearance_AppCompat_Widget_Button_Inverse = 2131493118; + + // aapt resource value: 0x7f0c00ff + public const int TextAppearance_AppCompat_Widget_DropDownItem = 2131493119; + + // aapt resource value: 0x7f0c0100 + public const int TextAppearance_AppCompat_Widget_PopupMenu_Header = 2131493120; + + // aapt resource value: 0x7f0c0101 + public const int TextAppearance_AppCompat_Widget_PopupMenu_Large = 2131493121; + + // aapt resource value: 0x7f0c0102 + public const int TextAppearance_AppCompat_Widget_PopupMenu_Small = 2131493122; + + // aapt resource value: 0x7f0c0103 + public const int TextAppearance_AppCompat_Widget_Switch = 2131493123; + + // aapt resource value: 0x7f0c0104 + public const int TextAppearance_AppCompat_Widget_TextView_SpinnerItem = 2131493124; + + // aapt resource value: 0x7f0c0188 + public const int TextAppearance_Compat_Notification = 2131493256; + + // aapt resource value: 0x7f0c0189 + public const int TextAppearance_Compat_Notification_Info = 2131493257; + + // aapt resource value: 0x7f0c0165 + public const int TextAppearance_Compat_Notification_Info_Media = 2131493221; + + // aapt resource value: 0x7f0c018e + public const int TextAppearance_Compat_Notification_Line2 = 2131493262; + + // aapt resource value: 0x7f0c0169 + public const int TextAppearance_Compat_Notification_Line2_Media = 2131493225; + + // aapt resource value: 0x7f0c0166 + public const int TextAppearance_Compat_Notification_Media = 2131493222; + + // aapt resource value: 0x7f0c018a + public const int TextAppearance_Compat_Notification_Time = 2131493258; + + // aapt resource value: 0x7f0c0167 + public const int TextAppearance_Compat_Notification_Time_Media = 2131493223; + + // aapt resource value: 0x7f0c018b + public const int TextAppearance_Compat_Notification_Title = 2131493259; + + // aapt resource value: 0x7f0c0168 + public const int TextAppearance_Compat_Notification_Title_Media = 2131493224; + + // aapt resource value: 0x7f0c0171 + public const int TextAppearance_Design_CollapsingToolbar_Expanded = 2131493233; + + // aapt resource value: 0x7f0c0172 + public const int TextAppearance_Design_Counter = 2131493234; + + // aapt resource value: 0x7f0c0173 + public const int TextAppearance_Design_Counter_Overflow = 2131493235; + + // aapt resource value: 0x7f0c0174 + public const int TextAppearance_Design_Error = 2131493236; + + // aapt resource value: 0x7f0c0175 + public const int TextAppearance_Design_Hint = 2131493237; + + // aapt resource value: 0x7f0c0176 + public const int TextAppearance_Design_Snackbar_Message = 2131493238; + + // aapt resource value: 0x7f0c0177 + public const int TextAppearance_Design_Tab = 2131493239; + + // aapt resource value: 0x7f0c0000 + public const int TextAppearance_MediaRouter_PrimaryText = 2131492864; + + // aapt resource value: 0x7f0c0001 + public const int TextAppearance_MediaRouter_SecondaryText = 2131492865; + + // aapt resource value: 0x7f0c0002 + public const int TextAppearance_MediaRouter_Title = 2131492866; + + // aapt resource value: 0x7f0c0105 + public const int TextAppearance_Widget_AppCompat_ExpandedMenu_Item = 2131493125; + + // aapt resource value: 0x7f0c0106 + public const int TextAppearance_Widget_AppCompat_Toolbar_Subtitle = 2131493126; + + // aapt resource value: 0x7f0c0107 + public const int TextAppearance_Widget_AppCompat_Toolbar_Title = 2131493127; + + // aapt resource value: 0x7f0c0108 + public const int Theme_AppCompat = 2131493128; + + // aapt resource value: 0x7f0c0109 + public const int Theme_AppCompat_CompactMenu = 2131493129; + + // aapt resource value: 0x7f0c0013 + public const int Theme_AppCompat_DayNight = 2131492883; + + // aapt resource value: 0x7f0c0014 + public const int Theme_AppCompat_DayNight_DarkActionBar = 2131492884; + + // aapt resource value: 0x7f0c0015 + public const int Theme_AppCompat_DayNight_Dialog = 2131492885; + + // aapt resource value: 0x7f0c0016 + public const int Theme_AppCompat_DayNight_Dialog_Alert = 2131492886; + + // aapt resource value: 0x7f0c0017 + public const int Theme_AppCompat_DayNight_Dialog_MinWidth = 2131492887; + + // aapt resource value: 0x7f0c0018 + public const int Theme_AppCompat_DayNight_DialogWhenLarge = 2131492888; + + // aapt resource value: 0x7f0c0019 + public const int Theme_AppCompat_DayNight_NoActionBar = 2131492889; + + // aapt resource value: 0x7f0c010a + public const int Theme_AppCompat_Dialog = 2131493130; + + // aapt resource value: 0x7f0c010b + public const int Theme_AppCompat_Dialog_Alert = 2131493131; + + // aapt resource value: 0x7f0c010c + public const int Theme_AppCompat_Dialog_MinWidth = 2131493132; + + // aapt resource value: 0x7f0c010d + public const int Theme_AppCompat_DialogWhenLarge = 2131493133; + + // aapt resource value: 0x7f0c010e + public const int Theme_AppCompat_Light = 2131493134; + + // aapt resource value: 0x7f0c010f + public const int Theme_AppCompat_Light_DarkActionBar = 2131493135; + + // aapt resource value: 0x7f0c0110 + public const int Theme_AppCompat_Light_Dialog = 2131493136; + + // aapt resource value: 0x7f0c0111 + public const int Theme_AppCompat_Light_Dialog_Alert = 2131493137; + + // aapt resource value: 0x7f0c0112 + public const int Theme_AppCompat_Light_Dialog_MinWidth = 2131493138; + + // aapt resource value: 0x7f0c0113 + public const int Theme_AppCompat_Light_DialogWhenLarge = 2131493139; + + // aapt resource value: 0x7f0c0114 + public const int Theme_AppCompat_Light_NoActionBar = 2131493140; + + // aapt resource value: 0x7f0c0115 + public const int Theme_AppCompat_NoActionBar = 2131493141; + + // aapt resource value: 0x7f0c0178 + public const int Theme_Design = 2131493240; + + // aapt resource value: 0x7f0c0179 + public const int Theme_Design_BottomSheetDialog = 2131493241; + + // aapt resource value: 0x7f0c017a + public const int Theme_Design_Light = 2131493242; + + // aapt resource value: 0x7f0c017b + public const int Theme_Design_Light_BottomSheetDialog = 2131493243; + + // aapt resource value: 0x7f0c017c + public const int Theme_Design_Light_NoActionBar = 2131493244; + + // aapt resource value: 0x7f0c017d + public const int Theme_Design_NoActionBar = 2131493245; + + // aapt resource value: 0x7f0c0003 + public const int Theme_MediaRouter = 2131492867; + + // aapt resource value: 0x7f0c0004 + public const int Theme_MediaRouter_Light = 2131492868; + + // aapt resource value: 0x7f0c0005 + public const int Theme_MediaRouter_Light_DarkControlPanel = 2131492869; + + // aapt resource value: 0x7f0c0006 + public const int Theme_MediaRouter_LightControlPanel = 2131492870; + + // aapt resource value: 0x7f0c0116 + public const int ThemeOverlay_AppCompat = 2131493142; + + // aapt resource value: 0x7f0c0117 + public const int ThemeOverlay_AppCompat_ActionBar = 2131493143; + + // aapt resource value: 0x7f0c0118 + public const int ThemeOverlay_AppCompat_Dark = 2131493144; + + // aapt resource value: 0x7f0c0119 + public const int ThemeOverlay_AppCompat_Dark_ActionBar = 2131493145; + + // aapt resource value: 0x7f0c011a + public const int ThemeOverlay_AppCompat_Dialog = 2131493146; + + // aapt resource value: 0x7f0c011b + public const int ThemeOverlay_AppCompat_Dialog_Alert = 2131493147; + + // aapt resource value: 0x7f0c011c + public const int ThemeOverlay_AppCompat_Light = 2131493148; + + // aapt resource value: 0x7f0c0007 + public const int ThemeOverlay_MediaRouter_Dark = 2131492871; + + // aapt resource value: 0x7f0c0008 + public const int ThemeOverlay_MediaRouter_Light = 2131492872; + + // aapt resource value: 0x7f0c011d + public const int Widget_AppCompat_ActionBar = 2131493149; + + // aapt resource value: 0x7f0c011e + public const int Widget_AppCompat_ActionBar_Solid = 2131493150; + + // aapt resource value: 0x7f0c011f + public const int Widget_AppCompat_ActionBar_TabBar = 2131493151; + + // aapt resource value: 0x7f0c0120 + public const int Widget_AppCompat_ActionBar_TabText = 2131493152; + + // aapt resource value: 0x7f0c0121 + public const int Widget_AppCompat_ActionBar_TabView = 2131493153; + + // aapt resource value: 0x7f0c0122 + public const int Widget_AppCompat_ActionButton = 2131493154; + + // aapt resource value: 0x7f0c0123 + public const int Widget_AppCompat_ActionButton_CloseMode = 2131493155; + + // aapt resource value: 0x7f0c0124 + public const int Widget_AppCompat_ActionButton_Overflow = 2131493156; + + // aapt resource value: 0x7f0c0125 + public const int Widget_AppCompat_ActionMode = 2131493157; + + // aapt resource value: 0x7f0c0126 + public const int Widget_AppCompat_ActivityChooserView = 2131493158; + + // aapt resource value: 0x7f0c0127 + public const int Widget_AppCompat_AutoCompleteTextView = 2131493159; + + // aapt resource value: 0x7f0c0128 + public const int Widget_AppCompat_Button = 2131493160; + + // aapt resource value: 0x7f0c0129 + public const int Widget_AppCompat_Button_Borderless = 2131493161; + + // aapt resource value: 0x7f0c012a + public const int Widget_AppCompat_Button_Borderless_Colored = 2131493162; + + // aapt resource value: 0x7f0c012b + public const int Widget_AppCompat_Button_ButtonBar_AlertDialog = 2131493163; + + // aapt resource value: 0x7f0c012c + public const int Widget_AppCompat_Button_Colored = 2131493164; + + // aapt resource value: 0x7f0c012d + public const int Widget_AppCompat_Button_Small = 2131493165; + + // aapt resource value: 0x7f0c012e + public const int Widget_AppCompat_ButtonBar = 2131493166; + + // aapt resource value: 0x7f0c012f + public const int Widget_AppCompat_ButtonBar_AlertDialog = 2131493167; + + // aapt resource value: 0x7f0c0130 + public const int Widget_AppCompat_CompoundButton_CheckBox = 2131493168; + + // aapt resource value: 0x7f0c0131 + public const int Widget_AppCompat_CompoundButton_RadioButton = 2131493169; + + // aapt resource value: 0x7f0c0132 + public const int Widget_AppCompat_CompoundButton_Switch = 2131493170; + + // aapt resource value: 0x7f0c0133 + public const int Widget_AppCompat_DrawerArrowToggle = 2131493171; + + // aapt resource value: 0x7f0c0134 + public const int Widget_AppCompat_DropDownItem_Spinner = 2131493172; + + // aapt resource value: 0x7f0c0135 + public const int Widget_AppCompat_EditText = 2131493173; + + // aapt resource value: 0x7f0c0136 + public const int Widget_AppCompat_ImageButton = 2131493174; + + // aapt resource value: 0x7f0c0137 + public const int Widget_AppCompat_Light_ActionBar = 2131493175; + + // aapt resource value: 0x7f0c0138 + public const int Widget_AppCompat_Light_ActionBar_Solid = 2131493176; + + // aapt resource value: 0x7f0c0139 + public const int Widget_AppCompat_Light_ActionBar_Solid_Inverse = 2131493177; + + // aapt resource value: 0x7f0c013a + public const int Widget_AppCompat_Light_ActionBar_TabBar = 2131493178; + + // aapt resource value: 0x7f0c013b + public const int Widget_AppCompat_Light_ActionBar_TabBar_Inverse = 2131493179; + + // aapt resource value: 0x7f0c013c + public const int Widget_AppCompat_Light_ActionBar_TabText = 2131493180; + + // aapt resource value: 0x7f0c013d + public const int Widget_AppCompat_Light_ActionBar_TabText_Inverse = 2131493181; + + // aapt resource value: 0x7f0c013e + public const int Widget_AppCompat_Light_ActionBar_TabView = 2131493182; + + // aapt resource value: 0x7f0c013f + public const int Widget_AppCompat_Light_ActionBar_TabView_Inverse = 2131493183; + + // aapt resource value: 0x7f0c0140 + public const int Widget_AppCompat_Light_ActionButton = 2131493184; + + // aapt resource value: 0x7f0c0141 + public const int Widget_AppCompat_Light_ActionButton_CloseMode = 2131493185; + + // aapt resource value: 0x7f0c0142 + public const int Widget_AppCompat_Light_ActionButton_Overflow = 2131493186; + + // aapt resource value: 0x7f0c0143 + public const int Widget_AppCompat_Light_ActionMode_Inverse = 2131493187; + + // aapt resource value: 0x7f0c0144 + public const int Widget_AppCompat_Light_ActivityChooserView = 2131493188; + + // aapt resource value: 0x7f0c0145 + public const int Widget_AppCompat_Light_AutoCompleteTextView = 2131493189; + + // aapt resource value: 0x7f0c0146 + public const int Widget_AppCompat_Light_DropDownItem_Spinner = 2131493190; + + // aapt resource value: 0x7f0c0147 + public const int Widget_AppCompat_Light_ListPopupWindow = 2131493191; + + // aapt resource value: 0x7f0c0148 + public const int Widget_AppCompat_Light_ListView_DropDown = 2131493192; + + // aapt resource value: 0x7f0c0149 + public const int Widget_AppCompat_Light_PopupMenu = 2131493193; + + // aapt resource value: 0x7f0c014a + public const int Widget_AppCompat_Light_PopupMenu_Overflow = 2131493194; + + // aapt resource value: 0x7f0c014b + public const int Widget_AppCompat_Light_SearchView = 2131493195; + + // aapt resource value: 0x7f0c014c + public const int Widget_AppCompat_Light_Spinner_DropDown_ActionBar = 2131493196; + + // aapt resource value: 0x7f0c014d + public const int Widget_AppCompat_ListMenuView = 2131493197; + + // aapt resource value: 0x7f0c014e + public const int Widget_AppCompat_ListPopupWindow = 2131493198; + + // aapt resource value: 0x7f0c014f + public const int Widget_AppCompat_ListView = 2131493199; + + // aapt resource value: 0x7f0c0150 + public const int Widget_AppCompat_ListView_DropDown = 2131493200; + + // aapt resource value: 0x7f0c0151 + public const int Widget_AppCompat_ListView_Menu = 2131493201; + + // aapt resource value: 0x7f0c0152 + public const int Widget_AppCompat_PopupMenu = 2131493202; + + // aapt resource value: 0x7f0c0153 + public const int Widget_AppCompat_PopupMenu_Overflow = 2131493203; + + // aapt resource value: 0x7f0c0154 + public const int Widget_AppCompat_PopupWindow = 2131493204; + + // aapt resource value: 0x7f0c0155 + public const int Widget_AppCompat_ProgressBar = 2131493205; + + // aapt resource value: 0x7f0c0156 + public const int Widget_AppCompat_ProgressBar_Horizontal = 2131493206; + + // aapt resource value: 0x7f0c0157 + public const int Widget_AppCompat_RatingBar = 2131493207; + + // aapt resource value: 0x7f0c0158 + public const int Widget_AppCompat_RatingBar_Indicator = 2131493208; + + // aapt resource value: 0x7f0c0159 + public const int Widget_AppCompat_RatingBar_Small = 2131493209; + + // aapt resource value: 0x7f0c015a + public const int Widget_AppCompat_SearchView = 2131493210; + + // aapt resource value: 0x7f0c015b + public const int Widget_AppCompat_SearchView_ActionBar = 2131493211; + + // aapt resource value: 0x7f0c015c + public const int Widget_AppCompat_SeekBar = 2131493212; + + // aapt resource value: 0x7f0c015d + public const int Widget_AppCompat_SeekBar_Discrete = 2131493213; + + // aapt resource value: 0x7f0c015e + public const int Widget_AppCompat_Spinner = 2131493214; + + // aapt resource value: 0x7f0c015f + public const int Widget_AppCompat_Spinner_DropDown = 2131493215; + + // aapt resource value: 0x7f0c0160 + public const int Widget_AppCompat_Spinner_DropDown_ActionBar = 2131493216; + + // aapt resource value: 0x7f0c0161 + public const int Widget_AppCompat_Spinner_Underlined = 2131493217; + + // aapt resource value: 0x7f0c0162 + public const int Widget_AppCompat_TextView_SpinnerItem = 2131493218; + + // aapt resource value: 0x7f0c0163 + public const int Widget_AppCompat_Toolbar = 2131493219; + + // aapt resource value: 0x7f0c0164 + public const int Widget_AppCompat_Toolbar_Button_Navigation = 2131493220; + + // aapt resource value: 0x7f0c018c + public const int Widget_Compat_NotificationActionContainer = 2131493260; + + // aapt resource value: 0x7f0c018d + public const int Widget_Compat_NotificationActionText = 2131493261; + + // aapt resource value: 0x7f0c017e + public const int Widget_Design_AppBarLayout = 2131493246; + + // aapt resource value: 0x7f0c017f + public const int Widget_Design_BottomNavigationView = 2131493247; + + // aapt resource value: 0x7f0c0180 + public const int Widget_Design_BottomSheet_Modal = 2131493248; + + // aapt resource value: 0x7f0c0181 + public const int Widget_Design_CollapsingToolbar = 2131493249; + + // aapt resource value: 0x7f0c0182 + public const int Widget_Design_CoordinatorLayout = 2131493250; + + // aapt resource value: 0x7f0c0183 + public const int Widget_Design_FloatingActionButton = 2131493251; + + // aapt resource value: 0x7f0c0184 + public const int Widget_Design_NavigationView = 2131493252; + + // aapt resource value: 0x7f0c0185 + public const int Widget_Design_ScrimInsetsFrameLayout = 2131493253; + + // aapt resource value: 0x7f0c0186 + public const int Widget_Design_Snackbar = 2131493254; + + // aapt resource value: 0x7f0c016a + public const int Widget_Design_TabLayout = 2131493226; + + // aapt resource value: 0x7f0c0187 + public const int Widget_Design_TextInputLayout = 2131493255; + + // aapt resource value: 0x7f0c0009 + public const int Widget_MediaRouter_Light_MediaRouteButton = 2131492873; + + // aapt resource value: 0x7f0c000a + public const int Widget_MediaRouter_MediaRouteButton = 2131492874; + + static Style() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Style() + { + } + } + + public partial class Styleable + { + + public static int[] ActionBar = new int[] { + 2130772003, + 2130772005, + 2130772006, + 2130772007, + 2130772008, + 2130772009, + 2130772010, + 2130772011, + 2130772012, + 2130772013, + 2130772014, + 2130772015, + 2130772016, + 2130772017, + 2130772018, + 2130772019, + 2130772020, + 2130772021, + 2130772022, + 2130772023, + 2130772024, + 2130772025, + 2130772026, + 2130772027, + 2130772028, + 2130772029, + 2130772030, + 2130772031, + 2130772101}; + + // aapt resource value: 10 + public const int ActionBar_background = 10; + + // aapt resource value: 12 + public const int ActionBar_backgroundSplit = 12; + + // aapt resource value: 11 + public const int ActionBar_backgroundStacked = 11; + + // aapt resource value: 21 + public const int ActionBar_contentInsetEnd = 21; + + // aapt resource value: 25 + public const int ActionBar_contentInsetEndWithActions = 25; + + // aapt resource value: 22 + public const int ActionBar_contentInsetLeft = 22; + + // aapt resource value: 23 + public const int ActionBar_contentInsetRight = 23; + + // aapt resource value: 20 + public const int ActionBar_contentInsetStart = 20; + + // aapt resource value: 24 + public const int ActionBar_contentInsetStartWithNavigation = 24; + + // aapt resource value: 13 + public const int ActionBar_customNavigationLayout = 13; + + // aapt resource value: 3 + public const int ActionBar_displayOptions = 3; + + // aapt resource value: 9 + public const int ActionBar_divider = 9; + + // aapt resource value: 26 + public const int ActionBar_elevation = 26; + + // aapt resource value: 0 + public const int ActionBar_height = 0; + + // aapt resource value: 19 + public const int ActionBar_hideOnContentScroll = 19; + + // aapt resource value: 28 + public const int ActionBar_homeAsUpIndicator = 28; + + // aapt resource value: 14 + public const int ActionBar_homeLayout = 14; + + // aapt resource value: 7 + public const int ActionBar_icon = 7; + + // aapt resource value: 16 + public const int ActionBar_indeterminateProgressStyle = 16; + + // aapt resource value: 18 + public const int ActionBar_itemPadding = 18; + + // aapt resource value: 8 + public const int ActionBar_logo = 8; + + // aapt resource value: 2 + public const int ActionBar_navigationMode = 2; + + // aapt resource value: 27 + public const int ActionBar_popupTheme = 27; + + // aapt resource value: 17 + public const int ActionBar_progressBarPadding = 17; + + // aapt resource value: 15 + public const int ActionBar_progressBarStyle = 15; + + // aapt resource value: 4 + public const int ActionBar_subtitle = 4; + + // aapt resource value: 6 + public const int ActionBar_subtitleTextStyle = 6; + + // aapt resource value: 1 + public const int ActionBar_title = 1; + + // aapt resource value: 5 + public const int ActionBar_titleTextStyle = 5; + + public static int[] ActionBarLayout = new int[] { + 16842931}; + + // aapt resource value: 0 + public const int ActionBarLayout_android_layout_gravity = 0; + + public static int[] ActionMenuItemView = new int[] { + 16843071}; + + // aapt resource value: 0 + public const int ActionMenuItemView_android_minWidth = 0; + + public static int[] ActionMenuView; + + public static int[] ActionMode = new int[] { + 2130772003, + 2130772009, + 2130772010, + 2130772014, + 2130772016, + 2130772032}; + + // aapt resource value: 3 + public const int ActionMode_background = 3; + + // aapt resource value: 4 + public const int ActionMode_backgroundSplit = 4; + + // aapt resource value: 5 + public const int ActionMode_closeItemLayout = 5; + + // aapt resource value: 0 + public const int ActionMode_height = 0; + + // aapt resource value: 2 + public const int ActionMode_subtitleTextStyle = 2; + + // aapt resource value: 1 + public const int ActionMode_titleTextStyle = 1; + + public static int[] ActivityChooserView = new int[] { + 2130772033, + 2130772034}; + + // aapt resource value: 1 + public const int ActivityChooserView_expandActivityOverflowButtonDrawable = 1; + + // aapt resource value: 0 + public const int ActivityChooserView_initialActivityCount = 0; + + public static int[] AlertDialog = new int[] { + 16842994, + 2130772035, + 2130772036, + 2130772037, + 2130772038, + 2130772039, + 2130772040}; + + // aapt resource value: 0 + public const int AlertDialog_android_layout = 0; + + // aapt resource value: 1 + public const int AlertDialog_buttonPanelSideLayout = 1; + + // aapt resource value: 5 + public const int AlertDialog_listItemLayout = 5; + + // aapt resource value: 2 + public const int AlertDialog_listLayout = 2; + + // aapt resource value: 3 + public const int AlertDialog_multiChoiceItemLayout = 3; + + // aapt resource value: 6 + public const int AlertDialog_showTitle = 6; + + // aapt resource value: 4 + public const int AlertDialog_singleChoiceItemLayout = 4; + + public static int[] AppBarLayout = new int[] { + 16842964, + 16843919, + 16844096, + 2130772030, + 2130772248}; + + // aapt resource value: 0 + public const int AppBarLayout_android_background = 0; + + // aapt resource value: 2 + public const int AppBarLayout_android_keyboardNavigationCluster = 2; + + // aapt resource value: 1 + public const int AppBarLayout_android_touchscreenBlocksFocus = 1; + + // aapt resource value: 3 + public const int AppBarLayout_elevation = 3; + + // aapt resource value: 4 + public const int AppBarLayout_expanded = 4; + + public static int[] AppBarLayoutStates = new int[] { + 2130772249, + 2130772250}; + + // aapt resource value: 0 + public const int AppBarLayoutStates_state_collapsed = 0; + + // aapt resource value: 1 + public const int AppBarLayoutStates_state_collapsible = 1; + + public static int[] AppBarLayout_Layout = new int[] { + 2130772251, + 2130772252}; + + // aapt resource value: 0 + public const int AppBarLayout_Layout_layout_scrollFlags = 0; + + // aapt resource value: 1 + public const int AppBarLayout_Layout_layout_scrollInterpolator = 1; + + public static int[] AppCompatImageView = new int[] { + 16843033, + 2130772041, + 2130772042, + 2130772043}; + + // aapt resource value: 0 + public const int AppCompatImageView_android_src = 0; + + // aapt resource value: 1 + public const int AppCompatImageView_srcCompat = 1; + + // aapt resource value: 2 + public const int AppCompatImageView_tint = 2; + + // aapt resource value: 3 + public const int AppCompatImageView_tintMode = 3; + + public static int[] AppCompatSeekBar = new int[] { + 16843074, + 2130772044, + 2130772045, + 2130772046}; + + // aapt resource value: 0 + public const int AppCompatSeekBar_android_thumb = 0; + + // aapt resource value: 1 + public const int AppCompatSeekBar_tickMark = 1; + + // aapt resource value: 2 + public const int AppCompatSeekBar_tickMarkTint = 2; + + // aapt resource value: 3 + public const int AppCompatSeekBar_tickMarkTintMode = 3; + + public static int[] AppCompatTextHelper = new int[] { + 16842804, + 16843117, + 16843118, + 16843119, + 16843120, + 16843666, + 16843667}; + + // aapt resource value: 2 + public const int AppCompatTextHelper_android_drawableBottom = 2; + + // aapt resource value: 6 + public const int AppCompatTextHelper_android_drawableEnd = 6; + + // aapt resource value: 3 + public const int AppCompatTextHelper_android_drawableLeft = 3; + + // aapt resource value: 4 + public const int AppCompatTextHelper_android_drawableRight = 4; + + // aapt resource value: 5 + public const int AppCompatTextHelper_android_drawableStart = 5; + + // aapt resource value: 1 + public const int AppCompatTextHelper_android_drawableTop = 1; + + // aapt resource value: 0 + public const int AppCompatTextHelper_android_textAppearance = 0; + + public static int[] AppCompatTextView = new int[] { + 16842804, + 2130772047, + 2130772048, + 2130772049, + 2130772050, + 2130772051, + 2130772052, + 2130772053}; + + // aapt resource value: 0 + public const int AppCompatTextView_android_textAppearance = 0; + + // aapt resource value: 6 + public const int AppCompatTextView_autoSizeMaxTextSize = 6; + + // aapt resource value: 5 + public const int AppCompatTextView_autoSizeMinTextSize = 5; + + // aapt resource value: 4 + public const int AppCompatTextView_autoSizePresetSizes = 4; + + // aapt resource value: 3 + public const int AppCompatTextView_autoSizeStepGranularity = 3; + + // aapt resource value: 2 + public const int AppCompatTextView_autoSizeTextType = 2; + + // aapt resource value: 7 + public const int AppCompatTextView_fontFamily = 7; + + // aapt resource value: 1 + public const int AppCompatTextView_textAllCaps = 1; + + public static int[] AppCompatTheme = new int[] { + 16842839, + 16842926, + 2130772054, + 2130772055, + 2130772056, + 2130772057, + 2130772058, + 2130772059, + 2130772060, + 2130772061, + 2130772062, + 2130772063, + 2130772064, + 2130772065, + 2130772066, + 2130772067, + 2130772068, + 2130772069, + 2130772070, + 2130772071, + 2130772072, + 2130772073, + 2130772074, + 2130772075, + 2130772076, + 2130772077, + 2130772078, + 2130772079, + 2130772080, + 2130772081, + 2130772082, + 2130772083, + 2130772084, + 2130772085, + 2130772086, + 2130772087, + 2130772088, + 2130772089, + 2130772090, + 2130772091, + 2130772092, + 2130772093, + 2130772094, + 2130772095, + 2130772096, + 2130772097, + 2130772098, + 2130772099, + 2130772100, + 2130772101, + 2130772102, + 2130772103, + 2130772104, + 2130772105, + 2130772106, + 2130772107, + 2130772108, + 2130772109, + 2130772110, + 2130772111, + 2130772112, + 2130772113, + 2130772114, + 2130772115, + 2130772116, + 2130772117, + 2130772118, + 2130772119, + 2130772120, + 2130772121, + 2130772122, + 2130772123, + 2130772124, + 2130772125, + 2130772126, + 2130772127, + 2130772128, + 2130772129, + 2130772130, + 2130772131, + 2130772132, + 2130772133, + 2130772134, + 2130772135, + 2130772136, + 2130772137, + 2130772138, + 2130772139, + 2130772140, + 2130772141, + 2130772142, + 2130772143, + 2130772144, + 2130772145, + 2130772146, + 2130772147, + 2130772148, + 2130772149, + 2130772150, + 2130772151, + 2130772152, + 2130772153, + 2130772154, + 2130772155, + 2130772156, + 2130772157, + 2130772158, + 2130772159, + 2130772160, + 2130772161, + 2130772162, + 2130772163, + 2130772164, + 2130772165, + 2130772166, + 2130772167, + 2130772168, + 2130772169, + 2130772170}; + + // aapt resource value: 23 + public const int AppCompatTheme_actionBarDivider = 23; + + // aapt resource value: 24 + public const int AppCompatTheme_actionBarItemBackground = 24; + + // aapt resource value: 17 + public const int AppCompatTheme_actionBarPopupTheme = 17; + + // aapt resource value: 22 + public const int AppCompatTheme_actionBarSize = 22; + + // aapt resource value: 19 + public const int AppCompatTheme_actionBarSplitStyle = 19; + + // aapt resource value: 18 + public const int AppCompatTheme_actionBarStyle = 18; + + // aapt resource value: 13 + public const int AppCompatTheme_actionBarTabBarStyle = 13; + + // aapt resource value: 12 + public const int AppCompatTheme_actionBarTabStyle = 12; + + // aapt resource value: 14 + public const int AppCompatTheme_actionBarTabTextStyle = 14; + + // aapt resource value: 20 + public const int AppCompatTheme_actionBarTheme = 20; + + // aapt resource value: 21 + public const int AppCompatTheme_actionBarWidgetTheme = 21; + + // aapt resource value: 50 + public const int AppCompatTheme_actionButtonStyle = 50; + + // aapt resource value: 46 + public const int AppCompatTheme_actionDropDownStyle = 46; + + // aapt resource value: 25 + public const int AppCompatTheme_actionMenuTextAppearance = 25; + + // aapt resource value: 26 + public const int AppCompatTheme_actionMenuTextColor = 26; + + // aapt resource value: 29 + public const int AppCompatTheme_actionModeBackground = 29; + + // aapt resource value: 28 + public const int AppCompatTheme_actionModeCloseButtonStyle = 28; + + // aapt resource value: 31 + public const int AppCompatTheme_actionModeCloseDrawable = 31; + + // aapt resource value: 33 + public const int AppCompatTheme_actionModeCopyDrawable = 33; + + // aapt resource value: 32 + public const int AppCompatTheme_actionModeCutDrawable = 32; + + // aapt resource value: 37 + public const int AppCompatTheme_actionModeFindDrawable = 37; + + // aapt resource value: 34 + public const int AppCompatTheme_actionModePasteDrawable = 34; + + // aapt resource value: 39 + public const int AppCompatTheme_actionModePopupWindowStyle = 39; + + // aapt resource value: 35 + public const int AppCompatTheme_actionModeSelectAllDrawable = 35; + + // aapt resource value: 36 + public const int AppCompatTheme_actionModeShareDrawable = 36; + + // aapt resource value: 30 + public const int AppCompatTheme_actionModeSplitBackground = 30; + + // aapt resource value: 27 + public const int AppCompatTheme_actionModeStyle = 27; + + // aapt resource value: 38 + public const int AppCompatTheme_actionModeWebSearchDrawable = 38; + + // aapt resource value: 15 + public const int AppCompatTheme_actionOverflowButtonStyle = 15; + + // aapt resource value: 16 + public const int AppCompatTheme_actionOverflowMenuStyle = 16; + + // aapt resource value: 58 + public const int AppCompatTheme_activityChooserViewStyle = 58; + + // aapt resource value: 95 + public const int AppCompatTheme_alertDialogButtonGroupStyle = 95; + + // aapt resource value: 96 + public const int AppCompatTheme_alertDialogCenterButtons = 96; + + // aapt resource value: 94 + public const int AppCompatTheme_alertDialogStyle = 94; + + // aapt resource value: 97 + public const int AppCompatTheme_alertDialogTheme = 97; + + // aapt resource value: 1 + public const int AppCompatTheme_android_windowAnimationStyle = 1; + + // aapt resource value: 0 + public const int AppCompatTheme_android_windowIsFloating = 0; + + // aapt resource value: 102 + public const int AppCompatTheme_autoCompleteTextViewStyle = 102; + + // aapt resource value: 55 + public const int AppCompatTheme_borderlessButtonStyle = 55; + + // aapt resource value: 52 + public const int AppCompatTheme_buttonBarButtonStyle = 52; + + // aapt resource value: 100 + public const int AppCompatTheme_buttonBarNegativeButtonStyle = 100; + + // aapt resource value: 101 + public const int AppCompatTheme_buttonBarNeutralButtonStyle = 101; + + // aapt resource value: 99 + public const int AppCompatTheme_buttonBarPositiveButtonStyle = 99; + + // aapt resource value: 51 + public const int AppCompatTheme_buttonBarStyle = 51; + + // aapt resource value: 103 + public const int AppCompatTheme_buttonStyle = 103; + + // aapt resource value: 104 + public const int AppCompatTheme_buttonStyleSmall = 104; + + // aapt resource value: 105 + public const int AppCompatTheme_checkboxStyle = 105; + + // aapt resource value: 106 + public const int AppCompatTheme_checkedTextViewStyle = 106; + + // aapt resource value: 86 + public const int AppCompatTheme_colorAccent = 86; + + // aapt resource value: 93 + public const int AppCompatTheme_colorBackgroundFloating = 93; + + // aapt resource value: 90 + public const int AppCompatTheme_colorButtonNormal = 90; + + // aapt resource value: 88 + public const int AppCompatTheme_colorControlActivated = 88; + + // aapt resource value: 89 + public const int AppCompatTheme_colorControlHighlight = 89; + + // aapt resource value: 87 + public const int AppCompatTheme_colorControlNormal = 87; + + // aapt resource value: 118 + public const int AppCompatTheme_colorError = 118; + + // aapt resource value: 84 + public const int AppCompatTheme_colorPrimary = 84; + + // aapt resource value: 85 + public const int AppCompatTheme_colorPrimaryDark = 85; + + // aapt resource value: 91 + public const int AppCompatTheme_colorSwitchThumbNormal = 91; + + // aapt resource value: 92 + public const int AppCompatTheme_controlBackground = 92; + + // aapt resource value: 44 + public const int AppCompatTheme_dialogPreferredPadding = 44; + + // aapt resource value: 43 + public const int AppCompatTheme_dialogTheme = 43; + + // aapt resource value: 57 + public const int AppCompatTheme_dividerHorizontal = 57; + + // aapt resource value: 56 + public const int AppCompatTheme_dividerVertical = 56; + + // aapt resource value: 75 + public const int AppCompatTheme_dropDownListViewStyle = 75; + + // aapt resource value: 47 + public const int AppCompatTheme_dropdownListPreferredItemHeight = 47; + + // aapt resource value: 64 + public const int AppCompatTheme_editTextBackground = 64; + + // aapt resource value: 63 + public const int AppCompatTheme_editTextColor = 63; + + // aapt resource value: 107 + public const int AppCompatTheme_editTextStyle = 107; + + // aapt resource value: 49 + public const int AppCompatTheme_homeAsUpIndicator = 49; + + // aapt resource value: 65 + public const int AppCompatTheme_imageButtonStyle = 65; + + // aapt resource value: 83 + public const int AppCompatTheme_listChoiceBackgroundIndicator = 83; + + // aapt resource value: 45 + public const int AppCompatTheme_listDividerAlertDialog = 45; + + // aapt resource value: 115 + public const int AppCompatTheme_listMenuViewStyle = 115; + + // aapt resource value: 76 + public const int AppCompatTheme_listPopupWindowStyle = 76; + + // aapt resource value: 70 + public const int AppCompatTheme_listPreferredItemHeight = 70; + + // aapt resource value: 72 + public const int AppCompatTheme_listPreferredItemHeightLarge = 72; + + // aapt resource value: 71 + public const int AppCompatTheme_listPreferredItemHeightSmall = 71; + + // aapt resource value: 73 + public const int AppCompatTheme_listPreferredItemPaddingLeft = 73; + + // aapt resource value: 74 + public const int AppCompatTheme_listPreferredItemPaddingRight = 74; + + // aapt resource value: 80 + public const int AppCompatTheme_panelBackground = 80; + + // aapt resource value: 82 + public const int AppCompatTheme_panelMenuListTheme = 82; + + // aapt resource value: 81 + public const int AppCompatTheme_panelMenuListWidth = 81; + + // aapt resource value: 61 + public const int AppCompatTheme_popupMenuStyle = 61; + + // aapt resource value: 62 + public const int AppCompatTheme_popupWindowStyle = 62; + + // aapt resource value: 108 + public const int AppCompatTheme_radioButtonStyle = 108; + + // aapt resource value: 109 + public const int AppCompatTheme_ratingBarStyle = 109; + + // aapt resource value: 110 + public const int AppCompatTheme_ratingBarStyleIndicator = 110; + + // aapt resource value: 111 + public const int AppCompatTheme_ratingBarStyleSmall = 111; + + // aapt resource value: 69 + public const int AppCompatTheme_searchViewStyle = 69; + + // aapt resource value: 112 + public const int AppCompatTheme_seekBarStyle = 112; + + // aapt resource value: 53 + public const int AppCompatTheme_selectableItemBackground = 53; + + // aapt resource value: 54 + public const int AppCompatTheme_selectableItemBackgroundBorderless = 54; + + // aapt resource value: 48 + public const int AppCompatTheme_spinnerDropDownItemStyle = 48; + + // aapt resource value: 113 + public const int AppCompatTheme_spinnerStyle = 113; + + // aapt resource value: 114 + public const int AppCompatTheme_switchStyle = 114; + + // aapt resource value: 40 + public const int AppCompatTheme_textAppearanceLargePopupMenu = 40; + + // aapt resource value: 77 + public const int AppCompatTheme_textAppearanceListItem = 77; + + // aapt resource value: 78 + public const int AppCompatTheme_textAppearanceListItemSecondary = 78; + + // aapt resource value: 79 + public const int AppCompatTheme_textAppearanceListItemSmall = 79; + + // aapt resource value: 42 + public const int AppCompatTheme_textAppearancePopupMenuHeader = 42; + + // aapt resource value: 67 + public const int AppCompatTheme_textAppearanceSearchResultSubtitle = 67; + + // aapt resource value: 66 + public const int AppCompatTheme_textAppearanceSearchResultTitle = 66; + + // aapt resource value: 41 + public const int AppCompatTheme_textAppearanceSmallPopupMenu = 41; + + // aapt resource value: 98 + public const int AppCompatTheme_textColorAlertDialogListItem = 98; + + // aapt resource value: 68 + public const int AppCompatTheme_textColorSearchUrl = 68; + + // aapt resource value: 60 + public const int AppCompatTheme_toolbarNavigationButtonStyle = 60; + + // aapt resource value: 59 + public const int AppCompatTheme_toolbarStyle = 59; + + // aapt resource value: 117 + public const int AppCompatTheme_tooltipForegroundColor = 117; + + // aapt resource value: 116 + public const int AppCompatTheme_tooltipFrameBackground = 116; + + // aapt resource value: 2 + public const int AppCompatTheme_windowActionBar = 2; + + // aapt resource value: 4 + public const int AppCompatTheme_windowActionBarOverlay = 4; + + // aapt resource value: 5 + public const int AppCompatTheme_windowActionModeOverlay = 5; + + // aapt resource value: 9 + public const int AppCompatTheme_windowFixedHeightMajor = 9; + + // aapt resource value: 7 + public const int AppCompatTheme_windowFixedHeightMinor = 7; + + // aapt resource value: 6 + public const int AppCompatTheme_windowFixedWidthMajor = 6; + + // aapt resource value: 8 + public const int AppCompatTheme_windowFixedWidthMinor = 8; + + // aapt resource value: 10 + public const int AppCompatTheme_windowMinWidthMajor = 10; + + // aapt resource value: 11 + public const int AppCompatTheme_windowMinWidthMinor = 11; + + // aapt resource value: 3 + public const int AppCompatTheme_windowNoTitle = 3; + + public static int[] BottomNavigationView = new int[] { + 2130772030, + 2130772291, + 2130772292, + 2130772293, + 2130772294}; + + // aapt resource value: 0 + public const int BottomNavigationView_elevation = 0; + + // aapt resource value: 4 + public const int BottomNavigationView_itemBackground = 4; + + // aapt resource value: 2 + public const int BottomNavigationView_itemIconTint = 2; + + // aapt resource value: 3 + public const int BottomNavigationView_itemTextColor = 3; + + // aapt resource value: 1 + public const int BottomNavigationView_menu = 1; + + public static int[] BottomSheetBehavior_Layout = new int[] { + 2130772253, + 2130772254, + 2130772255}; + + // aapt resource value: 1 + public const int BottomSheetBehavior_Layout_behavior_hideable = 1; + + // aapt resource value: 0 + public const int BottomSheetBehavior_Layout_behavior_peekHeight = 0; + + // aapt resource value: 2 + public const int BottomSheetBehavior_Layout_behavior_skipCollapsed = 2; + + public static int[] ButtonBarLayout = new int[] { + 2130772171}; + + // aapt resource value: 0 + public const int ButtonBarLayout_allowStacking = 0; + + public static int[] CardView = new int[] { + 16843071, + 16843072, + 2130771991, + 2130771992, + 2130771993, + 2130771994, + 2130771995, + 2130771996, + 2130771997, + 2130771998, + 2130771999, + 2130772000, + 2130772001}; + + // aapt resource value: 1 + public const int CardView_android_minHeight = 1; + + // aapt resource value: 0 + public const int CardView_android_minWidth = 0; + + // aapt resource value: 2 + public const int CardView_cardBackgroundColor = 2; + + // aapt resource value: 3 + public const int CardView_cardCornerRadius = 3; + + // aapt resource value: 4 + public const int CardView_cardElevation = 4; + + // aapt resource value: 5 + public const int CardView_cardMaxElevation = 5; + + // aapt resource value: 7 + public const int CardView_cardPreventCornerOverlap = 7; + + // aapt resource value: 6 + public const int CardView_cardUseCompatPadding = 6; + + // aapt resource value: 8 + public const int CardView_contentPadding = 8; + + // aapt resource value: 12 + public const int CardView_contentPaddingBottom = 12; + + // aapt resource value: 9 + public const int CardView_contentPaddingLeft = 9; + + // aapt resource value: 10 + public const int CardView_contentPaddingRight = 10; + + // aapt resource value: 11 + public const int CardView_contentPaddingTop = 11; + + public static int[] CollapsingToolbarLayout = new int[] { + 2130772005, + 2130772256, + 2130772257, + 2130772258, + 2130772259, + 2130772260, + 2130772261, + 2130772262, + 2130772263, + 2130772264, + 2130772265, + 2130772266, + 2130772267, + 2130772268, + 2130772269, + 2130772270}; + + // aapt resource value: 13 + public const int CollapsingToolbarLayout_collapsedTitleGravity = 13; + + // aapt resource value: 7 + public const int CollapsingToolbarLayout_collapsedTitleTextAppearance = 7; + + // aapt resource value: 8 + public const int CollapsingToolbarLayout_contentScrim = 8; + + // aapt resource value: 14 + public const int CollapsingToolbarLayout_expandedTitleGravity = 14; + + // aapt resource value: 1 + public const int CollapsingToolbarLayout_expandedTitleMargin = 1; + + // aapt resource value: 5 + public const int CollapsingToolbarLayout_expandedTitleMarginBottom = 5; + + // aapt resource value: 4 + public const int CollapsingToolbarLayout_expandedTitleMarginEnd = 4; + + // aapt resource value: 2 + public const int CollapsingToolbarLayout_expandedTitleMarginStart = 2; + + // aapt resource value: 3 + public const int CollapsingToolbarLayout_expandedTitleMarginTop = 3; + + // aapt resource value: 6 + public const int CollapsingToolbarLayout_expandedTitleTextAppearance = 6; + + // aapt resource value: 12 + public const int CollapsingToolbarLayout_scrimAnimationDuration = 12; + + // aapt resource value: 11 + public const int CollapsingToolbarLayout_scrimVisibleHeightTrigger = 11; + + // aapt resource value: 9 + public const int CollapsingToolbarLayout_statusBarScrim = 9; + + // aapt resource value: 0 + public const int CollapsingToolbarLayout_title = 0; + + // aapt resource value: 15 + public const int CollapsingToolbarLayout_titleEnabled = 15; + + // aapt resource value: 10 + public const int CollapsingToolbarLayout_toolbarId = 10; + + public static int[] CollapsingToolbarLayout_Layout = new int[] { + 2130772271, + 2130772272}; + + // aapt resource value: 0 + public const int CollapsingToolbarLayout_Layout_layout_collapseMode = 0; + + // aapt resource value: 1 + public const int CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier = 1; + + public static int[] ColorStateListItem = new int[] { + 16843173, + 16843551, + 2130772172}; + + // aapt resource value: 2 + public const int ColorStateListItem_alpha = 2; + + // aapt resource value: 1 + public const int ColorStateListItem_android_alpha = 1; + + // aapt resource value: 0 + public const int ColorStateListItem_android_color = 0; + + public static int[] CompoundButton = new int[] { + 16843015, + 2130772173, + 2130772174}; + + // aapt resource value: 0 + public const int CompoundButton_android_button = 0; + + // aapt resource value: 1 + public const int CompoundButton_buttonTint = 1; + + // aapt resource value: 2 + public const int CompoundButton_buttonTintMode = 2; + + public static int[] CoordinatorLayout = new int[] { + 2130772273, + 2130772274}; + + // aapt resource value: 0 + public const int CoordinatorLayout_keylines = 0; + + // aapt resource value: 1 + public const int CoordinatorLayout_statusBarBackground = 1; + + public static int[] CoordinatorLayout_Layout = new int[] { + 16842931, + 2130772275, + 2130772276, + 2130772277, + 2130772278, + 2130772279, + 2130772280}; + + // aapt resource value: 0 + public const int CoordinatorLayout_Layout_android_layout_gravity = 0; + + // aapt resource value: 2 + public const int CoordinatorLayout_Layout_layout_anchor = 2; + + // aapt resource value: 4 + public const int CoordinatorLayout_Layout_layout_anchorGravity = 4; + + // aapt resource value: 1 + public const int CoordinatorLayout_Layout_layout_behavior = 1; + + // aapt resource value: 6 + public const int CoordinatorLayout_Layout_layout_dodgeInsetEdges = 6; + + // aapt resource value: 5 + public const int CoordinatorLayout_Layout_layout_insetEdge = 5; + + // aapt resource value: 3 + public const int CoordinatorLayout_Layout_layout_keyline = 3; + + public static int[] DesignTheme = new int[] { + 2130772281, + 2130772282, + 2130772283}; + + // aapt resource value: 0 + public const int DesignTheme_bottomSheetDialogTheme = 0; + + // aapt resource value: 1 + public const int DesignTheme_bottomSheetStyle = 1; + + // aapt resource value: 2 + public const int DesignTheme_textColorError = 2; + + public static int[] DrawerArrowToggle = new int[] { + 2130772175, + 2130772176, + 2130772177, + 2130772178, + 2130772179, + 2130772180, + 2130772181, + 2130772182}; + + // aapt resource value: 4 + public const int DrawerArrowToggle_arrowHeadLength = 4; + + // aapt resource value: 5 + public const int DrawerArrowToggle_arrowShaftLength = 5; + + // aapt resource value: 6 + public const int DrawerArrowToggle_barLength = 6; + + // aapt resource value: 0 + public const int DrawerArrowToggle_color = 0; + + // aapt resource value: 2 + public const int DrawerArrowToggle_drawableSize = 2; + + // aapt resource value: 3 + public const int DrawerArrowToggle_gapBetweenBars = 3; + + // aapt resource value: 1 + public const int DrawerArrowToggle_spinBars = 1; + + // aapt resource value: 7 + public const int DrawerArrowToggle_thickness = 7; + + public static int[] FloatingActionButton = new int[] { + 2130772030, + 2130772246, + 2130772247, + 2130772284, + 2130772285, + 2130772286, + 2130772287, + 2130772288}; + + // aapt resource value: 1 + public const int FloatingActionButton_backgroundTint = 1; + + // aapt resource value: 2 + public const int FloatingActionButton_backgroundTintMode = 2; + + // aapt resource value: 6 + public const int FloatingActionButton_borderWidth = 6; + + // aapt resource value: 0 + public const int FloatingActionButton_elevation = 0; + + // aapt resource value: 4 + public const int FloatingActionButton_fabSize = 4; + + // aapt resource value: 5 + public const int FloatingActionButton_pressedTranslationZ = 5; + + // aapt resource value: 3 + public const int FloatingActionButton_rippleColor = 3; + + // aapt resource value: 7 + public const int FloatingActionButton_useCompatPadding = 7; + + public static int[] FloatingActionButton_Behavior_Layout = new int[] { + 2130772289}; + + // aapt resource value: 0 + public const int FloatingActionButton_Behavior_Layout_behavior_autoHide = 0; + + public static int[] FontFamily = new int[] { + 2130772330, + 2130772331, + 2130772332, + 2130772333, + 2130772334, + 2130772335}; + + // aapt resource value: 0 + public const int FontFamily_fontProviderAuthority = 0; + + // aapt resource value: 3 + public const int FontFamily_fontProviderCerts = 3; + + // aapt resource value: 4 + public const int FontFamily_fontProviderFetchStrategy = 4; + + // aapt resource value: 5 + public const int FontFamily_fontProviderFetchTimeout = 5; + + // aapt resource value: 1 + public const int FontFamily_fontProviderPackage = 1; + + // aapt resource value: 2 + public const int FontFamily_fontProviderQuery = 2; + + public static int[] FontFamilyFont = new int[] { + 16844082, + 16844083, + 16844095, + 2130772336, + 2130772337, + 2130772338}; + + // aapt resource value: 0 + public const int FontFamilyFont_android_font = 0; + + // aapt resource value: 2 + public const int FontFamilyFont_android_fontStyle = 2; + + // aapt resource value: 1 + public const int FontFamilyFont_android_fontWeight = 1; + + // aapt resource value: 4 + public const int FontFamilyFont_font = 4; + + // aapt resource value: 3 + public const int FontFamilyFont_fontStyle = 3; + + // aapt resource value: 5 + public const int FontFamilyFont_fontWeight = 5; + + public static int[] ForegroundLinearLayout = new int[] { + 16843017, + 16843264, + 2130772290}; + + // aapt resource value: 0 + public const int ForegroundLinearLayout_android_foreground = 0; + + // aapt resource value: 1 + public const int ForegroundLinearLayout_android_foregroundGravity = 1; + + // aapt resource value: 2 + public const int ForegroundLinearLayout_foregroundInsidePadding = 2; + + public static int[] LinearLayoutCompat = new int[] { + 16842927, + 16842948, + 16843046, + 16843047, + 16843048, + 2130772013, + 2130772183, + 2130772184, + 2130772185}; + + // aapt resource value: 2 + public const int LinearLayoutCompat_android_baselineAligned = 2; + + // aapt resource value: 3 + public const int LinearLayoutCompat_android_baselineAlignedChildIndex = 3; + + // aapt resource value: 0 + public const int LinearLayoutCompat_android_gravity = 0; + + // aapt resource value: 1 + public const int LinearLayoutCompat_android_orientation = 1; + + // aapt resource value: 4 + public const int LinearLayoutCompat_android_weightSum = 4; + + // aapt resource value: 5 + public const int LinearLayoutCompat_divider = 5; + + // aapt resource value: 8 + public const int LinearLayoutCompat_dividerPadding = 8; + + // aapt resource value: 6 + public const int LinearLayoutCompat_measureWithLargestChild = 6; + + // aapt resource value: 7 + public const int LinearLayoutCompat_showDividers = 7; + + public static int[] LinearLayoutCompat_Layout = new int[] { + 16842931, + 16842996, + 16842997, + 16843137}; + + // aapt resource value: 0 + public const int LinearLayoutCompat_Layout_android_layout_gravity = 0; + + // aapt resource value: 2 + public const int LinearLayoutCompat_Layout_android_layout_height = 2; + + // aapt resource value: 3 + public const int LinearLayoutCompat_Layout_android_layout_weight = 3; + + // aapt resource value: 1 + public const int LinearLayoutCompat_Layout_android_layout_width = 1; + + public static int[] ListPopupWindow = new int[] { + 16843436, + 16843437}; + + // aapt resource value: 0 + public const int ListPopupWindow_android_dropDownHorizontalOffset = 0; + + // aapt resource value: 1 + public const int ListPopupWindow_android_dropDownVerticalOffset = 1; + + public static int[] MediaRouteButton = new int[] { + 16843071, + 16843072, + 2130771989, + 2130771990}; + + // aapt resource value: 1 + public const int MediaRouteButton_android_minHeight = 1; + + // aapt resource value: 0 + public const int MediaRouteButton_android_minWidth = 0; + + // aapt resource value: 2 + public const int MediaRouteButton_externalRouteEnabledDrawable = 2; + + // aapt resource value: 3 + public const int MediaRouteButton_mediaRouteButtonTint = 3; + + public static int[] MenuGroup = new int[] { + 16842766, + 16842960, + 16843156, + 16843230, + 16843231, + 16843232}; + + // aapt resource value: 5 + public const int MenuGroup_android_checkableBehavior = 5; + + // aapt resource value: 0 + public const int MenuGroup_android_enabled = 0; + + // aapt resource value: 1 + public const int MenuGroup_android_id = 1; + + // aapt resource value: 3 + public const int MenuGroup_android_menuCategory = 3; + + // aapt resource value: 4 + public const int MenuGroup_android_orderInCategory = 4; + + // aapt resource value: 2 + public const int MenuGroup_android_visible = 2; + + public static int[] MenuItem = new int[] { + 16842754, + 16842766, + 16842960, + 16843014, + 16843156, + 16843230, + 16843231, + 16843233, + 16843234, + 16843235, + 16843236, + 16843237, + 16843375, + 2130772186, + 2130772187, + 2130772188, + 2130772189, + 2130772190, + 2130772191, + 2130772192, + 2130772193, + 2130772194, + 2130772195}; + + // aapt resource value: 16 + public const int MenuItem_actionLayout = 16; + + // aapt resource value: 18 + public const int MenuItem_actionProviderClass = 18; + + // aapt resource value: 17 + public const int MenuItem_actionViewClass = 17; + + // aapt resource value: 13 + public const int MenuItem_alphabeticModifiers = 13; + + // aapt resource value: 9 + public const int MenuItem_android_alphabeticShortcut = 9; + + // aapt resource value: 11 + public const int MenuItem_android_checkable = 11; + + // aapt resource value: 3 + public const int MenuItem_android_checked = 3; + + // aapt resource value: 1 + public const int MenuItem_android_enabled = 1; + + // aapt resource value: 0 + public const int MenuItem_android_icon = 0; + + // aapt resource value: 2 + public const int MenuItem_android_id = 2; + + // aapt resource value: 5 + public const int MenuItem_android_menuCategory = 5; + + // aapt resource value: 10 + public const int MenuItem_android_numericShortcut = 10; + + // aapt resource value: 12 + public const int MenuItem_android_onClick = 12; + + // aapt resource value: 6 + public const int MenuItem_android_orderInCategory = 6; + + // aapt resource value: 7 + public const int MenuItem_android_title = 7; + + // aapt resource value: 8 + public const int MenuItem_android_titleCondensed = 8; + + // aapt resource value: 4 + public const int MenuItem_android_visible = 4; + + // aapt resource value: 19 + public const int MenuItem_contentDescription = 19; + + // aapt resource value: 21 + public const int MenuItem_iconTint = 21; + + // aapt resource value: 22 + public const int MenuItem_iconTintMode = 22; + + // aapt resource value: 14 + public const int MenuItem_numericModifiers = 14; + + // aapt resource value: 15 + public const int MenuItem_showAsAction = 15; + + // aapt resource value: 20 + public const int MenuItem_tooltipText = 20; + + public static int[] MenuView = new int[] { + 16842926, + 16843052, + 16843053, + 16843054, + 16843055, + 16843056, + 16843057, + 2130772196, + 2130772197}; + + // aapt resource value: 4 + public const int MenuView_android_headerBackground = 4; + + // aapt resource value: 2 + public const int MenuView_android_horizontalDivider = 2; + + // aapt resource value: 5 + public const int MenuView_android_itemBackground = 5; + + // aapt resource value: 6 + public const int MenuView_android_itemIconDisabledAlpha = 6; + + // aapt resource value: 1 + public const int MenuView_android_itemTextAppearance = 1; + + // aapt resource value: 3 + public const int MenuView_android_verticalDivider = 3; + + // aapt resource value: 0 + public const int MenuView_android_windowAnimationStyle = 0; + + // aapt resource value: 7 + public const int MenuView_preserveIconSpacing = 7; + + // aapt resource value: 8 + public const int MenuView_subMenuArrow = 8; + + public static int[] NavigationView = new int[] { + 16842964, + 16842973, + 16843039, + 2130772030, + 2130772291, + 2130772292, + 2130772293, + 2130772294, + 2130772295, + 2130772296}; + + // aapt resource value: 0 + public const int NavigationView_android_background = 0; + + // aapt resource value: 1 + public const int NavigationView_android_fitsSystemWindows = 1; + + // aapt resource value: 2 + public const int NavigationView_android_maxWidth = 2; + + // aapt resource value: 3 + public const int NavigationView_elevation = 3; + + // aapt resource value: 9 + public const int NavigationView_headerLayout = 9; + + // aapt resource value: 7 + public const int NavigationView_itemBackground = 7; + + // aapt resource value: 5 + public const int NavigationView_itemIconTint = 5; + + // aapt resource value: 8 + public const int NavigationView_itemTextAppearance = 8; + + // aapt resource value: 6 + public const int NavigationView_itemTextColor = 6; + + // aapt resource value: 4 + public const int NavigationView_menu = 4; + + public static int[] PopupWindow = new int[] { + 16843126, + 16843465, + 2130772198}; + + // aapt resource value: 1 + public const int PopupWindow_android_popupAnimationStyle = 1; + + // aapt resource value: 0 + public const int PopupWindow_android_popupBackground = 0; + + // aapt resource value: 2 + public const int PopupWindow_overlapAnchor = 2; + + public static int[] PopupWindowBackgroundState = new int[] { + 2130772199}; + + // aapt resource value: 0 + public const int PopupWindowBackgroundState_state_above_anchor = 0; + + public static int[] RecycleListView = new int[] { + 2130772200, + 2130772201}; + + // aapt resource value: 0 + public const int RecycleListView_paddingBottomNoButtons = 0; + + // aapt resource value: 1 + public const int RecycleListView_paddingTopNoTitle = 1; + + public static int[] RecyclerView = new int[] { + 16842948, + 16842993, + 2130771968, + 2130771969, + 2130771970, + 2130771971, + 2130771972, + 2130771973, + 2130771974, + 2130771975, + 2130771976}; + + // aapt resource value: 1 + public const int RecyclerView_android_descendantFocusability = 1; + + // aapt resource value: 0 + public const int RecyclerView_android_orientation = 0; + + // aapt resource value: 6 + public const int RecyclerView_fastScrollEnabled = 6; + + // aapt resource value: 9 + public const int RecyclerView_fastScrollHorizontalThumbDrawable = 9; + + // aapt resource value: 10 + public const int RecyclerView_fastScrollHorizontalTrackDrawable = 10; + + // aapt resource value: 7 + public const int RecyclerView_fastScrollVerticalThumbDrawable = 7; + + // aapt resource value: 8 + public const int RecyclerView_fastScrollVerticalTrackDrawable = 8; + + // aapt resource value: 2 + public const int RecyclerView_layoutManager = 2; + + // aapt resource value: 4 + public const int RecyclerView_reverseLayout = 4; + + // aapt resource value: 3 + public const int RecyclerView_spanCount = 3; + + // aapt resource value: 5 + public const int RecyclerView_stackFromEnd = 5; + + public static int[] ScrimInsetsFrameLayout = new int[] { + 2130772297}; + + // aapt resource value: 0 + public const int ScrimInsetsFrameLayout_insetForeground = 0; + + public static int[] ScrollingViewBehavior_Layout = new int[] { + 2130772298}; + + // aapt resource value: 0 + public const int ScrollingViewBehavior_Layout_behavior_overlapTop = 0; + + public static int[] SearchView = new int[] { + 16842970, + 16843039, + 16843296, + 16843364, + 2130772202, + 2130772203, + 2130772204, + 2130772205, + 2130772206, + 2130772207, + 2130772208, + 2130772209, + 2130772210, + 2130772211, + 2130772212, + 2130772213, + 2130772214}; + + // aapt resource value: 0 + public const int SearchView_android_focusable = 0; + + // aapt resource value: 3 + public const int SearchView_android_imeOptions = 3; + + // aapt resource value: 2 + public const int SearchView_android_inputType = 2; + + // aapt resource value: 1 + public const int SearchView_android_maxWidth = 1; + + // aapt resource value: 8 + public const int SearchView_closeIcon = 8; + + // aapt resource value: 13 + public const int SearchView_commitIcon = 13; + + // aapt resource value: 7 + public const int SearchView_defaultQueryHint = 7; + + // aapt resource value: 9 + public const int SearchView_goIcon = 9; + + // aapt resource value: 5 + public const int SearchView_iconifiedByDefault = 5; + + // aapt resource value: 4 + public const int SearchView_layout = 4; + + // aapt resource value: 15 + public const int SearchView_queryBackground = 15; + + // aapt resource value: 6 + public const int SearchView_queryHint = 6; + + // aapt resource value: 11 + public const int SearchView_searchHintIcon = 11; + + // aapt resource value: 10 + public const int SearchView_searchIcon = 10; + + // aapt resource value: 16 + public const int SearchView_submitBackground = 16; + + // aapt resource value: 14 + public const int SearchView_suggestionRowLayout = 14; + + // aapt resource value: 12 + public const int SearchView_voiceIcon = 12; + + public static int[] SnackbarLayout = new int[] { + 16843039, + 2130772030, + 2130772299}; + + // aapt resource value: 0 + public const int SnackbarLayout_android_maxWidth = 0; + + // aapt resource value: 1 + public const int SnackbarLayout_elevation = 1; + + // aapt resource value: 2 + public const int SnackbarLayout_maxActionInlineWidth = 2; + + public static int[] Spinner = new int[] { + 16842930, + 16843126, + 16843131, + 16843362, + 2130772031}; + + // aapt resource value: 3 + public const int Spinner_android_dropDownWidth = 3; + + // aapt resource value: 0 + public const int Spinner_android_entries = 0; + + // aapt resource value: 1 + public const int Spinner_android_popupBackground = 1; + + // aapt resource value: 2 + public const int Spinner_android_prompt = 2; + + // aapt resource value: 4 + public const int Spinner_popupTheme = 4; + + public static int[] SwitchCompat = new int[] { + 16843044, + 16843045, + 16843074, + 2130772215, + 2130772216, + 2130772217, + 2130772218, + 2130772219, + 2130772220, + 2130772221, + 2130772222, + 2130772223, + 2130772224, + 2130772225}; + + // aapt resource value: 1 + public const int SwitchCompat_android_textOff = 1; + + // aapt resource value: 0 + public const int SwitchCompat_android_textOn = 0; + + // aapt resource value: 2 + public const int SwitchCompat_android_thumb = 2; + + // aapt resource value: 13 + public const int SwitchCompat_showText = 13; + + // aapt resource value: 12 + public const int SwitchCompat_splitTrack = 12; + + // aapt resource value: 10 + public const int SwitchCompat_switchMinWidth = 10; + + // aapt resource value: 11 + public const int SwitchCompat_switchPadding = 11; + + // aapt resource value: 9 + public const int SwitchCompat_switchTextAppearance = 9; + + // aapt resource value: 8 + public const int SwitchCompat_thumbTextPadding = 8; + + // aapt resource value: 3 + public const int SwitchCompat_thumbTint = 3; + + // aapt resource value: 4 + public const int SwitchCompat_thumbTintMode = 4; + + // aapt resource value: 5 + public const int SwitchCompat_track = 5; + + // aapt resource value: 6 + public const int SwitchCompat_trackTint = 6; + + // aapt resource value: 7 + public const int SwitchCompat_trackTintMode = 7; + + public static int[] TabItem = new int[] { + 16842754, + 16842994, + 16843087}; + + // aapt resource value: 0 + public const int TabItem_android_icon = 0; + + // aapt resource value: 1 + public const int TabItem_android_layout = 1; + + // aapt resource value: 2 + public const int TabItem_android_text = 2; + + public static int[] TabLayout = new int[] { + 2130772300, + 2130772301, + 2130772302, + 2130772303, + 2130772304, + 2130772305, + 2130772306, + 2130772307, + 2130772308, + 2130772309, + 2130772310, + 2130772311, + 2130772312, + 2130772313, + 2130772314, + 2130772315}; + + // aapt resource value: 3 + public const int TabLayout_tabBackground = 3; + + // aapt resource value: 2 + public const int TabLayout_tabContentStart = 2; + + // aapt resource value: 5 + public const int TabLayout_tabGravity = 5; + + // aapt resource value: 0 + public const int TabLayout_tabIndicatorColor = 0; + + // aapt resource value: 1 + public const int TabLayout_tabIndicatorHeight = 1; + + // aapt resource value: 7 + public const int TabLayout_tabMaxWidth = 7; + + // aapt resource value: 6 + public const int TabLayout_tabMinWidth = 6; + + // aapt resource value: 4 + public const int TabLayout_tabMode = 4; + + // aapt resource value: 15 + public const int TabLayout_tabPadding = 15; + + // aapt resource value: 14 + public const int TabLayout_tabPaddingBottom = 14; + + // aapt resource value: 13 + public const int TabLayout_tabPaddingEnd = 13; + + // aapt resource value: 11 + public const int TabLayout_tabPaddingStart = 11; + + // aapt resource value: 12 + public const int TabLayout_tabPaddingTop = 12; + + // aapt resource value: 10 + public const int TabLayout_tabSelectedTextColor = 10; + + // aapt resource value: 8 + public const int TabLayout_tabTextAppearance = 8; + + // aapt resource value: 9 + public const int TabLayout_tabTextColor = 9; + + public static int[] TextAppearance = new int[] { + 16842901, + 16842902, + 16842903, + 16842904, + 16842906, + 16842907, + 16843105, + 16843106, + 16843107, + 16843108, + 16843692, + 2130772047, + 2130772053}; + + // aapt resource value: 10 + public const int TextAppearance_android_fontFamily = 10; + + // aapt resource value: 6 + public const int TextAppearance_android_shadowColor = 6; + + // aapt resource value: 7 + public const int TextAppearance_android_shadowDx = 7; + + // aapt resource value: 8 + public const int TextAppearance_android_shadowDy = 8; + + // aapt resource value: 9 + public const int TextAppearance_android_shadowRadius = 9; + + // aapt resource value: 3 + public const int TextAppearance_android_textColor = 3; + + // aapt resource value: 4 + public const int TextAppearance_android_textColorHint = 4; + + // aapt resource value: 5 + public const int TextAppearance_android_textColorLink = 5; + + // aapt resource value: 0 + public const int TextAppearance_android_textSize = 0; + + // aapt resource value: 2 + public const int TextAppearance_android_textStyle = 2; + + // aapt resource value: 1 + public const int TextAppearance_android_typeface = 1; + + // aapt resource value: 12 + public const int TextAppearance_fontFamily = 12; + + // aapt resource value: 11 + public const int TextAppearance_textAllCaps = 11; + + public static int[] TextInputLayout = new int[] { + 16842906, + 16843088, + 2130772316, + 2130772317, + 2130772318, + 2130772319, + 2130772320, + 2130772321, + 2130772322, + 2130772323, + 2130772324, + 2130772325, + 2130772326, + 2130772327, + 2130772328, + 2130772329}; + + // aapt resource value: 1 + public const int TextInputLayout_android_hint = 1; + + // aapt resource value: 0 + public const int TextInputLayout_android_textColorHint = 0; + + // aapt resource value: 6 + public const int TextInputLayout_counterEnabled = 6; + + // aapt resource value: 7 + public const int TextInputLayout_counterMaxLength = 7; + + // aapt resource value: 9 + public const int TextInputLayout_counterOverflowTextAppearance = 9; + + // aapt resource value: 8 + public const int TextInputLayout_counterTextAppearance = 8; + + // aapt resource value: 4 + public const int TextInputLayout_errorEnabled = 4; + + // aapt resource value: 5 + public const int TextInputLayout_errorTextAppearance = 5; + + // aapt resource value: 10 + public const int TextInputLayout_hintAnimationEnabled = 10; + + // aapt resource value: 3 + public const int TextInputLayout_hintEnabled = 3; + + // aapt resource value: 2 + public const int TextInputLayout_hintTextAppearance = 2; + + // aapt resource value: 13 + public const int TextInputLayout_passwordToggleContentDescription = 13; + + // aapt resource value: 12 + public const int TextInputLayout_passwordToggleDrawable = 12; + + // aapt resource value: 11 + public const int TextInputLayout_passwordToggleEnabled = 11; + + // aapt resource value: 14 + public const int TextInputLayout_passwordToggleTint = 14; + + // aapt resource value: 15 + public const int TextInputLayout_passwordToggleTintMode = 15; + + public static int[] Toolbar = new int[] { + 16842927, + 16843072, + 2130772005, + 2130772008, + 2130772012, + 2130772024, + 2130772025, + 2130772026, + 2130772027, + 2130772028, + 2130772029, + 2130772031, + 2130772226, + 2130772227, + 2130772228, + 2130772229, + 2130772230, + 2130772231, + 2130772232, + 2130772233, + 2130772234, + 2130772235, + 2130772236, + 2130772237, + 2130772238, + 2130772239, + 2130772240, + 2130772241, + 2130772242}; + + // aapt resource value: 0 + public const int Toolbar_android_gravity = 0; + + // aapt resource value: 1 + public const int Toolbar_android_minHeight = 1; + + // aapt resource value: 21 + public const int Toolbar_buttonGravity = 21; + + // aapt resource value: 23 + public const int Toolbar_collapseContentDescription = 23; + + // aapt resource value: 22 + public const int Toolbar_collapseIcon = 22; + + // aapt resource value: 6 + public const int Toolbar_contentInsetEnd = 6; + + // aapt resource value: 10 + public const int Toolbar_contentInsetEndWithActions = 10; + + // aapt resource value: 7 + public const int Toolbar_contentInsetLeft = 7; + + // aapt resource value: 8 + public const int Toolbar_contentInsetRight = 8; + + // aapt resource value: 5 + public const int Toolbar_contentInsetStart = 5; + + // aapt resource value: 9 + public const int Toolbar_contentInsetStartWithNavigation = 9; + + // aapt resource value: 4 + public const int Toolbar_logo = 4; + + // aapt resource value: 26 + public const int Toolbar_logoDescription = 26; + + // aapt resource value: 20 + public const int Toolbar_maxButtonHeight = 20; + + // aapt resource value: 25 + public const int Toolbar_navigationContentDescription = 25; + + // aapt resource value: 24 + public const int Toolbar_navigationIcon = 24; + + // aapt resource value: 11 + public const int Toolbar_popupTheme = 11; + + // aapt resource value: 3 + public const int Toolbar_subtitle = 3; + + // aapt resource value: 13 + public const int Toolbar_subtitleTextAppearance = 13; + + // aapt resource value: 28 + public const int Toolbar_subtitleTextColor = 28; + + // aapt resource value: 2 + public const int Toolbar_title = 2; + + // aapt resource value: 14 + public const int Toolbar_titleMargin = 14; + + // aapt resource value: 18 + public const int Toolbar_titleMarginBottom = 18; + + // aapt resource value: 16 + public const int Toolbar_titleMarginEnd = 16; + + // aapt resource value: 15 + public const int Toolbar_titleMarginStart = 15; + + // aapt resource value: 17 + public const int Toolbar_titleMarginTop = 17; + + // aapt resource value: 19 + public const int Toolbar_titleMargins = 19; + + // aapt resource value: 12 + public const int Toolbar_titleTextAppearance = 12; + + // aapt resource value: 27 + public const int Toolbar_titleTextColor = 27; + + public static int[] View = new int[] { + 16842752, + 16842970, + 2130772243, + 2130772244, + 2130772245}; + + // aapt resource value: 1 + public const int View_android_focusable = 1; + + // aapt resource value: 0 + public const int View_android_theme = 0; + + // aapt resource value: 3 + public const int View_paddingEnd = 3; + + // aapt resource value: 2 + public const int View_paddingStart = 2; + + // aapt resource value: 4 + public const int View_theme = 4; + + public static int[] ViewBackgroundHelper = new int[] { + 16842964, + 2130772246, + 2130772247}; + + // aapt resource value: 0 + public const int ViewBackgroundHelper_android_background = 0; + + // aapt resource value: 1 + public const int ViewBackgroundHelper_backgroundTint = 1; + + // aapt resource value: 2 + public const int ViewBackgroundHelper_backgroundTintMode = 2; + + public static int[] ViewStubCompat = new int[] { + 16842960, + 16842994, + 16842995}; + + // aapt resource value: 0 + public const int ViewStubCompat_android_id = 0; + + // aapt resource value: 2 + public const int ViewStubCompat_android_inflatedId = 2; + + // aapt resource value: 1 + public const int ViewStubCompat_android_layout = 1; + + static Styleable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Styleable() + { + } + } + } +} +#pragma warning restore 1591 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/layout/activity_main.axml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/layout/activity_main.axml new file mode 100644 index 00000000..7447d516 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/layout/activity_main.axml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..c9ad5f98 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher_round.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..c9ad5f98 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2531cb31efc3a0a3de6113ab9c7845dc1d9654e4 GIT binary patch literal 1634 zcmV-o2A%ndP)B+Z3$1(8#|f~9B42Y^N-3=o2YCq0YUY$Zu=pM;#hG{lHi%n~Vh z1d1vN#EDO19X?u$`cV z!a}AKG@Bb*#1cdYg8af_;jP69b`k%G1n?0=F^8bI^o>wg-vEliK^U}y^!D|^p|ax; zC|pK=f+FHp!RUAhtlpGGUxJb|wm^5! z<1r%$<$TR02wajxKZ4MiR#aAxDLE(##UNyD|ABr4WoGRF*?@e^2|~Hq(gurSSJH*;Q~5lw{J5A_(PCXBWhzZE${qgzv0{dk-F( z1<}>r181tLiEla&f1j&?p2xjbfp2cTt-c1Ox~?9EhK9`cJ9Vatf)loIoQ@#h&}cIGD>Z#QLE}&(bMo@7Ff|7f#Nm^$PJpVcbj+v~K7wfVwF}=) zRQsc+`=A-+C)vrRvaIC-5u>|;3h z*G4-u#RI<_vuSN~vZ6{|I~q5FFk3%de#+*>UFG>&bq6~ zUEMZ~FIOmFO=kA^5rkp-Msw?^63xvdXVZ-rv@{6{iVO}M!}^Je%2BPbi+(L<5<%~h z2v^D+f<|j%7~cJjOzg*!GPQ{%uE{i%YgcZhuZh{yNlQ}RhaU1jd=K+AopVKP+D}&} zZ3y$llqZiln=Z_A$!qzkGbX0D{?l(v5@1|`QyCvCnQ`eKI>|zj_zo%y#fKf85VhQ} zP)y&j4P*nR3q{-o35iV6nx7QDqq<;WDVIt}|N%`!dgv*y3va8eLNj zU9x(?ieweHfQ*yXk8|=ssZ~qJEz^QoKJ|iGa>ge_Vm_8l}S+UvJ{8g4jr+o#aTSFsz1W;PDP zW765JXGU#3JL>SlIl3NRV2{7B2dLO1cIP)a4ZRYL|MBD36O1#oSgAf}APz5@;x=_U-<=y)Py7*}O5(uu7BL_eLe6Ek7pH|G zMq)FrF1EFq&yruS5b=F=w)fVVoPd(oeRyTFym_Uwyn~L=OL(O?cf^2L5R(SmjORx6 z%nmZf^W=3pkvT*>@osUNi>DULH1hL;y`JGQX$onRCr_U0=H~Viodq!<7Q{3rPk~{G gu#IhOV;e2n|1(WJB~7~kivR!s07*qoM6N<$g7lUVaR2}S literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_foreground.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..7a859c25556af7a2e46e22a2220eaded55628e9f GIT binary patch literal 1441 zcma)+`#%#30L6FjMQg%tuA0%p#0??L`*E=rD#U2F4L5n@F+O9Sp;(QwEQy7+?sX?r zCWN(!Hg`+j5k8*H@|yQEtnAi*(D{7M`Tlf1=eKjq)BUsp2nqrK01B=yNUv`!`EH=x zx8$xJQUd^Fuec%|(TT&0V}4orr_==mmCnEuzD+ff8Pg>pJRqsWsD{#?eGPaCu0(sEH_2RG@<6-Nt<8 ztPMUmmAz9Ga$23Y9~p9dqJSgJJ#Jk_r@o13^%d-Xf46i+Lrmz3 zy9(DUDVXj;Zny7nO+yn&W2flEX=C!8&D0zI`G# z8;XmlonoghgRFUY*$+7pPLa}Uy)onw>TT9t(FTV6#BV8&lXWDPRvQW_n~xZ|yLcZjX>m$Eaf1)dwXS`&E^ zkNjO;%;fWywchc=+w4utQ0Vbn%B>b~yy4I#D{?1!P`$P>Wdo+ljCo(tYia04JTc=$$u+IbzDVPFYpm8+AQj+ zGKH zfS{{hN%W)kF+(26oZpkURD5Q_G_z97F6{Jval+TOj-;5y)*Rdo3a$^^k~q5gpTzmp1q@+2X9O z;_VUF>;s~C1~gpFrFoh?{aQ|LlBIYz!z^P~lndX5-ES)p#+9GW*|-WBTzQ*&gKOE` zM##bUaWl`6rZBXw0!~_oUhf+H$tNc@lLZCj0bZT^KSo@C|P?7YR8dP0se1jj z9aA0|7MONf(ZYaLZs$s}r*05fx25-iN6mZe_*Rq%uyz(+^-k;t`!R`?uf~rn#1ZC7 zuv3}UrmMzcBbo4jym@fS5%I+G`GJIC1s$)MQs3Vhld?a2U;w}$@V%dC@%qpO7+3#$ N&GnQ!lI8SQ#{X#Iv!eh2 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_round.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b8d35b3a1cfe5308bb099a5a1429555c41417e36 GIT binary patch literal 3552 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/ic_launcher_round.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8f56909cddfa86f1387074bf43003f36d6e67be1 GIT binary patch literal 2413 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 b;}yxl--Ss53i@>Q4YQuNcebmsMJN0NT!aL! literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/ic_launcher_round.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..9737d79c0492b2c8a811621660eba33e79fab959 GIT binary patch literal 4858 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9133e31b43252d00767a6a3806df9ba68de2d265 GIT binary patch literal 3871 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher_round.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ae5f5ccdecc01a9b17a2a0c2b1bb20602f0151 GIT binary patch literal 8001 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 z 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 F@PB1o0BisN literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/ic_launcher_round.png b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..ef89bd5215ffcc38c68b119a7495a77a7084543b GIT binary patch literal 10893 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 literal 0 HcmV?d00001 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/colors.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/colors.xml new file mode 100644 index 00000000..bf1bf200 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #2c3e50 + #1B3147 + #3498db + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/ic_launcher_background.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/ic_launcher_background.xml new file mode 100644 index 00000000..6ec24e64 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/strings.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/strings.xml new file mode 100644 index 00000000..c8ee97ca --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/strings.xml @@ -0,0 +1,5 @@ + + + LaunchDarkly.XamarinSdk.Android.Tests + Settings + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml new file mode 100644 index 00000000..dde52b81 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file From 122cef105322b9c67fa2a8d34c8c9fdec85b7660 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 18:12:53 -0700 Subject: [PATCH 126/254] try building the Android test project --- .circleci/config.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f2235d1..6ae5f99f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,8 +33,6 @@ jobs: - run: sudo apt install apt-transport-https dirmngr gnupg ca-certificates && sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - run: echo y | sudo apt install mono-devel nuget libzip4 - - run: nuget restore - - restore_cache: key: xamarin-android-cache-v9-2-99-172 - run: ./scripts/check_xamarin_android_cache.sh @@ -52,6 +50,10 @@ jobs: name: Build SDK command: msbuild /restore /p:TargetFramework=MonoAndroid81 src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj + - run: + name: Build test project + command: msbuild /restore tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + test-ios: macos: xcode: "10.2.1" From fe45d9590c21561655114607fead687b4f4bc9c1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 18:14:20 -0700 Subject: [PATCH 127/254] better organization of commands into labeled groups --- .circleci/config.yml | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ae5f99f..c83cd982 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,24 +27,35 @@ jobs: steps: - checkout - - run: sdkmanager "system-images;android-24;default;armeabi-v7a" || true - - run: sdkmanager --licenses + - run: + name: Install Android SDK tools + command: | + sdkmanager "system-images;android-24;default;armeabi-v7a" || true + sdkmanager --licenses - - run: sudo apt install apt-transport-https dirmngr gnupg ca-certificates && sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - - run: echo y | sudo apt install mono-devel nuget libzip4 + - run: + name: Install Mono tools + command: | + sudo apt install apt-transport-https dirmngr gnupg ca-certificates && sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update + echo y | sudo apt install mono-devel nuget libzip4 - restore_cache: key: xamarin-android-cache-v9-2-99-172 - - run: ./scripts/check_xamarin_android_cache.sh + - run: + name: Download Xamarin Android tools if not already cached + command: ./scripts/check_xamarin_android_cache.sh - save_cache: key: xamarin-android-cache-v9-2-99-172 paths: - ~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release - - run: sudo mkdir "/usr/lib/xamarin.android" && sudo mkdir "/usr/lib/mono/xbuild/Xamarin/" - - run: cd ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release && sudo cp -a "bin/Debug/lib/xamarin.android/." "/usr/lib/xamarin.android/" - - run: rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - - run: sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" && sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" + - run: + name: Move tools to expected locations + command: | + sudo mkdir "/usr/lib/xamarin.android" && sudo mkdir "/usr/lib/mono/xbuild/Xamarin/" + cd ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release && sudo cp -a "bin/Debug/lib/xamarin.android/." "/usr/lib/xamarin.android/" + rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" + sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" && sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - run: name: Build SDK From 7a9282546604185637d5a66d968808add733f1c4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 18:22:27 -0700 Subject: [PATCH 128/254] make commands a little more readable --- .circleci/config.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c83cd982..a8e8ec01 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,8 +36,10 @@ jobs: - run: name: Install Mono tools command: | - sudo apt install apt-transport-https dirmngr gnupg ca-certificates && sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - echo y | sudo apt install mono-devel nuget libzip4 + sudo apt -y install apt-transport-https dirmngr gnupg ca-certificates + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF + echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update + sudo apt -y install mono-devel nuget libzip4 - restore_cache: key: xamarin-android-cache-v9-2-99-172 @@ -55,7 +57,8 @@ jobs: sudo mkdir "/usr/lib/xamarin.android" && sudo mkdir "/usr/lib/mono/xbuild/Xamarin/" cd ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release && sudo cp -a "bin/Debug/lib/xamarin.android/." "/usr/lib/xamarin.android/" rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" && sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" + sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" + sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - run: name: Build SDK From 8db5b2a3b6b5150b5680a3dca46bdfa7ddc282d8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 18:40:40 -0700 Subject: [PATCH 129/254] build with xabuild --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a8e8ec01..642a4551 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,9 @@ jobs: - run: name: Build test project - command: msbuild /restore tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + # Note, xabuild is just a wrapper for msbuild that adds various tools paths etc. necessary for building an + # Android app. See: https://github.com/xamarin/xamarin-android/blob/master/tools/scripts/xabuild test-ios: macos: From 4f674eece40605cbc89cb1c556675afae4ec799c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 19:11:28 -0700 Subject: [PATCH 130/254] rm autogenerated UI file that for some reason broke the build --- .../LaunchDarkly.XamarinSdk.Android.Tests.csproj | 1 - .../Properties/AndroidManifest.xml | 2 +- .../Resources/Resource.designer.cs | 3 --- .../Resources/values/styles.xml | 10 ---------- 4 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index 0202bfd4..b93b875b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -121,7 +121,6 @@ - diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml index 4e00401d..c5781e4c 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs index a119fbb2..f352a1ff 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs @@ -6218,9 +6218,6 @@ public partial class Style // aapt resource value: 0x7f0c016e public const int Animation_Design_BottomSheetDialog = 2131493230; - // aapt resource value: 0x7f0c018f - public const int AppTheme = 2131493263; - // aapt resource value: 0x7f0c00a9 public const int Base_AlertDialog_AppCompat = 2131493033; diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml deleted file mode 100644 index dde52b81..00000000 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/values/styles.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file From fe262f37635316024a760693ab556ba0a28762b4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 19:20:09 -0700 Subject: [PATCH 131/254] auto-restore --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 642a4551..796d65bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ jobs: - run: name: Build test project - command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild /restore tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj # Note, xabuild is just a wrapper for msbuild that adds various tools paths etc. necessary for building an # Android app. See: https://github.com/xamarin/xamarin-android/blob/master/tools/scripts/xabuild From b41f585183c5769344d843c410e8ea3313ee49ee Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 19:20:33 -0700 Subject: [PATCH 132/254] misc cleanup --- .../Assets/AboutAssets.txt | 19 -------- ...unchDarkly.XamarinSdk.Android.Tests.csproj | 10 +---- .../MainActivity.cs | 3 +- .../Resources/AboutResources.txt | 44 ------------------- .../AppDelegate.cs | 3 +- 5 files changed, 3 insertions(+), 76 deletions(-) delete mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt delete mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt deleted file mode 100644 index b0633374..00000000 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with you package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index b93b875b..ad9e5b13 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -110,9 +110,7 @@ - - @@ -161,6 +159,7 @@ + @@ -169,11 +168,4 @@ - \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs index ea291a39..d511c86c 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/MainActivity.cs @@ -15,8 +15,7 @@ protected override void OnCreate(Bundle bundle) AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); AddTestAssembly(Assembly.GetExecutingAssembly()); - AutoStart = true; - //TerminateAfterExecution = true; + AutoStart = true; // this is necessary in order for the CI test job to work base.OnCreate(bundle); } diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt deleted file mode 100644 index c2bca974..00000000 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/AboutResources.txt +++ /dev/null @@ -1,44 +0,0 @@ -Images, layout descriptions, binary blobs and string dictionaries can be included -in your application as resource files. Various Android APIs are designed to -operate on the resource IDs instead of dealing with images, strings or binary blobs -directly. - -For example, a sample Android app that contains a user interface layout (main.axml), -an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) -would keep its resources in the "Resources" directory of the application: - -Resources/ - drawable/ - icon.png - - layout/ - main.axml - - values/ - strings.xml - -In order to get the build system to recognize Android resources, set the build action to -"AndroidResource". The native Android APIs do not operate directly with filenames, but -instead operate on resource IDs. When you compile an Android application that uses resources, -the build system will package the resources for distribution and generate a class called "R" -(this is an Android convention) that contains the tokens for each one of the resources -included. For example, for the above Resources layout, this is what the R class would expose: - -public class R { - public class drawable { - public const int icon = 0x123; - } - - public class layout { - public const int main = 0x456; - } - - public class strings { - public const int first_string = 0xabc; - public const int second_string = 0xbcd; - } -} - -You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main -to reference the layout/main.axml file, or R.strings.first_string to reference the first -string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs index fa931387..46ae1823 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AppDelegate.cs @@ -20,8 +20,7 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary options) AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); AddTestAssembly(Assembly.GetExecutingAssembly()); - AutoStart = true; - TerminateAfterExecution = true; + AutoStart = true; // this is necessary in order for the CI test job to work return base.FinishedLaunching(app, options); } From fdc1179153871cb5c3bd304cdcedfb6cdbbd2cf0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 19:24:18 -0700 Subject: [PATCH 133/254] actually run the tests --- .circleci/config.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 796d65bb..25d5f256 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,6 +70,31 @@ jobs: # Note, xabuild is just a wrapper for msbuild that adds various tools paths etc. necessary for building an # Android app. See: https://github.com/xamarin/xamarin-android/blob/master/tools/scripts/xabuild + - run: + name: Set up emulator + command: echo no | avdmanager create avd -n xm-android -f -k "system-images;android-24;default;armeabi-v7a" + + - run: + name: Start emulator + command: emulator -avd xm-android -netdelay none -netspeed full -no-audio -no-window -no-snapshot -use-system-libs -no-boot-anim + background: true + timeout: 1200 + no_output_timeout: 20m + + - run: + name: Start capturing log output + command: adb logcat + background: true + no_output_timeout: 5m + + - run: + name: Deploy app to emulator + command: adb install Droid/bin/Debug/com.launchdarkly.xamarin_mobile_restwrapper-Signed.apk + + - run: + name: Start app in emulator + command: adb shell monkey -p com.launchdarkly.xamarinandroidtests 1 + test-ios: macos: xcode: "10.2.1" From 0744bd25b9a7f87a155bcbb7d88030c98c90f0c5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 26 Jun 2019 19:37:49 -0700 Subject: [PATCH 134/254] install libpulse0 --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 25d5f256..2b98ee6f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,8 @@ jobs: sudo apt -y install apt-transport-https dirmngr gnupg ca-certificates sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - sudo apt -y install mono-devel nuget libzip4 + sudo apt -y install mono-devel nuget libzip4 libpulse0 + # libpulse0 is required to run the emulator; the result are required to build the app - restore_cache: key: xamarin-android-cache-v9-2-99-172 From 5c9b1c709c16351cd7ebf53de6270ae4826af105 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 27 Jun 2019 15:42:22 -0700 Subject: [PATCH 135/254] misc Android CI fixes --- .circleci/config.yml | 8 ++++++-- scripts/android-wait-for-boot.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100755 scripts/android-wait-for-boot.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b98ee6f..9e892890 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ jobs: - run: name: Build test project - command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild /restore tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild /restore /t:SignAndroidPackage tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj # Note, xabuild is just a wrapper for msbuild that adds various tools paths etc. necessary for building an # Android app. See: https://github.com/xamarin/xamarin-android/blob/master/tools/scripts/xabuild @@ -82,6 +82,10 @@ jobs: timeout: 1200 no_output_timeout: 20m + - run: + name: Wait for emulator to become available + command: ./scripts/android-wait-for-boot.sh + - run: name: Start capturing log output command: adb logcat @@ -90,7 +94,7 @@ jobs: - run: name: Deploy app to emulator - command: adb install Droid/bin/Debug/com.launchdarkly.xamarin_mobile_restwrapper-Signed.apk + command: adb install tests/LaunchDarkly.XamarinSdk.Android.Tests/bin/Debug/com.launchdarkly.xamarinandroidtests-Signed.apk - run: name: Start app in emulator diff --git a/scripts/android-wait-for-boot.sh b/scripts/android-wait-for-boot.sh new file mode 100755 index 00000000..abf11812 --- /dev/null +++ b/scripts/android-wait-for-boot.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# used only for CI build + +# Origin from https://github.com/travis-ci/travis-cookbooks/blob/master/community-cookbooks/android-sdk/files/default/android-wait-for-emulator + +set +e + +bootanim="" +failcounter=0 +timeout_in_sec=360 + +echo -n "Waiting for emulator to start" + +until [[ "$bootanim" =~ "stopped" ]]; do + bootanim=`adb shell getprop init.svc.bootanim 2>&1 &` + if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" + || "$bootanim" =~ "running" ]]; then + let "failcounter += 1" + echo -n "." + if [[ $failcounter -gt $timeout_in_sec ]]; then + echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" + exit 1 + fi + fi + sleep 2 +done + +echo " ready" From 9d6eb2e755027506e4bc8791423c30fa8264e6ea Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 27 Jun 2019 16:14:48 -0700 Subject: [PATCH 136/254] better test log output --- .circleci/config.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9e892890..b9f59b3f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,7 +88,7 @@ jobs: - run: name: Start capturing log output - command: adb logcat + command: adb logcat | grep 'mono-stdout:' >test-run.log background: true no_output_timeout: 5m @@ -100,6 +100,19 @@ jobs: name: Start app in emulator command: adb shell monkey -p com.launchdarkly.xamarinandroidtests 1 + - run: + name: Wait for tests to finish running + # https://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found + command: "( tail -f -c+0 test-run.log & ) | grep -q 'Tests run:'" + + - run: + name: Show all test output + command: | + cat test-run.log | tr -s ' ' | cut -d ' ' -f 1,2,7- + if grep '\[FAIL\]' test-run.log >/dev/null; then exit 1; fi + # "exit 1" causes the CI job to fail if there were any test failures. Note that we still won't have a + # JUnit-compatible test results file; you'll just have to look at the output. + test-ios: macos: xcode: "10.2.1" From 3c4cbc4f0d2bbb6156b0c606ecfc26c9fb424090 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 28 Jun 2019 11:00:35 -0700 Subject: [PATCH 137/254] better log capturing --- .circleci/config.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b9f59b3f..681223b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,9 +88,10 @@ jobs: - run: name: Start capturing log output - command: adb logcat | grep 'mono-stdout:' >test-run.log + command: adb logcat mono-stdout:D *:S | tee test-run.log + # mono-stdout is the default tag for standard output from a Xamarin app - that's where our test runner output goes background: true - no_output_timeout: 5m + no_output_timeout: 10m - run: name: Deploy app to emulator @@ -158,7 +159,7 @@ jobs: - run: name: Start capturing log output - command: xcrun simctl spawn booted log stream --predicate 'senderImagePath contains "LaunchDarkly.XamarinSdk.iOS.Tests"' >test-run.log + command: xcrun simctl spawn booted log stream --predicate 'senderImagePath contains "LaunchDarkly.XamarinSdk.iOS.Tests"' | tee test-run.log background: true - run: From 72cfdcba822878ce341f3ca567842ca669847e35 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 28 Jun 2019 14:31:33 -0700 Subject: [PATCH 138/254] skip Android tests that use WireMock (for now) + misc test fixes --- .../Properties/AndroidManifest.xml | 1 + .../LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 53 ++++++++++++++----- .../FeatureFlagRequestorTests.cs | 8 +-- .../LDClientEndToEndTests.cs | 18 +++---- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml index c5781e4c..e6e74d18 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AndroidManifest.xml @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs index 36ee2c06..1a1849a6 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Common.Logging; using WireMock.Server; @@ -9,6 +10,16 @@ namespace LaunchDarkly.Xamarin.Tests [Collection("serialize all tests")] public class BaseTest : IDisposable { +#if __ANDROID__ + // WireMock.Net currently doesn't work on Android, so we can't run tests that need an embedded HTTP server. + // Mark such tests with [Fact(Skip = SkipIfCannotCreateHttpServer)] or [Theory(Skip = SkipIfCannotCreateHttpServer)] + // and they'll be skipped on Android (but not on other platforms, because "Skip = null" is a no-op). + // https://github.com/WireMock-Net/WireMock.Net/issues/292 + public const string SkipIfCannotCreateHttpServer = "can't run this test because we can't create an embedded HTTP server on this platform; see BaseTest.cs"; +#else + public const string SkipIfCannotCreateHttpServer = null; +#endif + public BaseTest() { LogManager.Adapter = new LogSinkFactoryAdapter(); @@ -22,7 +33,7 @@ public void Dispose() protected void WithServer(Action a) { - var s = FluentMockServer.Start(); + var s = MakeServer(); try { a(s); @@ -33,17 +44,35 @@ protected void WithServer(Action a) } } - protected async Task WithServerAsync(Func a) - { - var s = FluentMockServer.Start(); - try - { - await a(s); - } - finally - { + protected async Task WithServerAsync(Func a) + { + var s = MakeServer(); + try + { + await a(s); + } + finally + { s.Stop(); - } - } + } + } + + protected FluentMockServer MakeServer() + { +#pragma warning disable RECS0110 // Condition is always 'true' or always 'false' + if (SkipIfCannotCreateHttpServer != null) + { + // Until WireMock.Net supports all of our platforms, we'll need to mark any tests that use an embedded server + // with [ConditionalFact(Condition = TestCondition.CanRunHttpServer)] or [ConditionalFact(Condition = TestCondition.CanRunHttpServer)] + // instead of [Fact] or [Theory]; otherwise, you'll see this error. + throw new Exception("tried to create an embedded HTTP server on a platform that doesn't support it; see BaseTest.cs"); + } +#pragma warning restore RECS0110 // Condition is always 'true' or always 'false' + + // currently we don't need to customize any server settings +#pragma warning disable CS0162 // Unreachable code detected + return FluentMockServer.Start(); +#pragma warning restore CS0162 // Unreachable code detected + } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index 5c0dd584..9788ef5d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -16,7 +16,7 @@ public class FeatureFlagRequestorTests : BaseTest private const string _allDataJson = "{}"; // Note that in this implementation, unlike the .NET SDK, FeatureFlagRequestor does not unmarshal the response - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public async Task GetFlagsUsesCorrectUriAndMethodInGetModeAsync() { await WithServerAsync(async server => @@ -42,7 +42,7 @@ await WithServerAsync(async server => }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public async Task GetFlagsUsesCorrectUriAndMethodInGetModeWithReasonsAsync() { await WithServerAsync(async server => @@ -68,7 +68,7 @@ await WithServerAsync(async server => }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync() { await WithServerAsync(async server => @@ -97,7 +97,7 @@ await WithServerAsync(async server => }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public async Task GetFlagsUsesCorrectUriAndMethodInReportModeWithReasonsAsync() { await WithServerAsync(async server => diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 37dd9052..0e172ad9 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -39,7 +39,7 @@ public class LdClientEndToEndTests : BaseTest { new object[] { UpdateMode.Streaming } } }; - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public void InitGetsFlagsSync(UpdateMode mode) { @@ -56,7 +56,7 @@ public void InitGetsFlagsSync(UpdateMode mode) }); } - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public async Task InitGetsFlagsAsync(UpdateMode mode) { @@ -72,7 +72,7 @@ await WithServerAsync(async server => }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public void InitCanTimeOutSync() { WithServer(server => @@ -93,7 +93,7 @@ public void InitCanTimeOutSync() }); } - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public void InitFailsOn401Sync(UpdateMode mode) { @@ -121,7 +121,7 @@ public void InitFailsOn401Sync(UpdateMode mode) }); } - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public async Task InitFailsOn401Async(UpdateMode mode) { @@ -144,7 +144,7 @@ await WithServerAsync(async server => }); } - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { @@ -172,7 +172,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) }); } - [Theory] + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public async Task IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode mode) { @@ -200,7 +200,7 @@ await WithServerAsync(async server => }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public void OfflineClientUsesCachedFlagsSync() { WithServer(server => @@ -226,7 +226,7 @@ public void OfflineClientUsesCachedFlagsSync() }); } - [Fact] + [Fact(Skip = SkipIfCannotCreateHttpServer)] public async Task OfflineClientUsesCachedFlagsAsync() { await WithServerAsync(async server => From bbdf0f205cb063120942eef2600bf773efe7bf97 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 28 Jun 2019 15:00:34 -0700 Subject: [PATCH 139/254] rm unused import --- tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs index 1a1849a6..e020d44e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Common.Logging; using WireMock.Server; From 47011c1c38ce6e1de8e3f352514e780564f04c07 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 11:58:25 -0700 Subject: [PATCH 140/254] use CircleCI helper script --- .circleci/config.yml | 2 +- scripts/android-wait-for-boot.sh | 29 ----------------------------- 2 files changed, 1 insertion(+), 30 deletions(-) delete mode 100755 scripts/android-wait-for-boot.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 681223b3..b371d428 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,7 +84,7 @@ jobs: - run: name: Wait for emulator to become available - command: ./scripts/android-wait-for-boot.sh + command: circle-android wait-for-boot - run: name: Start capturing log output diff --git a/scripts/android-wait-for-boot.sh b/scripts/android-wait-for-boot.sh deleted file mode 100755 index abf11812..00000000 --- a/scripts/android-wait-for-boot.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -# used only for CI build - -# Origin from https://github.com/travis-ci/travis-cookbooks/blob/master/community-cookbooks/android-sdk/files/default/android-wait-for-emulator - -set +e - -bootanim="" -failcounter=0 -timeout_in_sec=360 - -echo -n "Waiting for emulator to start" - -until [[ "$bootanim" =~ "stopped" ]]; do - bootanim=`adb shell getprop init.svc.bootanim 2>&1 &` - if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" - || "$bootanim" =~ "running" ]]; then - let "failcounter += 1" - echo -n "." - if [[ $failcounter -gt $timeout_in_sec ]]; then - echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" - exit 1 - fi - fi - sleep 2 -done - -echo " ready" From 1f28e35830f9802167ca3673862be5b0abf85030 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 12:38:49 -0700 Subject: [PATCH 141/254] comment --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index b371d428..25ade6af 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,6 +85,8 @@ jobs: - run: name: Wait for emulator to become available command: circle-android wait-for-boot + # this script is provided in the CircleCI Android images: + # https://raw.githubusercontent.com/circleci/circleci-images/master/android/bin/circle-android - run: name: Start capturing log output From fdef9dd97b4508dcee00b960b12795cc7749bc74 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 14:26:03 -0700 Subject: [PATCH 142/254] Implement unique anon user key in .NET Standard --- .../DefaultDeviceInfo.shared.cs | 14 +++++++ .../DeviceInfo.shared.cs | 13 ------- src/LaunchDarkly.XamarinSdk/Factory.shared.cs | 2 +- .../LaunchDarkly.XamarinSdk.csproj | 18 ++++++--- .../ClientIdentifier.android.cs | 13 +++++++ .../PlatformSpecific/ClientIdentifier.ios.cs | 13 +++++++ .../ClientIdentifier.netstandard.cs | 14 +++++++ .../ClientIdentifier.shared.cs | 37 +++++++++++++++++++ .../LDClientEndToEndTests.cs | 30 +++++++++++++++ 9 files changed, 135 insertions(+), 19 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.netstandard.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs new file mode 100644 index 00000000..704f5523 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs @@ -0,0 +1,14 @@ +using LaunchDarkly.Xamarin.PlatformSpecific; + +namespace LaunchDarkly.Xamarin +{ + // This just delegates to the conditionally-compiled code in LaunchDarkly.Xamarin.PlatformSpecific. + // The only reason it is a pluggable component is for unit tests; we don't currently expose IDeviceInfo. + public class DefaultDeviceInfo : IDeviceInfo + { + public string UniqueDeviceId() + { + return ClientIdentifier.PlatformGetOrCreateClientId(); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs deleted file mode 100644 index 1d576528..00000000 --- a/src/LaunchDarkly.XamarinSdk/DeviceInfo.shared.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Plugin.DeviceInfo; - -namespace LaunchDarkly.Xamarin -{ - public class DeviceInfo : IDeviceInfo - { - public string UniqueDeviceId() - { - return CrossDeviceInfo.Current.Id; - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Factory.shared.cs b/src/LaunchDarkly.XamarinSdk/Factory.shared.cs index 8815fe43..3e98be0f 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.shared.cs @@ -81,7 +81,7 @@ internal static IPersistentStorage CreatePersistentStorage(Configuration configu internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) { - return configuration.DeviceInfo ?? new DeviceInfo(); + return configuration.DeviceInfo ?? new DefaultDeviceInfo(); } internal static IFeatureFlagListenerManager CreateFeatureFlagListenerManager(Configuration configuration) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 83070170..f9ec405f 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -23,49 +23,57 @@ - - - - - + + + + + + + + + + + + + diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs new file mode 100644 index 00000000..444a32e2 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs @@ -0,0 +1,13 @@ +using Plugin.DeviceInfo; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class ClientIdentifier + { + // For mobile platforms that really have a device ID, we delegate to Plugin.DeviceInfo to get the ID. + public static string PlatformGetOrCreateClientId() + { + return CrossDeviceInfo.Current.Id(); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs new file mode 100644 index 00000000..444a32e2 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs @@ -0,0 +1,13 @@ +using Plugin.DeviceInfo; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class ClientIdentifier + { + // For mobile platforms that really have a device ID, we delegate to Plugin.DeviceInfo to get the ID. + public static string PlatformGetOrCreateClientId() + { + return CrossDeviceInfo.Current.Id(); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.netstandard.cs new file mode 100644 index 00000000..feae56e0 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.netstandard.cs @@ -0,0 +1,14 @@ + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class ClientIdentifier + { + // Unlike mobile platforms, .NET standard doesn't have an OS-based notion of a device identifier. + // Instead, we'll do what we do in the non-mobile client-side SDKs: see if we've already cached a + // user key for this (OS) user account, and if not, generate a randomized ID and cache it. + public static string PlatformGetOrCreateClientId() + { + return GetOrCreateRandomizedClientId(); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs new file mode 100644 index 00000000..8f418bb9 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs @@ -0,0 +1,37 @@ +using System; +using LaunchDarkly.Xamarin.Preferences; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class ClientIdentifier + { + private const string PreferencesAnonUserIdKey = "anonUserId"; + + public static string GetOrCreateClientId() + { + return PlatformGetOrCreateClientId(); + } + + // Used only for testing, to keep previous calls to GetOrCreateRandomizedClientId from affecting test state. + // On mobile platforms this has no effect. + internal static void ClearCachedClientId() + { + Preferences.Preferences.Clear(PreferencesAnonUserIdKey); + } + + private static string GetOrCreateRandomizedClientId() + { + // On non-mobile platforms, there may not be an OS-based notion of a device identifier. Instead, + // we'll do what we do in the non-mobile client-side SDKs: see if we've already cached a user key + // for this user account (OS user, that is), and if not, generate a randomized ID and cache it. + string cachedKey = Preferences.Preferences.Get(PreferencesAnonUserIdKey, null); + if (cachedKey != null) + { + return cachedKey; + } + string guid = Guid.NewGuid().ToString(); + Preferences.Preferences.Set(PreferencesAnonUserIdKey, guid); + return guid; + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 0e172ad9..183c7dc5 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Common.Logging; using LaunchDarkly.Client; +using LaunchDarkly.Xamarin.PlatformSpecific; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WireMock.Server; @@ -144,6 +145,35 @@ await WithServerAsync(async server => }); } + [Fact(Skip = SkipIfCannotCreateHttpServer)] + public async Task InitWithKeylessAnonUserAddsKeyAndReusesIt() + { + // Note, we don't care about polling mode vs. streaming mode for this functionality. + await WithServerAsync(async server => + { + server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + var anonUser = User.WithKey(null).AndAnonymous(true); + + // Note, on mobile platforms, the generated user key is the device ID and is stable; on other platforms, + // it's a GUID that is cached in local storage. Calling ClearCachedClientId() resets the latter. + ClientIdentifier.ClearCachedClientId(); + + string generatedKey = null; + using (var client = await TestUtil.CreateClientAsync(config, anonUser)) + { + Assert.NotNull(client.User.Key); + generatedKey = client.User.Key; + } + + using (var client = await TestUtil.CreateClientAsync(config, anonUser)) + { + Assert.Equal(generatedKey, client.User.Key); + } + }); + } + [Theory(Skip = SkipIfCannotCreateHttpServer)] [MemberData(nameof(PollingAndStreaming))] public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) From 18339bd4ad6b06ad2b9e63cca233c022573c9c47 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 14:34:27 -0700 Subject: [PATCH 143/254] syntax error --- .../PlatformSpecific/ClientIdentifier.android.cs | 2 +- .../PlatformSpecific/ClientIdentifier.ios.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs index 444a32e2..7d3d398b 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.android.cs @@ -7,7 +7,7 @@ internal static partial class ClientIdentifier // For mobile platforms that really have a device ID, we delegate to Plugin.DeviceInfo to get the ID. public static string PlatformGetOrCreateClientId() { - return CrossDeviceInfo.Current.Id(); + return CrossDeviceInfo.Current.Id; } } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs index 444a32e2..7d3d398b 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.ios.cs @@ -7,7 +7,7 @@ internal static partial class ClientIdentifier // For mobile platforms that really have a device ID, we delegate to Plugin.DeviceInfo to get the ID. public static string PlatformGetOrCreateClientId() { - return CrossDeviceInfo.Current.Id(); + return CrossDeviceInfo.Current.Id; } } } From 08e48dd76e59701e62f2b283f7fd2b0c0eb14c6e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 14:38:17 -0700 Subject: [PATCH 144/254] list tests in .NET Standard --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 25ade6af..6432a0c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ jobs: - checkout - run: dotnet restore - run: dotnet build src/LaunchDarkly.XamarinSdk -f netstandard2.0 - - run: dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 + - run: dotnet test -v=normal tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 test-android: docker: From ee2dd4dce1611930d08ec4ea99fd8e83fa1a915b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 14:56:11 -0700 Subject: [PATCH 145/254] suppress ASP.NET debug output in iOS --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6432a0c7..815ea4b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,7 +147,7 @@ jobs: - run: name: Build test project - command: msbuild /restore tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj + command: msbuild /restore tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj -p:MtouchExtraArgs="--setenv:ASPNETCORE_SUPPRESSSTATUSMESSAGES=true" - run: name: Start simulator From b0b991019977ea07a3cdae2e5650d72d823e29c8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 15:07:39 -0700 Subject: [PATCH 146/254] Revert "suppress ASP.NET debug output in iOS" This reverts commit ee2dd4dce1611930d08ec4ea99fd8e83fa1a915b. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 815ea4b2..6432a0c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,7 +147,7 @@ jobs: - run: name: Build test project - command: msbuild /restore tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj -p:MtouchExtraArgs="--setenv:ASPNETCORE_SUPPRESSSTATUSMESSAGES=true" + command: msbuild /restore tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj - run: name: Start simulator From 6b731901654837328ce31f05378e545731d17379 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 15:15:09 -0700 Subject: [PATCH 147/254] fix test state cleanup --- .../PlatformSpecific/ClientIdentifier.shared.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs index 8f418bb9..81e6b116 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs @@ -16,7 +16,7 @@ public static string GetOrCreateClientId() // On mobile platforms this has no effect. internal static void ClearCachedClientId() { - Preferences.Preferences.Clear(PreferencesAnonUserIdKey); + Preferences.Preferences.Remove(PreferencesAnonUserIdKey); } private static string GetOrCreateRandomizedClientId() From 7025940962a4933f248ddab6d0b543281f25d73d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 15:30:37 -0700 Subject: [PATCH 148/254] add lower-level unit test for device ID --- .../DefaultDeviceInfoTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/DefaultDeviceInfoTests.cs diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/DefaultDeviceInfoTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/DefaultDeviceInfoTests.cs new file mode 100644 index 00000000..e376326f --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/DefaultDeviceInfoTests.cs @@ -0,0 +1,26 @@ +using LaunchDarkly.Xamarin.PlatformSpecific; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + // The DefaultDeviceInfo functionality is also tested by LdClientEndToEndTests.InitWithKeylessAnonUserAddsKeyAndReusesIt(), + // which is a more realistic test since it uses a full client instance. However, currently LdClientEndToEndTests can't be + // run on every platform, so we'll also test the lower-level logic here. + public class DefaultDeviceInfoTests : BaseTest + { + [Fact] + public void UniqueDeviceIdGeneratesStableValue() + { + // Note, on mobile platforms, the generated user key is the device ID and is stable; on other platforms, + // it's a GUID that is cached in local storage. Calling ClearCachedClientId() resets the latter. + ClientIdentifier.ClearCachedClientId(); + + var ddi = new DefaultDeviceInfo(); + var id0 = ddi.UniqueDeviceId(); + Assert.NotNull(id0); + + var id1 = ddi.UniqueDeviceId(); + Assert.Equal(id0, id1); + } + } +} From 807428d5f19f59fe0bbf8c4133901e8f775bb002 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 18:11:27 -0700 Subject: [PATCH 149/254] clean up structure of conditionally compiled code --- .../{AsyncUtils.shared.cs => AsyncUtils.cs} | 0 ...ientRunMode.shared.cs => ClientRunMode.cs} | 0 ...nfiguration.shared.cs => Configuration.cs} | 0 .../Connectivity/Connectivity.shared.enums.cs | 42 ------------------- .../{Constants.shared.cs => Constants.cs} | 0 ...iceInfo.shared.cs => DefaultDeviceInfo.cs} | 0 ....shared.cs => DefaultPersistentStorage.cs} | 6 +-- .../{Extensions.shared.cs => Extensions.cs} | 0 .../{Factory.shared.cs => Factory.cs} | 0 .../{FeatureFlag.shared.cs => FeatureFlag.cs} | 0 ...hared.cs => FeatureFlagListenerManager.cs} | 0 ...stor.shared.cs => FeatureFlagRequestor.cs} | 0 ...eManager.shared.cs => FlagCacheManager.cs} | 0 ...anager.shared.cs => IConnectionManager.cs} | 0 .../{IDeviceInfo.shared.cs => IDeviceInfo.cs} | 0 ...ener.shared.cs => IFeatureFlagListener.cs} | 0 ...ared.cs => IFeatureFlagListenerManager.cs} | 0 ...Manager.shared.cs => IFlagCacheManager.cs} | 0 ...ileClient.shared.cs => ILdMobileClient.cs} | 0 ...tion.shared.cs => IMobileConfiguration.cs} | 0 ...or.shared.cs => IMobileUpdateProcessor.cs} | 0 ...torage.shared.cs => IPersistentStorage.cs} | 0 ...rFlagCache.shared.cs => IUserFlagCache.cs} | 0 .../LaunchDarkly.XamarinSdk.csproj | 6 +-- .../{LdClient.shared.cs => LdClient.cs} | 9 ++-- ...r.shared.cs => MobileConnectionManager.cs} | 7 ++-- ...or.shared.cs => MobilePollingProcessor.cs} | 0 ...ared.cs => MobileSideClientEnvironment.cs} | 0 ....shared.cs => MobileStreamingProcessor.cs} | 0 .../BackgroundDetection.android.cs | 2 +- .../BackgroundDetection.ios.cs | 2 +- .../BackgroundDetection.netstandard.cs | 2 +- .../BackgroundDetection.shared.cs | 2 +- .../ClientIdentifier.shared.cs | 7 ++-- .../Connectivity.android.cs | 2 +- .../Connectivity.ios.cs | 2 +- .../Connectivity.ios.reachability.cs | 0 .../Connectivity.netstandard.cs | 2 +- .../Connectivity.shared.cs | 20 ++++++++- .../Permissions.android.cs | 2 +- .../Permissions.shared.cs} | 2 +- .../Platform.android.cs | 2 +- .../Platform.shared.cs | 2 +- .../Preferences.android.cs | 2 +- .../Preferences.ios.cs | 2 +- .../Preferences.netstandard.cs | 2 +- .../Preferences.shared.cs | 2 +- .../UserMetadata.android.cs | 2 +- .../UserMetadata.ios.cs | 2 +- .../UserMetadata.netstandard.cs | 2 +- .../UserMetadata.shared.cs | 2 +- ...AssemblyInfo.shared.cs => AssemblyInfo.cs} | 0 ...Cache.shared.cs => UserFlagDeviceCache.cs} | 0 ...che.shared.cs => UserFlagInMemoryCache.cs} | 0 .../{ValueType.shared.cs => ValueType.cs} | 0 55 files changed, 55 insertions(+), 80 deletions(-) rename src/LaunchDarkly.XamarinSdk/{AsyncUtils.shared.cs => AsyncUtils.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{ClientRunMode.shared.cs => ClientRunMode.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{Configuration.shared.cs => Configuration.cs} (100%) delete mode 100644 src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs rename src/LaunchDarkly.XamarinSdk/{Constants.shared.cs => Constants.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{DefaultDeviceInfo.shared.cs => DefaultDeviceInfo.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{DefaultPersistentStorage.shared.cs => DefaultPersistentStorage.cs} (59%) rename src/LaunchDarkly.XamarinSdk/{Extensions.shared.cs => Extensions.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{Factory.shared.cs => Factory.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{FeatureFlag.shared.cs => FeatureFlag.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{FeatureFlagListenerManager.shared.cs => FeatureFlagListenerManager.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{FeatureFlagRequestor.shared.cs => FeatureFlagRequestor.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{FlagCacheManager.shared.cs => FlagCacheManager.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IConnectionManager.shared.cs => IConnectionManager.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IDeviceInfo.shared.cs => IDeviceInfo.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IFeatureFlagListener.shared.cs => IFeatureFlagListener.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IFeatureFlagListenerManager.shared.cs => IFeatureFlagListenerManager.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IFlagCacheManager.shared.cs => IFlagCacheManager.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{ILdMobileClient.shared.cs => ILdMobileClient.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IMobileConfiguration.shared.cs => IMobileConfiguration.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IMobileUpdateProcessor.shared.cs => IMobileUpdateProcessor.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IPersistentStorage.shared.cs => IPersistentStorage.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{IUserFlagCache.shared.cs => IUserFlagCache.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{LdClient.shared.cs => LdClient.cs} (96%) rename src/LaunchDarkly.XamarinSdk/{MobileConnectionManager.shared.cs => MobileConnectionManager.cs} (70%) rename src/LaunchDarkly.XamarinSdk/{MobilePollingProcessor.shared.cs => MobilePollingProcessor.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{MobileSideClientEnvironment.shared.cs => MobileSideClientEnvironment.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{MobileStreamingProcessor.shared.cs => MobileStreamingProcessor.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{BackgroundDetection => PlatformSpecific}/BackgroundDetection.android.cs (96%) rename src/LaunchDarkly.XamarinSdk/{BackgroundDetection => PlatformSpecific}/BackgroundDetection.ios.cs (95%) rename src/LaunchDarkly.XamarinSdk/{BackgroundDetection => PlatformSpecific}/BackgroundDetection.netstandard.cs (90%) rename src/LaunchDarkly.XamarinSdk/{BackgroundDetection => PlatformSpecific}/BackgroundDetection.shared.cs (97%) rename src/LaunchDarkly.XamarinSdk/{Connectivity => PlatformSpecific}/Connectivity.android.cs (99%) rename src/LaunchDarkly.XamarinSdk/{Connectivity => PlatformSpecific}/Connectivity.ios.cs (98%) rename src/LaunchDarkly.XamarinSdk/{Connectivity => PlatformSpecific}/Connectivity.ios.reachability.cs (100%) rename src/LaunchDarkly.XamarinSdk/{Connectivity => PlatformSpecific}/Connectivity.netstandard.cs (96%) rename src/LaunchDarkly.XamarinSdk/{Connectivity => PlatformSpecific}/Connectivity.shared.cs (92%) rename src/LaunchDarkly.XamarinSdk/{Permissions => PlatformSpecific}/Permissions.android.cs (99%) rename src/LaunchDarkly.XamarinSdk/{Permissions/Permissions.shared.enums.cs => PlatformSpecific/Permissions.shared.cs} (97%) rename src/LaunchDarkly.XamarinSdk/{Platform => PlatformSpecific}/Platform.android.cs (99%) rename src/LaunchDarkly.XamarinSdk/{Platform => PlatformSpecific}/Platform.shared.cs (96%) rename src/LaunchDarkly.XamarinSdk/{Preferences => PlatformSpecific}/Preferences.android.cs (98%) rename src/LaunchDarkly.XamarinSdk/{Preferences => PlatformSpecific}/Preferences.ios.cs (98%) rename src/LaunchDarkly.XamarinSdk/{Preferences => PlatformSpecific}/Preferences.netstandard.cs (99%) rename src/LaunchDarkly.XamarinSdk/{Preferences => PlatformSpecific}/Preferences.shared.cs (99%) rename src/LaunchDarkly.XamarinSdk/{UserMetadata => PlatformSpecific}/UserMetadata.android.cs (85%) rename src/LaunchDarkly.XamarinSdk/{UserMetadata => PlatformSpecific}/UserMetadata.ios.cs (94%) rename src/LaunchDarkly.XamarinSdk/{UserMetadata => PlatformSpecific}/UserMetadata.netstandard.cs (78%) rename src/LaunchDarkly.XamarinSdk/{UserMetadata => PlatformSpecific}/UserMetadata.shared.cs (95%) rename src/LaunchDarkly.XamarinSdk/Properties/{AssemblyInfo.shared.cs => AssemblyInfo.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{UserFlagDeviceCache.shared.cs => UserFlagDeviceCache.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{UserFlagInMemoryCache.shared.cs => UserFlagInMemoryCache.cs} (100%) rename src/LaunchDarkly.XamarinSdk/{ValueType.shared.cs => ValueType.cs} (100%) diff --git a/src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs b/src/LaunchDarkly.XamarinSdk/AsyncUtils.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/AsyncUtils.shared.cs rename to src/LaunchDarkly.XamarinSdk/AsyncUtils.cs diff --git a/src/LaunchDarkly.XamarinSdk/ClientRunMode.shared.cs b/src/LaunchDarkly.XamarinSdk/ClientRunMode.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/ClientRunMode.shared.cs rename to src/LaunchDarkly.XamarinSdk/ClientRunMode.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.shared.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Configuration.shared.cs rename to src/LaunchDarkly.XamarinSdk/Configuration.cs diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs b/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs deleted file mode 100644 index 88e37766..00000000 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.enums.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -namespace LaunchDarkly.Xamarin.Connectivity -{ - internal enum ConnectionProfile - { - Unknown = 0, - Bluetooth = 1, - Cellular = 2, - Ethernet = 3, - WiFi = 4 - } - - internal enum NetworkAccess - { - Unknown = 0, - None = 1, - Local = 2, - ConstrainedInternet = 3, - Internet = 4 - } -} diff --git a/src/LaunchDarkly.XamarinSdk/Constants.shared.cs b/src/LaunchDarkly.XamarinSdk/Constants.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Constants.shared.cs rename to src/LaunchDarkly.XamarinSdk/Constants.cs diff --git a/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.shared.cs rename to src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs diff --git a/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs similarity index 59% rename from src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs rename to src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs index 10a69193..37af6cdf 100644 --- a/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs @@ -1,4 +1,4 @@ -using System; +using LaunchDarkly.Xamarin.PlatformSpecific; namespace LaunchDarkly.Xamarin { @@ -6,12 +6,12 @@ internal class DefaultPersistentStorage : IPersistentStorage { public void Save(string key, string value) { - LaunchDarkly.Xamarin.Preferences.Preferences.Set(key, value); + Preferences.Set(key, value); } public string GetValue(string key) { - return LaunchDarkly.Xamarin.Preferences.Preferences.Get(key, null); + return Preferences.Get(key, null); } } } diff --git a/src/LaunchDarkly.XamarinSdk/Extensions.shared.cs b/src/LaunchDarkly.XamarinSdk/Extensions.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Extensions.shared.cs rename to src/LaunchDarkly.XamarinSdk/Extensions.cs diff --git a/src/LaunchDarkly.XamarinSdk/Factory.shared.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Factory.shared.cs rename to src/LaunchDarkly.XamarinSdk/Factory.cs diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlag.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlag.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/FeatureFlag.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlag.cs diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.shared.cs rename to src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs diff --git a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/FlagCacheManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/IConnectionManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IConnectionManager.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IConnectionManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IConnectionManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/IDeviceInfo.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IDeviceInfo.shared.cs rename to src/LaunchDarkly.XamarinSdk/IDeviceInfo.cs diff --git a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.shared.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs diff --git a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/IFlagCacheManager.shared.cs b/src/LaunchDarkly.XamarinSdk/IFlagCacheManager.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IFlagCacheManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/IFlagCacheManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.shared.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/ILdMobileClient.shared.cs rename to src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs diff --git a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IMobileConfiguration.shared.cs rename to src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs diff --git a/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs diff --git a/src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs b/src/LaunchDarkly.XamarinSdk/IPersistentStorage.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IPersistentStorage.shared.cs rename to src/LaunchDarkly.XamarinSdk/IPersistentStorage.cs diff --git a/src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/IUserFlagCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index f9ec405f..08362d91 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -25,14 +25,14 @@ + + - - @@ -43,7 +43,6 @@ - @@ -70,7 +69,6 @@ - diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs similarity index 96% rename from src/LaunchDarkly.XamarinSdk/LdClient.shared.cs rename to src/LaunchDarkly.XamarinSdk/LdClient.cs index 9dedb090..e4d5058d 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -6,6 +6,7 @@ using Common.Logging; using LaunchDarkly.Client; using LaunchDarkly.Common; +using LaunchDarkly.Xamarin.PlatformSpecific; using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin @@ -87,7 +88,7 @@ public sealed class LdClient : ILdMobileClient eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); SetupConnectionManager(); - BackgroundDetection.BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; + BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; } ///

@@ -521,7 +522,7 @@ void Dispose(bool disposing) { Log.InfoFormat("Shutting down the LaunchDarkly client"); - BackgroundDetection.BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; + BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; updateProcessor.Dispose(); eventProcessor.Dispose(); } @@ -548,12 +549,12 @@ public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener l flagListenerManager.UnregisterListener(listener, flagKey); } - internal void OnBackgroundModeChanged(object sender, BackgroundDetection.BackgroundModeChangedEventArgs args) + internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); } - internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundDetection.BackgroundModeChangedEventArgs args) + internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) { if (args.IsInBackground) { diff --git a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs similarity index 70% rename from src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs index cf2bb06c..0de11a80 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs @@ -1,4 +1,5 @@ using System; +using LaunchDarkly.Xamarin.PlatformSpecific; namespace LaunchDarkly.Xamarin { @@ -9,7 +10,7 @@ internal class MobileConnectionManager : IConnectionManager internal MobileConnectionManager() { UpdateConnectedStatus(); - LaunchDarkly.Xamarin.Connectivity.Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; } bool isConnected; @@ -22,7 +23,7 @@ bool IConnectionManager.IsConnected } } - void Connectivity_ConnectivityChanged(object sender, Connectivity.ConnectivityChangedEventArgs e) + void Connectivity_ConnectivityChanged(object sender, ConnectivityChangedEventArgs e) { UpdateConnectedStatus(); ConnectionChanged?.Invoke(isConnected); @@ -30,7 +31,7 @@ void Connectivity_ConnectivityChanged(object sender, Connectivity.ConnectivityCh private void UpdateConnectedStatus() { - isConnected = LaunchDarkly.Xamarin.Connectivity.Connectivity.NetworkAccess == LaunchDarkly.Xamarin.Connectivity.NetworkAccess.Internet; + isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; } } } diff --git a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs diff --git a/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs diff --git a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.shared.cs b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.shared.cs rename to src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs similarity index 96% rename from src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs index ad27eb29..3412324e 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs @@ -3,7 +3,7 @@ using Android.App; using Android.OS; -namespace LaunchDarkly.Xamarin.BackgroundDetection +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class BackgroundDetection { diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.ios.cs similarity index 95% rename from src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.ios.cs index fcd1a1ba..1e3a8342 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.ios.cs @@ -3,7 +3,7 @@ using UIKit; using Foundation; -namespace LaunchDarkly.Xamarin.BackgroundDetection +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class BackgroundDetection { diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.netstandard.cs similarity index 90% rename from src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.netstandard.cs index 0aeef6bf..3d768296 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.netstandard.cs @@ -1,6 +1,6 @@ using System; -namespace LaunchDarkly.Xamarin.BackgroundDetection +namespace LaunchDarkly.Xamarin.PlatformSpecific { // This code is not from Xamarin Essentials, though it implements the same abstraction. It is a stub // that does nothing, since in .NET Standard there is no notion of an application being in the diff --git a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs similarity index 97% rename from src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs index 80dfc9c6..996ec6bf 100644 --- a/src/LaunchDarkly.XamarinSdk/BackgroundDetection/BackgroundDetection.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs @@ -1,6 +1,6 @@ using System; -namespace LaunchDarkly.Xamarin.BackgroundDetection +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class BackgroundDetection { diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs index 81e6b116..18aacadf 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/ClientIdentifier.shared.cs @@ -1,5 +1,4 @@ using System; -using LaunchDarkly.Xamarin.Preferences; namespace LaunchDarkly.Xamarin.PlatformSpecific { @@ -16,7 +15,7 @@ public static string GetOrCreateClientId() // On mobile platforms this has no effect. internal static void ClearCachedClientId() { - Preferences.Preferences.Remove(PreferencesAnonUserIdKey); + Preferences.Remove(PreferencesAnonUserIdKey); } private static string GetOrCreateRandomizedClientId() @@ -24,13 +23,13 @@ private static string GetOrCreateRandomizedClientId() // On non-mobile platforms, there may not be an OS-based notion of a device identifier. Instead, // we'll do what we do in the non-mobile client-side SDKs: see if we've already cached a user key // for this user account (OS user, that is), and if not, generate a randomized ID and cache it. - string cachedKey = Preferences.Preferences.Get(PreferencesAnonUserIdKey, null); + string cachedKey = Preferences.Get(PreferencesAnonUserIdKey, null); if (cachedKey != null) { return cachedKey; } string guid = Guid.NewGuid().ToString(); - Preferences.Preferences.Set(PreferencesAnonUserIdKey, guid); + Preferences.Set(PreferencesAnonUserIdKey, guid); return guid; } } diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs similarity index 99% rename from src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs index d91290db..6db4395d 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs @@ -28,7 +28,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using Android.OS; using Debug = System.Diagnostics.Debug; -namespace LaunchDarkly.Xamarin.Connectivity +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal partial class Connectivity { diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs similarity index 98% rename from src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs index 162f2d18..ccde933e 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; -namespace LaunchDarkly.Xamarin.Connectivity +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class Connectivity { diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.reachability.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.ios.reachability.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.netstandard.cs similarity index 96% rename from src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.netstandard.cs index d0fcf671..614bc3ea 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.netstandard.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace LaunchDarkly.Xamarin.Connectivity +namespace LaunchDarkly.Xamarin.PlatformSpecific { // This code is not from Xamarin Essentials, though it implements the same Connectivity abstraction. // It is a stub that always reports that we do have network connectivity. diff --git a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs similarity index 92% rename from src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs index 1889f9e4..942350d2 100644 --- a/src/LaunchDarkly.XamarinSdk/Connectivity/Connectivity.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs @@ -24,8 +24,26 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Collections.Generic; using System.Linq; -namespace LaunchDarkly.Xamarin.Connectivity +namespace LaunchDarkly.Xamarin.PlatformSpecific { + internal enum ConnectionProfile + { + Unknown = 0, + Bluetooth = 1, + Cellular = 2, + Ethernet = 3, + WiFi = 4 + } + + internal enum NetworkAccess + { + Unknown = 0, + None = 1, + Local = 2, + ConstrainedInternet = 3, + Internet = 4 + } + internal static partial class Connectivity { static event EventHandler ConnectivityChangedInternal; diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs similarity index 99% rename from src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs index ab7a9bc8..1b640e32 100644 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs @@ -30,7 +30,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using Android.Support.V4.App; using Android.Support.V4.Content; -namespace LaunchDarkly.Xamarin.Permissions +namespace LaunchDarkly.Xamarin.PlatformSpecific { // All commented-out code in this file came from Xamarin Essentials but was removed because it is not used by this SDK. // Note that we are no longer using the shared Permissions abstraction at all; this Android code is being used directly diff --git a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.enums.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.shared.cs similarity index 97% rename from src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.enums.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.shared.cs index ce24091a..1faed91c 100644 --- a/src/LaunchDarkly.XamarinSdk/Permissions/Permissions.shared.enums.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.shared.cs @@ -20,7 +20,7 @@ portions of the Software. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace LaunchDarkly.Xamarin.Permissions +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal enum PermissionStatus { diff --git a/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.android.cs similarity index 99% rename from src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.android.cs index 8ea21d35..7e357723 100644 --- a/src/LaunchDarkly.XamarinSdk/Platform/Platform.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.android.cs @@ -34,7 +34,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // All commented-out code in this file came from Xamarin Essentials but was removed because it is not used by this SDK. -namespace LaunchDarkly.Xamarin.Platform +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class Platform { diff --git a/src/LaunchDarkly.XamarinSdk/Platform/Platform.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.shared.cs similarity index 96% rename from src/LaunchDarkly.XamarinSdk/Platform/Platform.shared.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.shared.cs index 59395a07..54a3f996 100644 --- a/src/LaunchDarkly.XamarinSdk/Platform/Platform.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Platform.shared.cs @@ -20,7 +20,7 @@ portions of the Software. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace LaunchDarkly.Xamarin.Platform +namespace LaunchDarkly.Xamarin.PlatformSpecific { #if !NETSTANDARD internal static partial class Platform diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs similarity index 98% rename from src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs index 1e6f8a7c..c958bb64 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs @@ -26,7 +26,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using Android.Content; using Android.Preferences; -namespace LaunchDarkly.Xamarin.Preferences +namespace LaunchDarkly.Xamarin.PlatformSpecific { // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class // to store them. Therefore, the overloads for non-string types have been removed, thereby diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.ios.cs similarity index 98% rename from src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.ios.cs index 616cd03b..90392ea5 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.ios.cs @@ -24,7 +24,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Globalization; using Foundation; -namespace LaunchDarkly.Xamarin.Preferences +namespace LaunchDarkly.Xamarin.PlatformSpecific { // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class // to store them. Therefore, the overloads for non-string types have been removed, thereby diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.netstandard.cs similarity index 99% rename from src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.netstandard.cs index 71fc87f0..e8225639 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.netstandard.cs @@ -9,7 +9,7 @@ using LaunchDarkly.Common; #endif -namespace LaunchDarkly.Xamarin.Preferences +namespace LaunchDarkly.Xamarin.PlatformSpecific { // This code is not from Xamarin Essentials, though it implements the same Preferences abstraction. // diff --git a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.shared.cs similarity index 99% rename from src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.shared.cs index ef221963..9314a6b0 100644 --- a/src/LaunchDarkly.XamarinSdk/Preferences/Preferences.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.shared.cs @@ -22,7 +22,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -namespace LaunchDarkly.Xamarin.Preferences +namespace LaunchDarkly.Xamarin.PlatformSpecific { // Modified for LaunchDarkly: the SDK always serializes values to strings before using this class // to store them. Therefore, the overloads for non-string types have been removed, thereby diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs similarity index 85% rename from src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs index a992e4d0..b6b1bc02 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs @@ -1,6 +1,6 @@ using Android.OS; -namespace LaunchDarkly.Xamarin +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class UserMetadata { diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs similarity index 94% rename from src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs index 6f66f57f..ec7a1b29 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs @@ -1,6 +1,6 @@ using UIKit; -namespace LaunchDarkly.Xamarin +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class UserMetadata { diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs similarity index 78% rename from src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs index 013ebf0e..145b7ac4 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs @@ -1,5 +1,5 @@  -namespace LaunchDarkly.Xamarin +namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class UserMetadata { diff --git a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs similarity index 95% rename from src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs rename to src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs index 3c769c16..7da32ec9 100644 --- a/src/LaunchDarkly.XamarinSdk/UserMetadata/UserMetadata.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs @@ -1,5 +1,5 @@  -namespace LaunchDarkly.Xamarin +namespace LaunchDarkly.Xamarin.PlatformSpecific { // This class and the rest of its partial class implementations are not derived from Xamarin Essentials. diff --git a/src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs b/src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.shared.cs rename to src/LaunchDarkly.XamarinSdk/Properties/AssemblyInfo.cs diff --git a/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs diff --git a/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.shared.cs b/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.shared.cs rename to src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs diff --git a/src/LaunchDarkly.XamarinSdk/ValueType.shared.cs b/src/LaunchDarkly.XamarinSdk/ValueType.cs similarity index 100% rename from src/LaunchDarkly.XamarinSdk/ValueType.shared.cs rename to src/LaunchDarkly.XamarinSdk/ValueType.cs From 06902cf452c12fc4d44145315beeddd19eb62b40 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 18:21:33 -0700 Subject: [PATCH 150/254] merge some iOS code --- .../PlatformSpecific/Connectivity.ios.cs | 156 +++++++++++++++ .../Connectivity.ios.reachability.cs | 187 ------------------ 2 files changed, 156 insertions(+), 187 deletions(-) delete mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs index ccde933e..949e9ca7 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs @@ -84,4 +84,160 @@ static IEnumerable PlatformConnectionProfiles } } } + + enum NetworkStatus + { + NotReachable, + ReachableViaCarrierDataNetwork, + ReachableViaWiFiNetwork + } + + internal static class Reachability + { + internal const string HostName = "www.microsoft.com"; + + internal static NetworkStatus RemoteHostStatus() + { + using (var remoteHostReachability = new NetworkReachability(HostName)) + { + var reachable = remoteHostReachability.TryGetFlags(out var flags); + + if (!reachable) + return NetworkStatus.NotReachable; + + if (!IsReachableWithoutRequiringConnection(flags)) + return NetworkStatus.NotReachable; + + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + return NetworkStatus.ReachableViaCarrierDataNetwork; + + return NetworkStatus.ReachableViaWiFiNetwork; + } + } + + internal static NetworkStatus InternetConnectionStatus() + { + var status = NetworkStatus.NotReachable; + + var defaultNetworkAvailable = IsNetworkAvailable(out var flags); + + // If it's a WWAN connection.. + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + status = NetworkStatus.ReachableViaCarrierDataNetwork; + + // If the connection is reachable and no connection is required, then assume it's WiFi + if (defaultNetworkAvailable) + { + status = NetworkStatus.ReachableViaWiFiNetwork; + } + + // If the connection is on-demand or on-traffic and no user intervention + // is required, then assume WiFi. + if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && + (flags & NetworkReachabilityFlags.InterventionRequired) == 0) + { + status = NetworkStatus.ReachableViaWiFiNetwork; + } + + return status; + } + + internal static IEnumerable GetActiveConnectionType() + { + var status = new List(); + + var defaultNetworkAvailable = IsNetworkAvailable(out var flags); + + // If it's a WWAN connection.. + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + { + status.Add(NetworkStatus.ReachableViaCarrierDataNetwork); + } + else if (defaultNetworkAvailable) + { + status.Add(NetworkStatus.ReachableViaWiFiNetwork); + } + else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && + (flags & NetworkReachabilityFlags.InterventionRequired) == 0) + { + // If the connection is on-demand or on-traffic and no user intervention + // is required, then assume WiFi. + status.Add(NetworkStatus.ReachableViaWiFiNetwork); + } + + return status; + } + + internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags) + { + var ip = new IPAddress(0); + using (var defaultRouteReachability = new NetworkReachability(ip)) + { + if (!defaultRouteReachability.TryGetFlags(out flags)) + return false; + + return IsReachableWithoutRequiringConnection(flags); + } + } + + internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags) + { + // Is it reachable with the current network configuration? + var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0; + + // Do we need a connection to reach it? + var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0; + + // Since the network stack will automatically try to get the WAN up, + // probe that + if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) + noConnectionRequired = true; + + return isReachable && noConnectionRequired; + } + } + + internal class ReachabilityListener : IDisposable + { + NetworkReachability defaultRouteReachability; + NetworkReachability remoteHostReachability; + + internal ReachabilityListener() + { + var ip = new IPAddress(0); + defaultRouteReachability = new NetworkReachability(ip); + defaultRouteReachability.SetNotification(OnChange); + defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); + + remoteHostReachability = new NetworkReachability(Reachability.HostName); + + // Need to probe before we queue, or we wont get any meaningful values + // this only happens when you create NetworkReachability from a hostname + remoteHostReachability.TryGetFlags(out var flags); + + remoteHostReachability.SetNotification(OnChange); + remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); + } + + internal event Action ReachabilityChanged; + + void IDisposable.Dispose() => Dispose(); + + internal void Dispose() + { + defaultRouteReachability?.Dispose(); + defaultRouteReachability = null; + remoteHostReachability?.Dispose(); + remoteHostReachability = null; + } + + async void OnChange(NetworkReachabilityFlags flags) + { + // Add in artifical delay so the connection status has time to change + // else it will return true no matter what. + await Task.Delay(100); + + ReachabilityChanged?.Invoke(); + } + } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs deleted file mode 100644 index 0c8f5018..00000000 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.reachability.cs +++ /dev/null @@ -1,187 +0,0 @@ -/* -Xamarin.Essentials(https://github.com/xamarin/Essentials) code used under MIT License - -The MIT License(MIT) -Copyright(c) Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using CoreFoundation; -using SystemConfiguration; - -namespace LaunchDarkly.Xamarin.Connectivity -{ - enum NetworkStatus - { - NotReachable, - ReachableViaCarrierDataNetwork, - ReachableViaWiFiNetwork - } - - internal static class Reachability - { - internal const string HostName = "www.microsoft.com"; - - internal static NetworkStatus RemoteHostStatus() - { - using (var remoteHostReachability = new NetworkReachability(HostName)) - { - var reachable = remoteHostReachability.TryGetFlags(out var flags); - - if (!reachable) - return NetworkStatus.NotReachable; - - if (!IsReachableWithoutRequiringConnection(flags)) - return NetworkStatus.NotReachable; - - if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) - return NetworkStatus.ReachableViaCarrierDataNetwork; - - return NetworkStatus.ReachableViaWiFiNetwork; - } - } - - internal static NetworkStatus InternetConnectionStatus() - { - var status = NetworkStatus.NotReachable; - - var defaultNetworkAvailable = IsNetworkAvailable(out var flags); - - // If it's a WWAN connection.. - if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) - status = NetworkStatus.ReachableViaCarrierDataNetwork; - - // If the connection is reachable and no connection is required, then assume it's WiFi - if (defaultNetworkAvailable) - { - status = NetworkStatus.ReachableViaWiFiNetwork; - } - - // If the connection is on-demand or on-traffic and no user intervention - // is required, then assume WiFi. - if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && - (flags & NetworkReachabilityFlags.InterventionRequired) == 0) - { - status = NetworkStatus.ReachableViaWiFiNetwork; - } - - return status; - } - - internal static IEnumerable GetActiveConnectionType() - { - var status = new List(); - - var defaultNetworkAvailable = IsNetworkAvailable(out var flags); - - // If it's a WWAN connection.. - if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) - { - status.Add(NetworkStatus.ReachableViaCarrierDataNetwork); - } - else if (defaultNetworkAvailable) - { - status.Add(NetworkStatus.ReachableViaWiFiNetwork); - } - else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && - (flags & NetworkReachabilityFlags.InterventionRequired) == 0) - { - // If the connection is on-demand or on-traffic and no user intervention - // is required, then assume WiFi. - status.Add(NetworkStatus.ReachableViaWiFiNetwork); - } - - return status; - } - - internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags) - { - var ip = new IPAddress(0); - using (var defaultRouteReachability = new NetworkReachability(ip)) - { - if (!defaultRouteReachability.TryGetFlags(out flags)) - return false; - - return IsReachableWithoutRequiringConnection(flags); - } - } - - internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags) - { - // Is it reachable with the current network configuration? - var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0; - - // Do we need a connection to reach it? - var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0; - - // Since the network stack will automatically try to get the WAN up, - // probe that - if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) - noConnectionRequired = true; - - return isReachable && noConnectionRequired; - } - } - - class ReachabilityListener : IDisposable - { - NetworkReachability defaultRouteReachability; - NetworkReachability remoteHostReachability; - - internal ReachabilityListener() - { - var ip = new IPAddress(0); - defaultRouteReachability = new NetworkReachability(ip); - defaultRouteReachability.SetNotification(OnChange); - defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); - - remoteHostReachability = new NetworkReachability(Reachability.HostName); - - // Need to probe before we queue, or we wont get any meaningful values - // this only happens when you create NetworkReachability from a hostname - remoteHostReachability.TryGetFlags(out var flags); - - remoteHostReachability.SetNotification(OnChange); - remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); - } - - internal event Action ReachabilityChanged; - - void IDisposable.Dispose() => Dispose(); - - internal void Dispose() - { - defaultRouteReachability?.Dispose(); - defaultRouteReachability = null; - remoteHostReachability?.Dispose(); - remoteHostReachability = null; - } - - async void OnChange(NetworkReachabilityFlags flags) - { - // Add in artifical delay so the connection status has time to change - // else it will return true no matter what. - await Task.Delay(100); - - ReachabilityChanged?.Invoke(); - } - } -} From 0a0d21f4af090640e775ea452bcf5cd0a11cb04c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 18:25:38 -0700 Subject: [PATCH 151/254] fix some more broken references --- .../PlatformSpecific/Connectivity.android.cs | 18 +++++++++--------- .../PlatformSpecific/Connectivity.ios.cs | 4 ++++ .../PlatformSpecific/Permissions.android.cs | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs index 6db4395d..a074cc95 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs @@ -36,11 +36,11 @@ internal partial class Connectivity static void StartListeners() { - Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); + Permissions.EnsureDeclared(PermissionType.NetworkState); conectivityReceiver = new ConnectivityBroadcastReceiver(OnConnectivityChanged); - Platform.Platform.AppContext.RegisterReceiver(conectivityReceiver, new IntentFilter(ConnectivityManager.ConnectivityAction)); + Platform.AppContext.RegisterReceiver(conectivityReceiver, new IntentFilter(ConnectivityManager.ConnectivityAction)); } static void StopListeners() @@ -49,7 +49,7 @@ static void StopListeners() return; try { - Platform.Platform.AppContext.UnregisterReceiver(conectivityReceiver); + Platform.AppContext.UnregisterReceiver(conectivityReceiver); } catch (Java.Lang.IllegalArgumentException) { @@ -66,14 +66,14 @@ static NetworkAccess PlatformNetworkAccess { get { - Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); + Permissions.EnsureDeclared(PermissionType.NetworkState); try { var currentAccess = NetworkAccess.None; - var manager = Platform.Platform.ConnectivityManager; + var manager = Platform.ConnectivityManager; - if (Platform.Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) { foreach (var network in manager.GetAllNetworks()) { @@ -140,10 +140,10 @@ static IEnumerable PlatformConnectionProfiles { get { - Permissions.Permissions.EnsureDeclared(Permissions.PermissionType.NetworkState); + Permissions.EnsureDeclared(PermissionType.NetworkState); - var manager = Platform.Platform.ConnectivityManager; - if (Platform.Platform.HasApiLevel(BuildVersionCodes.Lollipop)) + var manager = Platform.ConnectivityManager; + if (Platform.HasApiLevel(BuildVersionCodes.Lollipop)) { foreach (var network in manager.GetAllNetworks()) { diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs index 949e9ca7..c7196f6c 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.ios.cs @@ -22,6 +22,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using CoreFoundation; +using SystemConfiguration; namespace LaunchDarkly.Xamarin.PlatformSpecific { diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs index 1b640e32..a63e2a10 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Permissions.android.cs @@ -53,7 +53,7 @@ internal static void EnsureDeclared(PermissionType permission) if (androidPermissions == null || !androidPermissions.Any()) return; - var context = Platform.Platform.AppContext; + var context = Platform.AppContext; foreach (var ap in androidPermissions) { From eeca41e5965861dd782c0b1c14ef39ca74c52355 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 20:12:01 -0700 Subject: [PATCH 152/254] improvements to CONTRIBUTING file --- CONTRIBUTING.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 366a1812..72383fcd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,45 +1,48 @@ -Contributing to the LaunchDarkly Client-side SDK for Xamarin -================================================ +# Contributing to the LaunchDarkly Client-side SDK for Xamarin LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. -Submitting bug reports and feature requests ------------------- +## Submitting bug reports and feature requests The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/xamarin-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. -Submitting pull requests ------------------- +## Submitting pull requests We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. -Build instructions ------------------- +## Build instructions ### Prerequisites The .NET Standard 1.6 and 2.0 targets require only the .NET Core 2.0 SDK, while the iOS and Android targets require the corresponding Xamarin SDKs. -To set up the project and dependencies, run the following command in the root SDK directory: +### Building + +To build the SDK (for all platforms) without running any tests: ``` -dotnet restore +msbuild /restore src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj ``` -### Building - -To build the SDK (for all platforms) without running any tests: +To build the SDK for only one of the supported platforms, add `-f X` where `X` is one of the items in the `` list of `LaunchDarkly.XamarinSdk.csproj`: `netstandard2.0` for .NET Standard 2.0, `MonoAndroid80` for Android 8.0, etc.: ``` -msbuild src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +msbuild /restore src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj -f netstandard2.0 ``` +Note that the main project, `src/LaunchDarkly.XamarinSdk`, contains source files that are built for all platforms (ending in just `.cs`, or `.shared.cs`), and also a smaller amount of code that is conditionally compiled for platform-specific functionality. The latter is all in the `PlatformSpecific` folder. We use `#ifdef` directives only for small sections that differ slightly between platform versions; otherwise the conditional compilation is done according to filename suffix (`.android.cs`, etc.) based on rules in the `.csproj` file. + ### Testing -To build the .NET Standard 2.0 target and run the basic unit tests, which cover all of the non-platform-specific functionality: +The .NET Standard unit tests cover all of the non-platform-specific functionality, as well as behavior specific to .NET Standard (e.g. caching flags in the filesystem). They can be run with only the basic Xamarin framework installed, via the `dotnet` tool: + ``` dotnet build src/LaunchDarkly.XamarinSdk -f netstandard2.0 dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 ``` -Currently the Android and iOS test projects can only be run from Visual Studio in MacOS. +The equivalent test suites in Android or iOS must be run in an Android or iOS emulator. The projects `tests/LaunchDarkly.XamarinSdk.Android.Tests` and `tests/LaunchDarkly.XamarinSdk.iOS.Tests` consist of applications based on the `xunit.runner.devices` tool, which show the test results visually in the emulator and also write the results to the emulator's system log. The actual unit test code is just the same tests from the main `tests/LaunchDarkly.XamarinSdk.Tests` project, but running them in this way exercises the mobile-specific behavior for those platforms (e.g. caching flags in user preferences). + +You can run the mobile test projects from Visual Studio (the iOS tests require MacOS); there is also a somewhat complicated process for running them from the command line, which is what the CI build does (see `.circleci/config.yml`). + +Note that the mobile unit tests currently do not cover background-mode behavior or connectivity detection. From ddff9a1b3a551d67bdab3782cf4ca2d8bb305607 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 1 Jul 2019 20:13:23 -0700 Subject: [PATCH 153/254] capitalization --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72383fcd..b21ac4e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to the LaunchDarkly Client-side SDK for Xamarin +# Contributing to the LaunchDarkly Client-Side SDK for Xamarin LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. diff --git a/README.md b/README.md index f3ebe7ff..e608de4d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LaunchDarkly Client-side SDK for Xamarin +LaunchDarkly Client-Side SDK for Xamarin =========================== [![CircleCI](https://circleci.com/gh/launchdarkly/xamarin-client-sdk/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/xamarin-client-sdk/tree/master) From ec0f5cdfeeb1d9bbd04b38474dcfca08fdb6d938 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 2 Jul 2019 14:13:22 -0700 Subject: [PATCH 154/254] 1.0.0-beta18 --- CHANGELOG.md | 15 +++++++++++++++ .../LaunchDarkly.XamarinSdk.csproj | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b78f488..8fa64ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the LaunchDarkly Client-side SDK for Xamarin will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [1.0.0-beta18] - 2019-07-02 +### Added: +- New `Configuration` property `PersistFlagValues` (default: true) allows you to turn off the SDK's normal behavior of storing flag values locally so they can be used offline. +- Flag values are now stored locally in .NET Standard by default, on the filesystem, using the .NET `IsolatedStorageFile` mechanism. +- Added CI unit test suites that exercise most of the SDK functionality in .NET Standard, Android, and iOS. The tests do not currently cover background-mode detection and network connectivity detection on mobile platforms. + +### Changed: +- `Configuration.WithUpdateProcessor` has been replaced with `Configuration.WithUpdateProcessorFactory`. These methods are for testing purposes and will not normally be used. + +### Fixed: +- In .NET Standard, if you specify a user with `Key == null` and `Anonymous == true`, the SDK now generates a GUID for a user key and caches it in local storage for future reuse. This is consistent with the other client-side SDKs. Previously, it caused an exception. + +### Removed: +- Several low-level component interfaces such as `IDeviceInfo` which had been exposed for testing are now internal. + # Note on future releases The LaunchDarkly SDK repositories are being renamed for consistency. This repository is now `xamarin-client-sdk` rather than `xamarin-client`. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 08362d91..3c13cb95 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -4,7 +4,7 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - 1.0.0-beta17 + 1.0.0-beta18 Library LaunchDarkly.XamarinSdk LaunchDarkly.XamarinSdk From 7058f3c5e123f99e84e59c7f0d67ae8a48421402 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 2 Jul 2019 21:00:13 -0700 Subject: [PATCH 155/254] add scripts for packaging & releasing --- scripts/package.sh | 26 ++++++++++++++++++++++++++ scripts/release.sh | 17 +++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100755 scripts/package.sh create mode 100755 scripts/release.sh diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 00000000..d11e4b89 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +# Usage: ./scripts/package.sh [debug|release] + +# msbuild expects word-capitalization of this parameter +CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` +if [[ -z "$CONFIG" ]]; then + CONFIG=Debug # currently we're releasing debug builds by default +fi + +# Remove any existing build products. + +msbuild /t:clean +rm -f ./src/LaunchDarkly.XamarinSdk/bin/Debug/*.nupkg +rm -f ./src/LaunchDarkly.XamarinSdk/bin/Release/*.nupkg + +# Build the project for all target frameworks. This includes building the .nupkg, because of +# the directive in our project file. + +msbuild /restore /p:Configuration:$CONFIG src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj + +# Run the .NET Standard 2.0 unit tests. (Android and iOS tests are run by CI jobs in config.yml) + +export ASPNETCORE_SUPPRESSSTATUSMESSAGES=true # suppresses some annoying test output +dotnet test tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj -f netcoreapp2.0 diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 00000000..5808c148 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +# Usage: ./scripts/release.sh [debug|release] + +# msbuild expects word-capitalization of this parameter +CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` +if [[ -z "$CONFIG" ]]; then + CONFIG=Debug # currently we're releasing debug builds by default +fi + +./scripts/package.sh $CONFIG + +# Since package.sh does a clean build, whichever .nupkg file now exists in the output directory +# is the one we want to upload. + +nuget push ./src/LaunchDarkly.XamarinSdk/bin/$CONFIG/LaunchDarkly.XamarinSdk.*.nupkg -Source https://www.nuget.org From 15e58b1cabbc861e86b569bfdc6188399572edde Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 2 Jul 2019 21:02:28 -0700 Subject: [PATCH 156/254] indents --- scripts/package.sh | 2 +- scripts/release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/package.sh b/scripts/package.sh index d11e4b89..4fa7f2a2 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -6,7 +6,7 @@ set -e # msbuild expects word-capitalization of this parameter CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` if [[ -z "$CONFIG" ]]; then - CONFIG=Debug # currently we're releasing debug builds by default + CONFIG=Debug # currently we're releasing debug builds by default fi # Remove any existing build products. diff --git a/scripts/release.sh b/scripts/release.sh index 5808c148..1f7fc68f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -6,7 +6,7 @@ set -e # msbuild expects word-capitalization of this parameter CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` if [[ -z "$CONFIG" ]]; then - CONFIG=Debug # currently we're releasing debug builds by default + CONFIG=Debug # currently we're releasing debug builds by default fi ./scripts/package.sh $CONFIG From fa5106becae03bc8d306d15d62b9eafc1df6a286 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 3 Jul 2019 09:50:07 -0700 Subject: [PATCH 157/254] comments --- CONTRIBUTING.md | 6 +++++- scripts/package.sh | 4 ++++ scripts/release.sh | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b21ac4e6..51fbe5eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,12 +18,14 @@ The .NET Standard 1.6 and 2.0 targets require only the .NET Core 2.0 SDK, while ### Building -To build the SDK (for all platforms) without running any tests: +To build the SDK (for all target platforms) without running any tests: ``` msbuild /restore src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj ``` +Currently this command can only be run on MacOS, because that is the only platform that allows building for all of the targets (.NET Standard, Android, and iOS). + To build the SDK for only one of the supported platforms, add `-f X` where `X` is one of the items in the `` list of `LaunchDarkly.XamarinSdk.csproj`: `netstandard2.0` for .NET Standard 2.0, `MonoAndroid80` for Android 8.0, etc.: ``` @@ -46,3 +48,5 @@ The equivalent test suites in Android or iOS must be run in an Android or iOS em You can run the mobile test projects from Visual Studio (the iOS tests require MacOS); there is also a somewhat complicated process for running them from the command line, which is what the CI build does (see `.circleci/config.yml`). Note that the mobile unit tests currently do not cover background-mode behavior or connectivity detection. + +### Packaging/releasing diff --git a/scripts/package.sh b/scripts/package.sh index 4fa7f2a2..1448f731 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -3,6 +3,10 @@ set -e # Usage: ./scripts/package.sh [debug|release] +# This script performs a clean build for all target platforms, which produces both the DLLs and the .nupkg, +# and also runs the .NET Standard unit tests. It is used in the LaunchDarkly release process. It must be run +# on MacOS, since iOS is one of the targets. + # msbuild expects word-capitalization of this parameter CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` if [[ -z "$CONFIG" ]]; then diff --git a/scripts/release.sh b/scripts/release.sh index 1f7fc68f..a1ede956 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -3,6 +3,9 @@ set -e # Usage: ./scripts/release.sh [debug|release] +# This script calls package.sh to create a NuGet package, and then uploads it to NuGet. It is used in +# the LaunchDarkly release process. It must be run on MacOS, since iOS is one of the targets. + # msbuild expects word-capitalization of this parameter CONFIG=`echo $1 | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}'` if [[ -z "$CONFIG" ]]; then From 6553a7c50715f46d132054a9c116f3897ea32da7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 19:51:01 -0700 Subject: [PATCH 158/254] typo --- scripts/package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package.sh b/scripts/package.sh index 1448f731..710d2b4b 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -22,7 +22,7 @@ rm -f ./src/LaunchDarkly.XamarinSdk/bin/Release/*.nupkg # Build the project for all target frameworks. This includes building the .nupkg, because of # the directive in our project file. -msbuild /restore /p:Configuration:$CONFIG src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +msbuild /restore /p:Configuration=$CONFIG src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj # Run the .NET Standard 2.0 unit tests. (Android and iOS tests are run by CI jobs in config.yml) From 56a6ff17e08502e9c35796b5b72f216896c6e7ce Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 19:56:48 -0700 Subject: [PATCH 159/254] call flag change listeners asynchronously --- .../FeatureFlagListenerManager.cs | 60 ++++++++++++++----- .../IFeatureFlagListenerManager.cs | 1 + src/LaunchDarkly.XamarinSdk/LdClient.cs | 6 ++ .../AsyncScheduler.android.cs | 13 ++++ .../PlatformSpecific/AsyncScheduler.ios.cs | 13 ++++ .../AsyncScheduler.netstandard.cs | 13 ++++ .../PlatformSpecific/AsyncScheduler.shared.cs | 18 ++++++ .../FeatureFlagListenerTests.cs | 56 ++++++++++++++--- .../FlagCacheManagerTests.cs | 16 +++-- .../LdClientTests.cs | 13 ++-- 10 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.netstandard.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs index 84cf1016..dfe46710 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs @@ -1,13 +1,17 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; +using Common.Logging; using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin { internal class FeatureFlagListenerManager : IFeatureFlagListenerManager, IFlagListenerUpdater { + private static readonly ILog Log = LogManager.GetLogger(typeof(FeatureFlagListenerManager)); + private readonly IDictionary> _map = new Dictionary>(); @@ -18,12 +22,11 @@ public void RegisterListener(IFeatureFlagListener listener, string flagKey) readWriteLock.EnterWriteLock(); try { - if (!_map.ContainsKey(flagKey)) + if (!_map.TryGetValue(flagKey, out var list)) { - _map[flagKey] = new List(); + list = new List(); + _map[flagKey] = list; } - - var list = _map[flagKey]; list.Add(listener); } finally @@ -37,10 +40,9 @@ public void UnregisterListener(IFeatureFlagListener listener, string flagKey) readWriteLock.EnterWriteLock(); try { - if (_map.ContainsKey(flagKey)) + if (_map.TryGetValue(flagKey, out var list)) { - var listOfListeners = _map[flagKey]; - listOfListeners.Remove(listener); + list.Remove(listener); } } finally @@ -49,16 +51,16 @@ public void UnregisterListener(IFeatureFlagListener listener, string flagKey) } } - public void FlagWasDeleted(string flagKey) + public bool IsListenerRegistered(IFeatureFlagListener listener, string flagKey) { readWriteLock.EnterReadLock(); try { - if (_map.ContainsKey(flagKey)) + if (_map.TryGetValue(flagKey, out var list)) { - var listeners = _map[flagKey]; - listeners.ForEach((listener) => listener.FeatureFlagDeleted(flagKey)); + return list.Contains(listener); } + return false; } finally { @@ -66,21 +68,49 @@ public void FlagWasDeleted(string flagKey) } } + public void FlagWasDeleted(string flagKey) + { + FireAction(flagKey, (listener) => listener.FeatureFlagDeleted(flagKey)); + } + public void FlagWasUpdated(string flagKey, JToken value) { + FireAction(flagKey, (listener) => listener.FeatureFlagChanged(flagKey, value)); + } + + private void FireAction(string flagKey, Action a) + { + IFeatureFlagListener[] listeners = null; readWriteLock.EnterReadLock(); try { - if (_map.ContainsKey(flagKey)) + if (_map.TryGetValue(flagKey, out var mutableListOfListeners)) { - var listeners = _map[flagKey]; - listeners.ForEach((listener) => listener.FeatureFlagChanged(flagKey, value)); + listeners = mutableListOfListeners.ToArray(); // this copies the list } } finally { readWriteLock.ExitReadLock(); } + if (listeners != null) + { + foreach (var l in listeners) + { + // Note, this schedules the listeners separately, rather than scheduling a single task that runs them all. + PlatformSpecific.AsyncScheduler.ScheduleAction(() => + { + try + { + a(l); + } + catch (Exception e) + { + Log.Warn("Unexpected exception from feature flag listener", e); + } + }); + } + } } } } diff --git a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs index 792e8145..0e9668af 100644 --- a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs +++ b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs @@ -6,6 +6,7 @@ internal interface IFeatureFlagListenerManager : IFlagListenerUpdater { void RegisterListener(IFeatureFlagListener listener, string flagKey); void UnregisterListener(IFeatureFlagListener listener, string flagKey); + bool IsListenerRegistered(IFeatureFlagListener listener, string flagKey); // used for testing } internal interface IFlagListenerUpdater diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index e4d5058d..5bd1b674 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -549,6 +549,12 @@ public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener l flagListenerManager.UnregisterListener(listener, flagKey); } + // for tests only + internal bool IsFeatureFlagListenerRegistered(string flagKey, IFeatureFlagListener listener) + { + return flagListenerManager.IsListenerRegistered(listener, flagKey); + } + internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs new file mode 100644 index 00000000..4a62552f --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs @@ -0,0 +1,13 @@ +using System; +using Android.OS; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class AsyncScheduler + { + private static void PlatformScheduleAction(Action a) + { + + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs new file mode 100644 index 00000000..54a9a8d9 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs @@ -0,0 +1,13 @@ +using System; +using Foundation; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class AsyncScheduler + { + private static void PlatformScheduleAction(Action a) + { + NSRunLoop.Main.BeginInvokeOnMainThread(a.Invoke); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.netstandard.cs new file mode 100644 index 00000000..ef7411f5 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.netstandard.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class AsyncScheduler + { + private static void PlatformScheduleAction(Action a) + { + Task.Run(a); + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs new file mode 100644 index 00000000..869201bf --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs @@ -0,0 +1,18 @@ +using System; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + // This class and the rest of its partial class implementations are not derived from Xamarin Essentials. + // It provides a method for asynchronously starting tasks, such as event handlers, using a mechanism + // that may vary by platform. + internal static partial class AsyncScheduler + { + // Queues a task to be executed asynchronously as soon as possible. On platforms that have a notion + // of a "main thread" or "UI thread", the action is guaranteed to run on that thread; otherwise it + // can be any thread. + public static void ScheduleAction(Action a) + { + PlatformScheduleAction(a); + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs index d20f2d9b..2eae60a1 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using Newtonsoft.Json.Linq; using Xunit; @@ -14,23 +15,27 @@ FeatureFlagListenerManager Manager() return new FeatureFlagListenerManager(); } - TestListener Listener() + TestListener Listener(int expectedTimes) { - return new TestListener(); + return new TestListener(expectedTimes); } [Fact] public void CanRegisterListeners() { var manager = Manager(); - var listener1 = Listener(); - var listener2 = Listener(); + var listener1 = Listener(2); + var listener2 = Listener(2); manager.RegisterListener(listener1, INT_FLAG); manager.RegisterListener(listener1, DOUBLE_FLAG); manager.RegisterListener(listener2, INT_FLAG); manager.RegisterListener(listener2, DOUBLE_FLAG); + manager.FlagWasUpdated(INT_FLAG, 7); manager.FlagWasUpdated(DOUBLE_FLAG, 10.5); + listener1.Countdown.Wait(); + listener2.Countdown.Wait(); + Assert.Equal(7, listener1.FeatureFlags[INT_FLAG]); Assert.Equal(10.5, listener1.FeatureFlags[DOUBLE_FLAG]); Assert.Equal(7, listener2.FeatureFlags[INT_FLAG]); @@ -41,14 +46,18 @@ public void CanRegisterListeners() public void CanUnregisterListeners() { var manager = Manager(); - var listener1 = Listener(); - var listener2 = Listener(); + var listener1 = Listener(2); + var listener2 = Listener(2); manager.RegisterListener(listener1, INT_FLAG); manager.RegisterListener(listener1, DOUBLE_FLAG); manager.RegisterListener(listener2, INT_FLAG); manager.RegisterListener(listener2, DOUBLE_FLAG); + manager.FlagWasUpdated(INT_FLAG, 7); manager.FlagWasUpdated(DOUBLE_FLAG, 10.5); + listener1.Countdown.Wait(); + listener2.Countdown.Wait(); + Assert.Equal(7, listener1.FeatureFlags[INT_FLAG]); Assert.Equal(10.5, listener1.FeatureFlags[DOUBLE_FLAG]); Assert.Equal(7, listener2.FeatureFlags[INT_FLAG]); @@ -58,9 +67,14 @@ public void CanUnregisterListeners() manager.UnregisterListener(listener2, INT_FLAG); manager.UnregisterListener(listener1, DOUBLE_FLAG); manager.UnregisterListener(listener2, DOUBLE_FLAG); + listener1.Reset(); + listener2.Reset(); manager.FlagWasUpdated(INT_FLAG, 2); manager.FlagWasUpdated(DOUBLE_FLAG, 12.5); + // This is pretty hacky, but since we're testing for the *lack* of a call, there's no signal we can wait on. + Thread.Sleep(100); + Assert.NotEqual(2, listener1.FeatureFlags[INT_FLAG]); Assert.NotEqual(12.5, listener1.FeatureFlags[DOUBLE_FLAG]); Assert.NotEqual(2, listener2.FeatureFlags[INT_FLAG]); @@ -71,12 +85,14 @@ public void CanUnregisterListeners() public void ListenerGetsUpdatedFlagValues() { var manager = Manager(); - var listener1 = Listener(); - var listener2 = Listener(); + var listener1 = Listener(1); + var listener2 = Listener(1); manager.RegisterListener(listener1, INT_FLAG); manager.RegisterListener(listener2, INT_FLAG); manager.FlagWasUpdated(INT_FLAG, JToken.FromObject(99)); + listener1.Countdown.Wait(); + listener2.Countdown.Wait(); Assert.Equal(99, listener1.FeatureFlags[INT_FLAG].Value()); Assert.Equal(99, listener2.FeatureFlags[INT_FLAG].Value()); @@ -86,17 +102,32 @@ public void ListenerGetsUpdatedFlagValues() public void ListenerGetsUpdatedWhenManagerFlagDeleted() { var manager = Manager(); - var listener = Listener(); + var listener = Listener(1); manager.RegisterListener(listener, INT_FLAG); + manager.FlagWasUpdated(INT_FLAG, 2); + listener.Countdown.Wait(); Assert.True(listener.FeatureFlags.ContainsKey(INT_FLAG)); + + listener.Reset(); manager.FlagWasDeleted(INT_FLAG); + listener.Countdown.Wait(); + Assert.False(listener.FeatureFlags.ContainsKey(INT_FLAG)); } } public class TestListener : IFeatureFlagListener { + private readonly int ExpectedCalls; + public CountdownEvent Countdown; + + public TestListener(int expectedCalls) + { + ExpectedCalls = expectedCalls; + Reset(); + } + IDictionary featureFlags = new Dictionary(); public IDictionary FeatureFlags { @@ -109,11 +140,18 @@ public IDictionary FeatureFlags public void FeatureFlagChanged(string featureFlagKey, JToken value) { featureFlags[featureFlagKey] = value; + Countdown.Signal(); } public void FeatureFlagDeleted(string featureFlagKey) { featureFlags.Remove(featureFlagKey); + Countdown.Signal(); + } + + public void Reset() + { + Countdown = new CountdownEvent(ExpectedCalls); } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs index fdf00d8d..a7435808 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs @@ -70,12 +70,15 @@ public void ShouldBeAbleToUpdateFlagForUser() [Fact] public void UpdateFlagUpdatesTheFlagOnListenerManager() { - var listener = new TestListener(); + var listener = new TestListener(1); listenerManager.RegisterListener(listener, "int-flag"); + var flagCacheManager = ManagerWithCachedFlags(); var updatedFeatureFlag = new FeatureFlag(); updatedFeatureFlag.value = JToken.FromObject(7); + flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); + listener.Countdown.Wait(); Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); } @@ -83,15 +86,15 @@ public void UpdateFlagUpdatesTheFlagOnListenerManager() [Fact] public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() { - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); + var listener = new TestListener(1); listener.FeatureFlags["int-flag"] = JToken.FromObject(1); - Assert.True(listener.FeatureFlags.ContainsKey("int-flag")); + listenerManager.RegisterListener(listener, "int-flag"); var flagCacheManager = ManagerWithCachedFlags(); var updatedFeatureFlag = new FeatureFlag(); updatedFeatureFlag.value = JToken.FromObject(7); flagCacheManager.RemoveFlagForUser("int-flag", user); + listener.Countdown.Wait(); Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); } @@ -99,12 +102,13 @@ public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() [Fact] public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() { - var flagCacheManager = ManagerWithCachedFlags(); - var listener = new TestListener(); + var listener = new TestListener(1); listenerManager.RegisterListener(listener, "int-flag"); + var flagCacheManager = ManagerWithCachedFlags(); var newFlagsJson = "{\"int-flag\":{\"value\":5}}"; flagCacheManager.CacheFlagsFromService(TestUtil.DecodeFlagsJson(newFlagsJson), user); + listener.Countdown.Wait(); Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 45afe92e..4c18fb94 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -217,10 +217,9 @@ public void CanRegisterListener() using (var client = Client()) { var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); + var listener = new TestListener(1); client.RegisterFeatureFlagListener("user1-flag", listener); - listenerMgr.FlagWasUpdated("user1-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user1-flag"].ToObject()); + Assert.True(client.IsFeatureFlagListenerRegistered("user1-flag", listener)); } } @@ -230,14 +229,12 @@ public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerMan using (var client = Client()) { var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); + var listener = new TestListener(1); client.RegisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user2-flag"]); + Assert.True(client.IsFeatureFlagListenerRegistered("user2-flag", listener)); client.UnregisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 12); - Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); + Assert.False(client.IsFeatureFlagListenerRegistered("user2-flag", listener)); } } From 4c7ee0f738c3d90ce532d4c6981f57a9eea3a3ad Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 20:03:30 -0700 Subject: [PATCH 160/254] copy Android implementation from Xamarin.Essentials --- .../PlatformSpecific/AsyncScheduler.android.cs | 16 ++++++++++++---- .../PlatformSpecific/AsyncScheduler.shared.cs | 3 +-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs index 4a62552f..ccb8dee3 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs @@ -5,9 +5,17 @@ namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class AsyncScheduler { - private static void PlatformScheduleAction(Action a) + private static Handler handler; + + private static void PlatformScheduleAction(Action a) { - - } - } + // This is based on the Android implementation in Xamarin.Essentials: + // https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/MainThread/MainThread.android.cs + if (handler?.Looper != Looper.MainLooper) + { + handler = new Handler(Looper.MainLooper); + } + handler.Post(a); + } + } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs index 869201bf..b2f9b44a 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.shared.cs @@ -2,8 +2,7 @@ namespace LaunchDarkly.Xamarin.PlatformSpecific { - // This class and the rest of its partial class implementations are not derived from Xamarin Essentials. - // It provides a method for asynchronously starting tasks, such as event handlers, using a mechanism + // This provides a method for asynchronously starting tasks, such as event handlers, using a mechanism // that may vary by platform. internal static partial class AsyncScheduler { From 3a2a2e00fc366cc76c704dbc5a3f3db959d87fbe Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 20:03:59 -0700 Subject: [PATCH 161/254] indents --- .../AsyncScheduler.android.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs index ccb8dee3..ad62cbb5 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs @@ -5,17 +5,17 @@ namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class AsyncScheduler { - private static Handler handler; + private static Handler handler; - private static void PlatformScheduleAction(Action a) + private static void PlatformScheduleAction(Action a) { - // This is based on the Android implementation in Xamarin.Essentials: - // https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/MainThread/MainThread.android.cs - if (handler?.Looper != Looper.MainLooper) - { - handler = new Handler(Looper.MainLooper); - } - handler.Post(a); - } - } + // This is based on the Android implementation in Xamarin.Essentials: + // https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/MainThread/MainThread.android.cs + if (handler?.Looper != Looper.MainLooper) + { + handler = new Handler(Looper.MainLooper); + } + handler.Post(a); + } + } } From 184a0543e3db4db2a26746961c88eb328ecd8912 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 20:06:26 -0700 Subject: [PATCH 162/254] indents --- .../PlatformSpecific/AsyncScheduler.ios.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs index 54a9a8d9..de4a2a55 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.ios.cs @@ -3,11 +3,11 @@ namespace LaunchDarkly.Xamarin.PlatformSpecific { - internal static partial class AsyncScheduler - { + internal static partial class AsyncScheduler + { private static void PlatformScheduleAction(Action a) - { + { NSRunLoop.Main.BeginInvokeOnMainThread(a.Invoke); } - } + } } From 81aab83db90a949aa864c264c9bf1a86f3d51a65 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 8 Jul 2019 20:38:04 -0700 Subject: [PATCH 163/254] doc comment --- src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index 9f13e1f1..7773eb49 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -152,9 +152,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Registers an instance of for a given flag key to observe /// flag value changes. - /// - /// It is important to note that this callback scheme will need to be handled on the Main UI thread, - /// if you plan on updating UI components with flag values. + /// + /// On platforms that have a main UI thread (such as iOS and Android), the listener is guaranteed to + /// be called on that thread; on other platforms, the SDK uses a thread pool. Either way, the listener + /// is called asynchronously after whichever SDK action triggered the flag change has already completed-- + /// so as to avoid deadlocks, in case the action was also on the main thread, or on a thread that was + /// holding a lock on some application resource that the listener also uses. /// /// /// The flag key you want to observe changes for. From 001b98cd6793326aebf9054c46351cc7c9900a7f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 9 Jul 2019 12:43:51 -0700 Subject: [PATCH 164/254] simpler Android AsyncScheduler implementation --- .../PlatformSpecific/AsyncScheduler.android.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs index ad62cbb5..d36e44ea 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/AsyncScheduler.android.cs @@ -5,16 +5,14 @@ namespace LaunchDarkly.Xamarin.PlatformSpecific { internal static partial class AsyncScheduler { - private static Handler handler; - private static void PlatformScheduleAction(Action a) { - // This is based on the Android implementation in Xamarin.Essentials: - // https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/MainThread/MainThread.android.cs - if (handler?.Looper != Looper.MainLooper) - { - handler = new Handler(Looper.MainLooper); - } + // Note that this logic is different from the implementation of the equivalent method in Xamarin Essentials + // (https://github.com/xamarin/Essentials/blob/master/Xamarin.Essentials/MainThread/MainThread.android.cs); + // it creates a new Handler object each time rather than lazily creating a static one. This avoids a potential + // race condition, at the expense of creating more ephemeral objects. However, in our use case we do not + // expect this method to be called very frequently since we are using it for flag change listeners only. + var handler = new Handler(Looper.MainLooper); handler.Post(a); } } From 89897d83a8383b667ba42be82f157f81b6f32c8d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 11 Jul 2019 16:04:42 -0700 Subject: [PATCH 165/254] typo --- scripts/package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package.sh b/scripts/package.sh index 1448f731..710d2b4b 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -22,7 +22,7 @@ rm -f ./src/LaunchDarkly.XamarinSdk/bin/Release/*.nupkg # Build the project for all target frameworks. This includes building the .nupkg, because of # the directive in our project file. -msbuild /restore /p:Configuration:$CONFIG src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +msbuild /restore /p:Configuration=$CONFIG src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj # Run the .NET Standard 2.0 unit tests. (Android and iOS tests are run by CI jobs in config.yml) From 99cec42302fe21ccbbaea9d2030cef69c520c81d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 11 Jul 2019 16:11:39 -0700 Subject: [PATCH 166/254] add test for deferring listener calls --- .../FeatureFlagListenerTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs index 2eae60a1..2bc58016 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs @@ -115,6 +115,28 @@ public void ListenerGetsUpdatedWhenManagerFlagDeleted() Assert.False(listener.FeatureFlags.ContainsKey(INT_FLAG)); } + + [Fact] + public void ListenerCallIsDeferred() + { + // This verifies that we are not making synchronous calls to listeners, so they cannot deadlock by trying to + // acquire some resource that is being held by the caller. There are three possible things that can happen: + // 1. We call the listener synchronously; listener.Called gets set to true before FlagWasUpdated returns. Fail. + // 2. The listener is queued somewhere for later execution, so it doesn't even start to run before the end of + // the test. Pass. + // 3. The listener starts executing immediately on another thread; that's OK too, because the lock(locker) block + // ensures it won't set Called until after we have checked it. Pass. + var manager = Manager(); + var locker = new object(); + var listener = new TestDeadlockListener(locker); + + manager.RegisterListener(listener, INT_FLAG); + lock (locker) + { + manager.FlagWasUpdated(INT_FLAG, 2); + Assert.False(listener.Called); + } + } } public class TestListener : IFeatureFlagListener @@ -154,4 +176,31 @@ public void Reset() Countdown = new CountdownEvent(ExpectedCalls); } } + + public class TestDeadlockListener : IFeatureFlagListener + { + private readonly object _locker; + public volatile bool Called; + + public TestDeadlockListener(object locker) + { + _locker = locker; + } + + public void FeatureFlagChanged(string featureFlagKey, JToken value) + { + lock(_locker) + { + Called = true; + } + } + + public void FeatureFlagDeleted(string featureFlagKey) + { + lock(_locker) + { + Called = true; + } + } + } } From aa2794437acd9a1df1a570c553ddad8a6657fcd5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 12 Jul 2019 12:58:20 -0700 Subject: [PATCH 167/254] allow singleton to be cleared on disposal; use better concurrency practices --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 33 ++++--- .../LdClientTests.cs | 24 ++++- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 98 +++++++++++++++---- 3 files changed, 120 insertions(+), 35 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 5bd1b674..7c1c803a 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -19,14 +19,17 @@ public sealed class LdClient : ILdMobileClient { private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); + static volatile LdClient instance; + /// - /// The singleton instance used by your application throughout its lifetime, can only be created once. + /// The singleton instance used by your application throughout its lifetime. Once this exists, you cannot + /// create a new client instance unless you first call on this one. /// - /// Use the designated static method - /// to set this LdClient instance. + /// Use the designated static methods or + /// to set this LdClient instance. /// /// The LdClient instance. - public static LdClient Instance { get; internal set; } + public static LdClient Instance => instance; /// /// The Configuration instance used to setup the LdClient. @@ -100,7 +103,7 @@ public sealed class LdClient : ILdMobileClient /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient + /// to instantiate the single instance of LdClient /// for the lifetime of your application. /// /// The singleton LdClient instance. @@ -164,18 +167,18 @@ public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTim throw new ArgumentOutOfRangeException(nameof(maxWaitTime)); } - CreateInstance(config, user); + var c = CreateInstance(config, user); - if (Instance.Online) + if (c.Online) { - if (!Instance.StartUpdateProcessor(maxWaitTime)) + if (!c.StartUpdateProcessor(maxWaitTime)) { Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", maxWaitTime.TotalMilliseconds); } } - return Instance; + return c; } /// @@ -215,7 +218,7 @@ static LdClient CreateInstance(Configuration configuration, User user) } var c = new LdClient(configuration, user); - Instance = c; + Interlocked.CompareExchange(ref instance, c, null); Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); return c; } @@ -510,7 +513,7 @@ User DecorateUser(User user) return newUser; } - void IDisposable.Dispose() + public void Dispose() { Dispose(true); GC.SuppressFinalize(this); @@ -525,9 +528,17 @@ void Dispose(bool disposing) BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; updateProcessor.Dispose(); eventProcessor.Dispose(); + + // Reset the static Instance to null *if* it was referring to this instance + DetachInstance(); } } + internal void DetachInstance() // exposed for testing + { + Interlocked.CompareExchange(ref instance, null, this); + } + /// public Version Version { diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 4c18fb94..8eabf37e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -79,17 +79,31 @@ public void IdentifyAsyncWithNullUserThrowsException() [Fact] public void SharedClientIsTheOnlyClientAvailable() { - lock (TestUtil.ClientInstanceLock) + TestUtil.WithClientLock(() => { var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); using (var client = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.Zero)); - } - } - TestUtil.ClearClient(); + } + TestUtil.ClearClient(); + }); } - + + [Fact] + public void CanCreateNewClientAfterDisposingOfSharedInstance() + { + TestUtil.WithClientLock(() => + { + TestUtil.ClearClient(); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + using (var client0 = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { } + Assert.Null(LdClient.Instance); + // Dispose() is called automatically at end of "using" block + using (var client1 = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { } + }); + } + [Fact] public void ConnectionManagerShouldKnowIfOnlineOrNot() { diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index be5d187b..05b35799 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using LaunchDarkly.Client; using Newtonsoft.Json; @@ -11,20 +12,83 @@ public static class TestUtil { // Any tests that are going to access the static LdClient.Instance must hold this lock, // to avoid interfering with tests that use CreateClient. - public static readonly object ClientInstanceLock = new object(); + private static readonly SemaphoreSlim ClientInstanceLock = new SemaphoreSlim(1); + + private static ThreadLocal InClientLock = new ThreadLocal(); + + public static T WithClientLock(Func f) + { + // This cumbersome combination of a ThreadLocal and a SemaphoreSlim is simply because 1. we have to use + // SemaphoreSlim (I think) since there's no way to wait on a regular lock in *async* code, and 2. SemaphoreSlim + // is not reentrant, so we need to make sure a thread can't block itself. + if (InClientLock.Value) + { + return f.Invoke(); + } + ClientInstanceLock.Wait(); + try + { + InClientLock.Value = true; + return f.Invoke(); + } + finally + { + InClientLock.Value = false; + ClientInstanceLock.Release(); + } + } + + public static void WithClientLock(Action a) + { + if (InClientLock.Value) + { + a.Invoke(); + return; + } + ClientInstanceLock.Wait(); + try + { + InClientLock.Value = true; + a.Invoke(); + } + finally + { + InClientLock.Value = false; + ClientInstanceLock.Release(); + } + } + + public static async Task WithClientLockAsync(Func> f) + { + if (InClientLock.Value) + { + return await f.Invoke(); + } + await ClientInstanceLock.WaitAsync(); + try + { + InClientLock.Value = true; + return await f.Invoke(); + } + finally + { + InClientLock.Value = false; + ClientInstanceLock.Release(); + } + } // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can // instantiate their own independent clients. Application code cannot do this because // the LdClient.Instance setter has internal scope. public static LdClient CreateClient(Configuration config, User user, TimeSpan? timeout = null) { - ClearClient(); - lock (ClientInstanceLock) - { + return WithClientLock(() => + { + ClearClient(); LdClient client = LdClient.Init(config, user, timeout ?? TimeSpan.FromSeconds(1)); - LdClient.Instance = null; + client.DetachInstance(); return client; - } + }); } // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can @@ -32,25 +96,21 @@ public static LdClient CreateClient(Configuration config, User user, TimeSpan? t // the LdClient.Instance setter has internal scope. public static async Task CreateClientAsync(Configuration config, User user) { - ClearClient(); - LdClient client = await LdClient.InitAsync(config, user); - lock (ClientInstanceLock) + return await WithClientLockAsync(async () => { - LdClient.Instance = null; - } - return client; + ClearClient(); + LdClient client = await LdClient.InitAsync(config, user); + client.DetachInstance(); + return client; + }); } public static void ClearClient() { - lock (ClientInstanceLock) + WithClientLock(() => { - if (LdClient.Instance != null) - { - (LdClient.Instance as IDisposable).Dispose(); - LdClient.Instance = null; - } - } + LdClient.Instance?.Dispose(); + }); } public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) From 267a0e0abc6585a2ed3326db8424f967f8d23fda Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 12 Jul 2019 13:06:24 -0700 Subject: [PATCH 168/254] fix race condition in CreateInstance --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 7c1c803a..b84688b1 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -20,6 +20,7 @@ public sealed class LdClient : ILdMobileClient private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); static volatile LdClient instance; + static readonly object createInstanceLock = new object(); /// /// The singleton instance used by your application throughout its lifetime. Once this exists, you cannot @@ -212,15 +213,18 @@ public static Task InitAsync(Configuration config, User user) static LdClient CreateInstance(Configuration configuration, User user) { - if (Instance != null) + lock (createInstanceLock) { - throw new Exception("LdClient instance already exists."); - } + if (Instance != null) + { + throw new Exception("LdClient instance already exists."); + } - var c = new LdClient(configuration, user); - Interlocked.CompareExchange(ref instance, c, null); - Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); - return c; + var c = new LdClient(configuration, user); + Interlocked.CompareExchange(ref instance, c, null); + Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); + return c; + } } bool StartUpdateProcessor(TimeSpan maxWaitTime) From 4b014fed232920d41be18c8597762a681edd3974 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 15 Jul 2019 11:01:30 -0700 Subject: [PATCH 169/254] add note about VS 2015 issue --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e608de4d..e37359e7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Supported platforms This beta release is built for the following targets: Android 7.1, 8.0, 8.1; iOS 10; .NET Standard 1.6, 2.0. It has also been tested with Android 9/API 28 and iOS 12.1. +Note that if you are targeting .NET Framework, there is a [known issue](https://stackoverflow.com/questions/46788323/installing-a-netstandard-2-0-nuget-package-into-a-vs2015-net-4-6-1-project) in Visual Studio 2015 where it does not correctly detect compatibility with .NET Standard packages. This is not a problem in later versions of Visual Studio. + Getting started ----------- From 30a7359c7dc61a4f6ab33339aad3f1933913a11d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 17 Jul 2019 11:15:07 -0700 Subject: [PATCH 170/254] rethink flag change event interface and implementation --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 8 +- src/LaunchDarkly.XamarinSdk/Factory.cs | 8 +- .../FeatureFlagListenerManager.cs | 116 ---------- .../FlagCacheManager.cs | 76 ++++--- .../FlagChangedEvent.cs | 87 ++++++++ .../IFeatureFlagListener.cs | 42 ---- .../IFeatureFlagListenerManager.cs | 17 -- .../ILdMobileClient.cs | 50 +++-- src/LaunchDarkly.XamarinSdk/LdClient.cs | 59 +++-- ...unchDarkly.XamarinSdk.Android.Tests.csproj | 6 +- .../FeatureFlagListenerTests.cs | 206 ------------------ .../FlagCacheManagerTests.cs | 48 ++-- .../FlagChangedEventTests.cs | 138 ++++++++++++ .../LdClientTests.cs | 29 +-- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 3 +- .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 6 +- 16 files changed, 380 insertions(+), 519 deletions(-) delete mode 100644 src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs create mode 100644 src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs delete mode 100644 src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs delete mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index 1d31887e..763ddfb0 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -134,7 +134,7 @@ public class Configuration : IMobileConfiguration internal Func UpdateProcessorFactory { get; set; } internal IPersistentStorage PersistentStorage { get; set; } internal IDeviceInfo DeviceInfo { get; set; } - internal IFeatureFlagListenerManager FeatureFlagListenerManager { get; set; } + internal IFlagChangedEventManager FlagChangedEventManager { get; set; } /// /// Default value for . @@ -207,7 +207,7 @@ public static Configuration Default(string mobileKey) { if (String.IsNullOrEmpty(mobileKey)) { - throw new ArgumentOutOfRangeException("mobileKey", "key is required"); + throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required"); } var defaultConfiguration = new Configuration { @@ -647,9 +647,9 @@ internal static Configuration WithDeviceInfo(this Configuration configuration, I return configuration; } - internal static Configuration WithFeatureFlagListenerManager(this Configuration configuration, IFeatureFlagListenerManager featureFlagListenerManager) + internal static Configuration WithFlagChangedEventManager(this Configuration configuration, IFlagChangedEventManager flagChangedEventManager) { - configuration.FeatureFlagListenerManager = featureFlagListenerManager; + configuration.FlagChangedEventManager = flagChangedEventManager; return configuration; } diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index 3e98be0f..924f59b2 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -12,7 +12,7 @@ internal static class Factory internal static IFlagCacheManager CreateFlagCacheManager(Configuration configuration, IPersistentStorage persister, - IFlagListenerUpdater updater, + IFlagChangedEventManager flagChangedEventManager, User user) { if (configuration.FlagCacheManager != null) @@ -23,7 +23,7 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura { var inMemoryCache = new UserFlagInMemoryCache(); var deviceCache = configuration.PersistFlagValues ? new UserFlagDeviceCache(persister) as IUserFlagCache : new NullUserFlagCache(); - return new FlagCacheManager(inMemoryCache, deviceCache, updater, user); + return new FlagCacheManager(inMemoryCache, deviceCache, flagChangedEventManager, user); } } @@ -84,9 +84,9 @@ internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) return configuration.DeviceInfo ?? new DefaultDeviceInfo(); } - internal static IFeatureFlagListenerManager CreateFeatureFlagListenerManager(Configuration configuration) + internal static IFlagChangedEventManager CreateFlagChangedEventManager(Configuration configuration) { - return configuration.FeatureFlagListenerManager ?? new FeatureFlagListenerManager(); + return configuration.FlagChangedEventManager ?? new FlagChangedEventManager(); } } } diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs deleted file mode 100644 index dfe46710..00000000 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagListenerManager.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Common.Logging; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin -{ - internal class FeatureFlagListenerManager : IFeatureFlagListenerManager, IFlagListenerUpdater - { - private static readonly ILog Log = LogManager.GetLogger(typeof(FeatureFlagListenerManager)); - - private readonly IDictionary> _map = - new Dictionary>(); - - private ReaderWriterLockSlim readWriteLock = new ReaderWriterLockSlim(); - - public void RegisterListener(IFeatureFlagListener listener, string flagKey) - { - readWriteLock.EnterWriteLock(); - try - { - if (!_map.TryGetValue(flagKey, out var list)) - { - list = new List(); - _map[flagKey] = list; - } - list.Add(listener); - } - finally - { - readWriteLock.ExitWriteLock(); - } - } - - public void UnregisterListener(IFeatureFlagListener listener, string flagKey) - { - readWriteLock.EnterWriteLock(); - try - { - if (_map.TryGetValue(flagKey, out var list)) - { - list.Remove(listener); - } - } - finally - { - readWriteLock.ExitWriteLock(); - } - } - - public bool IsListenerRegistered(IFeatureFlagListener listener, string flagKey) - { - readWriteLock.EnterReadLock(); - try - { - if (_map.TryGetValue(flagKey, out var list)) - { - return list.Contains(listener); - } - return false; - } - finally - { - readWriteLock.ExitReadLock(); - } - } - - public void FlagWasDeleted(string flagKey) - { - FireAction(flagKey, (listener) => listener.FeatureFlagDeleted(flagKey)); - } - - public void FlagWasUpdated(string flagKey, JToken value) - { - FireAction(flagKey, (listener) => listener.FeatureFlagChanged(flagKey, value)); - } - - private void FireAction(string flagKey, Action a) - { - IFeatureFlagListener[] listeners = null; - readWriteLock.EnterReadLock(); - try - { - if (_map.TryGetValue(flagKey, out var mutableListOfListeners)) - { - listeners = mutableListOfListeners.ToArray(); // this copies the list - } - } - finally - { - readWriteLock.ExitReadLock(); - } - if (listeners != null) - { - foreach (var l in listeners) - { - // Note, this schedules the listeners separately, rather than scheduling a single task that runs them all. - PlatformSpecific.AsyncScheduler.ScheduleAction(() => - { - try - { - a(l); - } - catch (Exception e) - { - Log.Warn("Unexpected exception from feature flag listener", e); - } - }); - } - } - } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs index d12680f9..a10d2bbc 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using LaunchDarkly.Client; +using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin { @@ -8,18 +10,18 @@ internal class FlagCacheManager : IFlagCacheManager { private readonly IUserFlagCache inMemoryCache; private readonly IUserFlagCache deviceCache; - private readonly IFlagListenerUpdater flagListenerUpdater; + private readonly IFlagChangedEventManager flagChangedEventManager; private ReaderWriterLockSlim readWriteLock = new ReaderWriterLockSlim(); public FlagCacheManager(IUserFlagCache inMemoryCache, IUserFlagCache deviceCache, - IFlagListenerUpdater flagListenerUpdater, + IFlagChangedEventManager flagChangedEventManager, User user) { this.inMemoryCache = inMemoryCache; this.deviceCache = deviceCache; - this.flagListenerUpdater = flagListenerUpdater; + this.flagChangedEventManager = flagChangedEventManager; var flagsFromDevice = deviceCache.RetrieveFlags(user); if (flagsFromDevice != null) @@ -43,6 +45,7 @@ public IDictionary FlagsForUser(User user) public void CacheFlagsFromService(IDictionary flags, User user) { + List> changes = null; readWriteLock.EnterWriteLock(); try { @@ -52,18 +55,16 @@ public void CacheFlagsFromService(IDictionary flags, User u foreach (var flag in flags) { - bool flagAlreadyExisted = previousFlags.ContainsKey(flag.Key); - bool flagValueChanged = false; - if (flagAlreadyExisted) + if (previousFlags.TryGetValue(flag.Key, out var originalFlag)) { - var originalFlag = previousFlags[flag.Key]; - flagValueChanged = originalFlag.value != flag.Value.value; - } - - // only update the Listeners if the flag value changed - if (flagValueChanged) - { - flagListenerUpdater.FlagWasUpdated(flag.Key, flag.Value.value); + if (!JToken.DeepEquals(originalFlag.value, flag.Value.value)) + { + if (changes == null) + { + changes = new List>(); + } + changes.Add(Tuple.Create(flag.Key, flag.Value.value, originalFlag.value)); + } } } } @@ -71,55 +72,78 @@ public void CacheFlagsFromService(IDictionary flags, User u { readWriteLock.ExitWriteLock(); } + if (changes != null) + { + foreach (var c in changes) + { + flagChangedEventManager.FlagWasUpdated(c.Item1, c.Item2, c.Item3); + } + } } public FeatureFlag FlagForUser(string flagKey, User user) { var flags = FlagsForUser(user); - FeatureFlag featureFlag; - if (flags.TryGetValue(flagKey, out featureFlag)) + if (flags.TryGetValue(flagKey, out var featureFlag)) { return featureFlag; } - return null; } public void RemoveFlagForUser(string flagKey, User user) { + JToken oldValue = null; readWriteLock.EnterWriteLock(); - try { var flagsForUser = inMemoryCache.RetrieveFlags(user); - flagsForUser.Remove(flagKey); - deviceCache.CacheFlagsForUser(flagsForUser, user); - inMemoryCache.CacheFlagsForUser(flagsForUser, user); - flagListenerUpdater.FlagWasDeleted(flagKey); + if (flagsForUser.TryGetValue(flagKey, out var flag)) + { + oldValue = flag.value; + flagsForUser.Remove(flagKey); + deviceCache.CacheFlagsForUser(flagsForUser, user); + inMemoryCache.CacheFlagsForUser(flagsForUser, user); + } } finally { readWriteLock.ExitWriteLock(); } - + if (oldValue != null) + { + flagChangedEventManager.FlagWasDeleted(flagKey, oldValue); + } } public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user) { + bool changed = false; + JToken oldValue = null; readWriteLock.EnterWriteLock(); - try { var flagsForUser = inMemoryCache.RetrieveFlags(user); + if (flagsForUser.TryGetValue(flagKey, out var oldFlag)) + { + if (!JToken.DeepEquals(oldFlag.value, featureFlag.value)) + { + oldValue = oldFlag.value; + changed = true; + } + } flagsForUser[flagKey] = featureFlag; deviceCache.CacheFlagsForUser(flagsForUser, user); inMemoryCache.CacheFlagsForUser(flagsForUser, user); - flagListenerUpdater.FlagWasUpdated(flagKey, featureFlag.value); } finally { readWriteLock.ExitWriteLock(); } + if (changed) + { + flagChangedEventManager.FlagWasUpdated(flagKey, featureFlag.value, oldValue); + } } } } diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs new file mode 100644 index 00000000..de534699 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using Common.Logging; +using Newtonsoft.Json.Linq; + +namespace LaunchDarkly.Xamarin +{ + public class FlagChangedEventArgs + { + public string Key { get; private set; } + + public JToken NewValue { get; private set; } + + public JToken OldValue { get; private set; } + + public bool FlagWasDeleted { get; private set; } + + public bool NewBoolValue => NewValue.Value(); + + public string NewStringValue => NewValue.Value(); + + public int NewIntValue => NewValue.Value(); + + public float NewFloatValue => NewValue.Value(); + + internal FlagChangedEventArgs(string key, JToken newValue, JToken oldValue, bool flagWasDeleted) + { + Key = key; + NewValue = newValue; + OldValue = oldValue; + FlagWasDeleted = flagWasDeleted; + } + } + + internal interface IFlagChangedEventManager + { + event EventHandler FlagChanged; + void FlagWasDeleted(string flagKey, JToken oldValue); + void FlagWasUpdated(string flagKey, JToken newValue, JToken oldValue); + } + + internal class FlagChangedEventManager : IFlagChangedEventManager + { + private static readonly ILog Log = LogManager.GetLogger(typeof(IFlagChangedEventManager)); + + public event EventHandler FlagChanged; + + public bool IsHandlerRegistered(EventHandler handler) + { + return FlagChanged != null && FlagChanged.GetInvocationList().Contains(handler); + } + + public void FlagWasDeleted(string flagKey, JToken oldValue) + { + FireEvent(new FlagChangedEventArgs(flagKey, null, oldValue, true)); + } + + public void FlagWasUpdated(string flagKey, JToken newValue, JToken oldValue) + { + FireEvent(new FlagChangedEventArgs(flagKey, newValue, oldValue, false)); + } + + private void FireEvent(FlagChangedEventArgs eventArgs) + { + var copyOfHandlers = FlagChanged; + var sender = this; + if (copyOfHandlers != null) + { + foreach (var h in copyOfHandlers.GetInvocationList()) + { + // Note, this schedules the listeners separately, rather than scheduling a single task that runs them all. + PlatformSpecific.AsyncScheduler.ScheduleAction(() => + { + try + { + h.DynamicInvoke(sender, eventArgs); + } + catch (Exception e) + { + Log.Warn("Unexpected exception from FlagChanged event handler", e); + } + }); + } + } + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs deleted file mode 100644 index 1ff877aa..00000000 --- a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListener.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin -{ - /// - /// Represents a callback listener for feature flag value changes. - /// - /// You should have your ViewController, Activity or custom class implement this interface if you want to - /// be notified of value changes for a given flag key. - /// - /// Look at for - /// usage of this interface. - /// - public interface IFeatureFlagListener - { - /// - /// Tells the implementer of this interface that the feature flag for the given key - /// was changed to the given value. - /// - /// It is important to know that this will not be called on your main UI thread, so if you plan - /// on updating the UI when this is called, you will need to use the appropriate pattern to post safely to the UI - /// main thread. - /// - /// The feature flag key. - /// The feature flag value that changed. - void FeatureFlagChanged(string featureFlagKey, JToken value); - - /// - /// Tells the implementer of this interface that the feature flag for the given key - /// was deleted on the LaunchDarkly service side. - /// - /// It is important to know that this will not be called on your main UI thread, so if you plan - /// on updating the UI when this is called, you will need to use the appropriate pattern to post safely to the UI - /// main thread. - /// - /// The feature flag key. - void FeatureFlagDeleted(string featureFlagKey); - } -} - diff --git a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs b/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs deleted file mode 100644 index 0e9668af..00000000 --- a/src/LaunchDarkly.XamarinSdk/IFeatureFlagListenerManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin -{ - internal interface IFeatureFlagListenerManager : IFlagListenerUpdater - { - void RegisterListener(IFeatureFlagListener listener, string flagKey); - void UnregisterListener(IFeatureFlagListener listener, string flagKey); - bool IsListenerRegistered(IFeatureFlagListener listener, string flagKey); // used for testing - } - - internal interface IFlagListenerUpdater - { - void FlagWasUpdated(string flagKey, JToken value); - void FlagWasDeleted(string flagKey); - } -} \ No newline at end of file diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index 7773eb49..ee9eb6c9 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using LaunchDarkly.Common; using System.Threading.Tasks; @@ -150,28 +151,33 @@ public interface ILdMobileClient : ILdCommonClient IDictionary AllFlags(); /// - /// Registers an instance of for a given flag key to observe - /// flag value changes. - /// - /// On platforms that have a main UI thread (such as iOS and Android), the listener is guaranteed to - /// be called on that thread; on other platforms, the SDK uses a thread pool. Either way, the listener - /// is called asynchronously after whichever SDK action triggered the flag change has already completed-- - /// so as to avoid deadlocks, in case the action was also on the main thread, or on a thread that was - /// holding a lock on some application resource that the listener also uses. - /// - /// - /// The flag key you want to observe changes for. - /// The instance of the IFeatureFlagListener. - void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener); - - /// - /// Unregisters an instance of for a given flag key to stop observing - /// flag value changes. - /// + /// This event is triggered when the client has received an updated value for a feature flag. /// - /// The flag key you want to observe changes for. - /// The instance of the IFeatureFlagListener. - void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener); + /// + /// This could mean that the flag configuration was changed in LaunchDarkly, or that you have changed the current + /// user and the flag values are different for this user than for the previous user. The event is not + /// triggered for the flag values that are first obtained when the client is initialized. It is only triggered + /// if the newly received flag value is actually different from the previous one. + /// + /// The properties will indicate the key of the feature flag, the new value, + /// and the previous value. + /// + /// On platforms that have a main UI thread (such as iOS and Android), handlers for this event are guaranteed to + /// be called on that thread; on other platforms, the SDK uses a thread pool. Either way, the handler is called + /// called asynchronously after whichever SDK action triggered the flag change has already completed. This is to + /// avoid deadlocks, in case the action was also on the main thread, or on a thread that was holding a lock on + /// some application resource that the handler also uses. + /// + /// + /// + /// client.FlagChanged += (sender, eventArgs) => { + /// if (eventArgs.Key == "key-for-flag-i-am-watching") { + /// DoSomethingWithNewFlagValue(eventArgs.NewBoolValue); + /// } + /// }; + /// + /// + event EventHandler FlagChanged; /// /// Registers the user. diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index b84688b1..6e3580e4 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -44,20 +44,20 @@ public sealed class LdClient : ILdMobileClient /// The User. public User User { get; private set; } - object myLockObjForConnectionChange = new object(); - object myLockObjForUserUpdate = new object(); - - IFlagCacheManager flagCacheManager; - IConnectionManager connectionManager; - IMobileUpdateProcessor updateProcessor; - IEventProcessor eventProcessor; - IPersistentStorage persister; - IDeviceInfo deviceInfo; + readonly object myLockObjForConnectionChange = new object(); + readonly object myLockObjForUserUpdate = new object(); + + readonly IFlagCacheManager flagCacheManager; + readonly IConnectionManager connectionManager; + IMobileUpdateProcessor updateProcessor; // not readonly - may need to be recreated + readonly IEventProcessor eventProcessor; + readonly IPersistentStorage persister; + readonly IDeviceInfo deviceInfo; readonly EventFactory eventFactoryDefault = EventFactory.Default; readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; - IFeatureFlagListenerManager flagListenerManager; + internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing - SemaphoreSlim connectionLock; + readonly SemaphoreSlim connectionLock; // private constructor prevents initialization of this class // without using WithConfigAnduser(config, user) @@ -67,11 +67,11 @@ public sealed class LdClient : ILdMobileClient { if (configuration == null) { - throw new ArgumentNullException("configuration"); + throw new ArgumentNullException(nameof(configuration)); } if (user == null) { - throw new ArgumentNullException("user"); + throw new ArgumentNullException(nameof(user)); } Config = configuration; @@ -80,11 +80,11 @@ public sealed class LdClient : ILdMobileClient persister = Factory.CreatePersistentStorage(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); - flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); + flagChangedEventManager = Factory.CreateFlagChangedEventManager(configuration); User = DecorateUser(user); - flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); + flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagChangedEventManager, User); connectionManager = Factory.CreateConnectionManager(configuration); updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, null); eventProcessor = Factory.CreateEventProcessor(configuration); @@ -457,7 +457,7 @@ public async Task IdentifyAsync(User user) { if (user == null) { - throw new ArgumentNullException("user"); + throw new ArgumentNullException(nameof(user)); } User newUser = DecorateUser(user); @@ -552,23 +552,18 @@ public Version Version } } - /// - public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.RegisterListener(listener, flagKey); - } - - /// - public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.UnregisterListener(listener, flagKey); - } - - // for tests only - internal bool IsFeatureFlagListenerRegistered(string flagKey, IFeatureFlagListener listener) + /// + public event EventHandler FlagChanged { - return flagListenerManager.IsListenerRegistered(listener, flagKey); - } + add + { + flagChangedEventManager.FlagChanged += value; + } + remove + { + flagChangedEventManager.FlagChanged -= value; + } + } internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index ad9e5b13..58182c3f 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -66,9 +66,6 @@ SharedTestCode\ConfigurationTest.cs - - SharedTestCode\FeatureFlagListenerTests.cs - SharedTestCode\FeatureFlagRequestorTests.cs @@ -78,6 +75,9 @@ SharedTestCode\FlagCacheManagerTests.cs + + SharedTestCode\FlagChangedEventTests.cs + SharedTestCode\LDClientEndToEndTests.cs diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs deleted file mode 100644 index 2bc58016..00000000 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagListenerTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class FeatureFlagListenerTests : BaseTest - { - private const string INT_FLAG = "int-flag"; - private const string DOUBLE_FLAG = "double-flag"; - - FeatureFlagListenerManager Manager() - { - return new FeatureFlagListenerManager(); - } - - TestListener Listener(int expectedTimes) - { - return new TestListener(expectedTimes); - } - - [Fact] - public void CanRegisterListeners() - { - var manager = Manager(); - var listener1 = Listener(2); - var listener2 = Listener(2); - manager.RegisterListener(listener1, INT_FLAG); - manager.RegisterListener(listener1, DOUBLE_FLAG); - manager.RegisterListener(listener2, INT_FLAG); - manager.RegisterListener(listener2, DOUBLE_FLAG); - - manager.FlagWasUpdated(INT_FLAG, 7); - manager.FlagWasUpdated(DOUBLE_FLAG, 10.5); - listener1.Countdown.Wait(); - listener2.Countdown.Wait(); - - Assert.Equal(7, listener1.FeatureFlags[INT_FLAG]); - Assert.Equal(10.5, listener1.FeatureFlags[DOUBLE_FLAG]); - Assert.Equal(7, listener2.FeatureFlags[INT_FLAG]); - Assert.Equal(10.5, listener2.FeatureFlags[DOUBLE_FLAG]); - } - - [Fact] - public void CanUnregisterListeners() - { - var manager = Manager(); - var listener1 = Listener(2); - var listener2 = Listener(2); - manager.RegisterListener(listener1, INT_FLAG); - manager.RegisterListener(listener1, DOUBLE_FLAG); - manager.RegisterListener(listener2, INT_FLAG); - manager.RegisterListener(listener2, DOUBLE_FLAG); - - manager.FlagWasUpdated(INT_FLAG, 7); - manager.FlagWasUpdated(DOUBLE_FLAG, 10.5); - listener1.Countdown.Wait(); - listener2.Countdown.Wait(); - - Assert.Equal(7, listener1.FeatureFlags[INT_FLAG]); - Assert.Equal(10.5, listener1.FeatureFlags[DOUBLE_FLAG]); - Assert.Equal(7, listener2.FeatureFlags[INT_FLAG]); - Assert.Equal(10.5, listener2.FeatureFlags[DOUBLE_FLAG]); - - manager.UnregisterListener(listener1, INT_FLAG); - manager.UnregisterListener(listener2, INT_FLAG); - manager.UnregisterListener(listener1, DOUBLE_FLAG); - manager.UnregisterListener(listener2, DOUBLE_FLAG); - listener1.Reset(); - listener2.Reset(); - manager.FlagWasUpdated(INT_FLAG, 2); - manager.FlagWasUpdated(DOUBLE_FLAG, 12.5); - - // This is pretty hacky, but since we're testing for the *lack* of a call, there's no signal we can wait on. - Thread.Sleep(100); - - Assert.NotEqual(2, listener1.FeatureFlags[INT_FLAG]); - Assert.NotEqual(12.5, listener1.FeatureFlags[DOUBLE_FLAG]); - Assert.NotEqual(2, listener2.FeatureFlags[INT_FLAG]); - Assert.NotEqual(12.5, listener2.FeatureFlags[DOUBLE_FLAG]); - } - - [Fact] - public void ListenerGetsUpdatedFlagValues() - { - var manager = Manager(); - var listener1 = Listener(1); - var listener2 = Listener(1); - manager.RegisterListener(listener1, INT_FLAG); - manager.RegisterListener(listener2, INT_FLAG); - - manager.FlagWasUpdated(INT_FLAG, JToken.FromObject(99)); - listener1.Countdown.Wait(); - listener2.Countdown.Wait(); - - Assert.Equal(99, listener1.FeatureFlags[INT_FLAG].Value()); - Assert.Equal(99, listener2.FeatureFlags[INT_FLAG].Value()); - } - - [Fact] - public void ListenerGetsUpdatedWhenManagerFlagDeleted() - { - var manager = Manager(); - var listener = Listener(1); - manager.RegisterListener(listener, INT_FLAG); - - manager.FlagWasUpdated(INT_FLAG, 2); - listener.Countdown.Wait(); - Assert.True(listener.FeatureFlags.ContainsKey(INT_FLAG)); - - listener.Reset(); - manager.FlagWasDeleted(INT_FLAG); - listener.Countdown.Wait(); - - Assert.False(listener.FeatureFlags.ContainsKey(INT_FLAG)); - } - - [Fact] - public void ListenerCallIsDeferred() - { - // This verifies that we are not making synchronous calls to listeners, so they cannot deadlock by trying to - // acquire some resource that is being held by the caller. There are three possible things that can happen: - // 1. We call the listener synchronously; listener.Called gets set to true before FlagWasUpdated returns. Fail. - // 2. The listener is queued somewhere for later execution, so it doesn't even start to run before the end of - // the test. Pass. - // 3. The listener starts executing immediately on another thread; that's OK too, because the lock(locker) block - // ensures it won't set Called until after we have checked it. Pass. - var manager = Manager(); - var locker = new object(); - var listener = new TestDeadlockListener(locker); - - manager.RegisterListener(listener, INT_FLAG); - lock (locker) - { - manager.FlagWasUpdated(INT_FLAG, 2); - Assert.False(listener.Called); - } - } - } - - public class TestListener : IFeatureFlagListener - { - private readonly int ExpectedCalls; - public CountdownEvent Countdown; - - public TestListener(int expectedCalls) - { - ExpectedCalls = expectedCalls; - Reset(); - } - - IDictionary featureFlags = new Dictionary(); - public IDictionary FeatureFlags - { - get - { - return featureFlags; - } - } - - public void FeatureFlagChanged(string featureFlagKey, JToken value) - { - featureFlags[featureFlagKey] = value; - Countdown.Signal(); - } - - public void FeatureFlagDeleted(string featureFlagKey) - { - featureFlags.Remove(featureFlagKey); - Countdown.Signal(); - } - - public void Reset() - { - Countdown = new CountdownEvent(ExpectedCalls); - } - } - - public class TestDeadlockListener : IFeatureFlagListener - { - private readonly object _locker; - public volatile bool Called; - - public TestDeadlockListener(object locker) - { - _locker = locker; - } - - public void FeatureFlagChanged(string featureFlagKey, JToken value) - { - lock(_locker) - { - Called = true; - } - } - - public void FeatureFlagDeleted(string featureFlagKey) - { - lock(_locker) - { - Called = true; - } - } - } -} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs index a7435808..9c8cef20 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs @@ -14,7 +14,7 @@ public class FlagCacheManagerTests : BaseTest IUserFlagCache deviceCache = new UserFlagInMemoryCache(); IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); - FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); + FlagChangedEventManager listenerManager = new FlagChangedEventManager(); User user = User.WithKey("someKey"); @@ -47,7 +47,7 @@ public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() } [Fact] - public void ShouldBeAbleToRemoveFlagForUser() + public void CanRemoveFlagForUser() { var manager = ManagerWithCachedFlags(); manager.RemoveFlagForUser("int-key", user); @@ -55,7 +55,7 @@ public void ShouldBeAbleToRemoveFlagForUser() } [Fact] - public void ShouldBeAbleToUpdateFlagForUser() + public void CanUpdateFlagForUser() { var flagCacheManager = ManagerWithCachedFlags(); var updatedFeatureFlag = new FeatureFlag(); @@ -68,49 +68,53 @@ public void ShouldBeAbleToUpdateFlagForUser() } [Fact] - public void UpdateFlagUpdatesTheFlagOnListenerManager() + public void UpdateFlagSendsFlagChangeEvent() { - var listener = new TestListener(1); - listenerManager.RegisterListener(listener, "int-flag"); + var listener = new FlagChangedEventSink(); + listenerManager.FlagChanged += listener.Handler; var flagCacheManager = ManagerWithCachedFlags(); var updatedFeatureFlag = new FeatureFlag(); updatedFeatureFlag.value = JToken.FromObject(7); flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); - listener.Countdown.Wait(); - Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); + var e = listener.Await(); + Assert.Equal("int-flag", e.Key); + Assert.Equal(7, e.NewIntValue); + Assert.False(e.FlagWasDeleted); } [Fact] - public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() - { - var listener = new TestListener(1); - listener.FeatureFlags["int-flag"] = JToken.FromObject(1); - listenerManager.RegisterListener(listener, "int-flag"); + public void RemoveFlagSendsFlagChangeEvent() + { + var listener = new FlagChangedEventSink(); + listenerManager.FlagChanged += listener.Handler; var flagCacheManager = ManagerWithCachedFlags(); var updatedFeatureFlag = new FeatureFlag(); updatedFeatureFlag.value = JToken.FromObject(7); - flagCacheManager.RemoveFlagForUser("int-flag", user); - listener.Countdown.Wait(); - - Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); + flagCacheManager.RemoveFlagForUser("int-flag", user); + + var e = listener.Await(); + Assert.Equal("int-flag", e.Key); + Assert.True(e.FlagWasDeleted); } [Fact] public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() - { - var listener = new TestListener(1); - listenerManager.RegisterListener(listener, "int-flag"); + { + var listener = new FlagChangedEventSink(); + listenerManager.FlagChanged += listener.Handler; var flagCacheManager = ManagerWithCachedFlags(); var newFlagsJson = "{\"int-flag\":{\"value\":5}}"; flagCacheManager.CacheFlagsFromService(TestUtil.DecodeFlagsJson(newFlagsJson), user); - listener.Countdown.Wait(); - Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); + var e = listener.Await(); + Assert.Equal("int-flag", e.Key); + Assert.Equal(5, e.NewIntValue); + Assert.False(e.FlagWasDeleted); } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs new file mode 100644 index 00000000..01dcc176 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs @@ -0,0 +1,138 @@ +using System.Collections.Concurrent; +using System.Threading; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class FlagChangedEventTests : BaseTest + { + private const string INT_FLAG = "int-flag"; + private const string DOUBLE_FLAG = "double-flag"; + + FlagChangedEventManager Manager() + { + return new FlagChangedEventManager(); + } + + [Fact] + public void CanRegisterListeners() + { + var manager = Manager(); + var listener1 = new FlagChangedEventSink(); + var listener2 = new FlagChangedEventSink(); + manager.FlagChanged += listener1.Handler; + manager.FlagChanged += listener2.Handler; + + manager.FlagWasUpdated(INT_FLAG, 7, 6); + manager.FlagWasUpdated(DOUBLE_FLAG, 10.5, 9.5); + + var event1a = listener1.Await(); + var event1b = listener1.Await(); + var event2a = listener2.Await(); + var event2b = listener2.Await(); + + Assert.Equal(INT_FLAG, event1a.Key); + Assert.Equal(INT_FLAG, event2a.Key); + Assert.Equal(7, event1a.NewIntValue); + Assert.Equal(7, event2a.NewIntValue); + Assert.Equal(6, event1a.OldValue); + Assert.Equal(6, event2a.OldValue); + Assert.False(event1a.FlagWasDeleted); + Assert.False(event2a.FlagWasDeleted); + + Assert.Equal(DOUBLE_FLAG, event1b.Key); + Assert.Equal(DOUBLE_FLAG, event2b.Key); + Assert.Equal(10.5, event1b.NewFloatValue); + Assert.Equal(10.5, event2b.NewFloatValue); + Assert.Equal(9.5, event1b.OldValue); + Assert.Equal(9.5, event2b.OldValue); + Assert.False(event1b.FlagWasDeleted); + Assert.False(event2b.FlagWasDeleted); + } + + [Fact] + public void CanUnregisterListeners() + { + var manager = Manager(); + var listener1 = new FlagChangedEventSink(); + var listener2 = new FlagChangedEventSink(); + manager.FlagChanged += listener1.Handler; + manager.FlagChanged += listener2.Handler; + + manager.FlagChanged -= listener1.Handler; + + manager.FlagWasUpdated(INT_FLAG, 7, 6); + + var e = listener2.Await(); + Assert.Equal(INT_FLAG, e.Key); + Assert.Equal(7, e.NewIntValue); + Assert.Equal(6, e.OldValue); + + // This is pretty hacky, but since we're testing for the *lack* of a call, there's no signal we can wait on. + Thread.Sleep(100); + + Assert.True(listener1.IsEmpty); + } + + [Fact] + public void ListenerGetsUpdatedWhenManagerFlagDeleted() + { + var manager = Manager(); + var listener = new FlagChangedEventSink(); + manager.FlagChanged += listener.Handler; + + manager.FlagWasDeleted(INT_FLAG, 1); + + var e = listener.Await(); + Assert.Equal(INT_FLAG, e.Key); + Assert.Equal(1, e.OldValue); + Assert.True(e.FlagWasDeleted); + } + + [Fact] + public void ListenerCallIsDeferred() + { + // This verifies that we are not making synchronous calls to listeners, so they cannot deadlock by trying to + // acquire some resource that is being held by the caller. There are three possible things that can happen: + // 1. We call the listener synchronously; listener.Called gets set to true before FlagWasUpdated returns. Fail. + // 2. The listener is queued somewhere for later execution, so it doesn't even start to run before the end of + // the test. Pass. + // 3. The listener starts executing immediately on another thread; that's OK too, because the lock(locker) block + // ensures it won't set Called until after we have checked it. Pass. + var manager = Manager(); + var locker = new object(); + var called = false; + + manager.FlagChanged += (sender, args) => + { + lock (locker) + { + Volatile.Write(ref called, true); + } + }; + + lock (locker) + { + manager.FlagWasUpdated(INT_FLAG, 2, 1); + Assert.False(Volatile.Read(ref called)); + } + } + } + + public class FlagChangedEventSink + { + private BlockingCollection _events = new BlockingCollection(); + + public void Handler(object sender, FlagChangedEventArgs args) + { + _events.Add(args); + } + + public FlagChangedEventArgs Await() + { + return _events.Take(); + } + + public bool IsEmpty => _events.Count == 0; + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 8eabf37e..4f2eda7c 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -226,29 +226,18 @@ public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() } [Fact] - public void CanRegisterListener() + public void CanRegisterAndUnregisterFlagChangedHandlers() { using (var client = Client()) { - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(1); - client.RegisterFeatureFlagListener("user1-flag", listener); - Assert.True(client.IsFeatureFlagListenerRegistered("user1-flag", listener)); - } - } - - [Fact] - public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerManager() - { - using (var client = Client()) - { - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(1); - client.RegisterFeatureFlagListener("user2-flag", listener); - Assert.True(client.IsFeatureFlagListenerRegistered("user2-flag", listener)); - - client.UnregisterFeatureFlagListener("user2-flag", listener); - Assert.False(client.IsFeatureFlagListenerRegistered("user2-flag", listener)); + EventHandler handler1 = (sender, args) => { }; + EventHandler handler2 = (sender, args) => { }; + var eventManager = client.flagChangedEventManager as FlagChangedEventManager; + client.FlagChanged += handler1; + client.FlagChanged += handler2; + client.FlagChanged -= handler1; + Assert.False(eventManager.IsHandlerRegistered(handler1)); + Assert.True(eventManager.IsHandlerRegistered(handler2)); } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 05b35799..ca31976b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -148,8 +148,7 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string .WithEventProcessor(new MockEventProcessor()) .WithUpdateProcessorFactory(MockPollingProcessor.Factory(null)) .WithPersistentStorage(new MockPersistentStorage()) - .WithDeviceInfo(new MockDeviceInfo("")) - .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); + .WithDeviceInfo(new MockDeviceInfo("")); return configuration; } } diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index a2988ae1..b04ca764 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -151,9 +151,6 @@ SharedTestCode\ConfigurationTest.cs - - SharedTestCode\FeatureFlagListenerTests.cs - SharedTestCode\FeatureFlagRequestorTests.cs @@ -163,6 +160,9 @@ SharedTestCode\FlagCacheManagerTests.cs + + SharedTestCode\FlagChangedEventTests.cs + SharedTestCode\LdClientEndToEndTests.cs From 157b9d087e93f093a239faf43d1142b9e7e12c90 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 17 Jul 2019 11:50:21 -0700 Subject: [PATCH 171/254] doc comments --- .../FlagChangedEvent.cs | 79 +++++++++++++++++-- .../ILdMobileClient.cs | 3 +- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index de534699..a630cf0c 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -5,23 +5,88 @@ namespace LaunchDarkly.Xamarin { + /// + /// An event object that is sent to handlers for the event. + /// public class FlagChangedEventArgs { + /// + /// The unique key of the feature flag whose value has changed. + /// public string Key { get; private set; } + /// + /// The updated value of the flag for the current user. + /// + /// + /// Since flag values can be of any JSON type, this property is a . The properties + /// , , etc. are shortcuts for accessing its value with a + /// specific data type. + /// + /// Flag evaluations always produce non-null values, but this property could still be null if the flag was + /// completely deleted or if it could not be evaluated due to an error of some kind. + /// + /// Note that in those cases, the Variation methods may return a different result from this property, + /// because of their "default value" behavior. For instance, if the flag "feature1" has been deleted, the + /// following expression will return the string "xyz", because that is the default value that you specified + /// in the method call: + /// + /// + /// client.StringVariation("feature1", "xyz"); + /// + /// + /// But when a FlagChangedEvent is sent for the deletion of the flag, it has no way to know that you + /// would have specified "xyz" as a default value when evaluating the flag, so NewValue will simply + /// be null. + /// public JToken NewValue { get; private set; } + /// + /// The last known value of the flag for the current user prior to the update. + /// public JToken OldValue { get; private set; } + /// + /// True if the flag was completely removed from the environment. + /// public bool FlagWasDeleted { get; private set; } - public bool NewBoolValue => NewValue.Value(); - - public string NewStringValue => NewValue.Value(); - - public int NewIntValue => NewValue.Value(); - - public float NewFloatValue => NewValue.Value(); + /// + /// Shortcut for converting to a bool. Returns false if the value is null or is not a + /// boolean (will never throw an exception). + /// + public bool NewBoolValue => AsType(ValueTypes.Bool, false); + + /// + /// Shortcut for converting to a string. Returns null if the value is null or is not a + /// string (will never throw an exception). + /// + public string NewStringValue => AsType(ValueTypes.String, null); + + /// + /// Shortcut for converting to an int. Returns 0 if the value is null or is not + /// numeric (will never throw an exception). + /// + public int NewIntValue => AsType(ValueTypes.Int, 0); + + /// + /// Shortcut for converting to a float. Returns 0 if the value is null or is not + /// numeric (will never throw an exception). + /// + public float NewFloatValue => AsType(ValueTypes.Float, 0); + + private T AsType(ValueType valueType, T defaultValue) + { + if (NewValue != null) + { + try + { + return valueType.ValueFromJson(NewValue); + } + catch (ArgumentException) {} + } + return defaultValue; + } internal FlagChangedEventArgs(string key, JToken newValue, JToken oldValue, bool flagWasDeleted) { diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index ee9eb6c9..21f33952 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -155,8 +155,7 @@ public interface ILdMobileClient : ILdCommonClient /// /// /// This could mean that the flag configuration was changed in LaunchDarkly, or that you have changed the current - /// user and the flag values are different for this user than for the previous user. The event is not - /// triggered for the flag values that are first obtained when the client is initialized. It is only triggered + /// user and the flag values are different for this user than for the previous user. The event is only triggered /// if the newly received flag value is actually different from the previous one. /// /// The properties will indicate the key of the feature flag, the new value, From 64eef47fe49a1bf80d1a969224c797d1884aa59a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 17 Jul 2019 18:07:32 -0700 Subject: [PATCH 172/254] try pinning to older versions of Mono packages --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6432a0c7..d0284371 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ jobs: sudo apt -y install apt-transport-https dirmngr gnupg ca-certificates sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - sudo apt -y install mono-devel nuget libzip4 libpulse0 + sudo apt -y install mono-devel=5.20.1.19-0xamarin2+debian9b1 nuget=4.8.2.5835.bin-0xamarin1+debian9b1 libzip4 libpulse0 # libpulse0 is required to run the emulator; the result are required to build the app - restore_cache: From 7e986ab4a55435a995a70b2cd362b1b78d718a85 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 17 Jul 2019 18:18:05 -0700 Subject: [PATCH 173/254] pin apt repo --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d0284371..42fffc9a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,8 +38,8 @@ jobs: command: | sudo apt -y install apt-transport-https dirmngr gnupg ca-certificates sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF - echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - sudo apt -y install mono-devel=5.20.1.19-0xamarin2+debian9b1 nuget=4.8.2.5835.bin-0xamarin1+debian9b1 libzip4 libpulse0 + echo "deb https://download.mono-project.com/repo/debian stable-stretch/snapshots/5.20.1.19 main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update + sudo apt -y install mono-devel nuget libzip4 libpulse0 # libpulse0 is required to run the emulator; the result are required to build the app - restore_cache: From 2569142d7b442e648487a6cf62b10cb4a4738dc1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 24 Jul 2019 14:06:40 -0700 Subject: [PATCH 174/254] bump common lib version, use User.Builder --- .../LaunchDarkly.XamarinSdk.csproj | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 23 ++++++++++++++----- .../LDClientEndToEndTests.cs | 4 +++- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LdClientTests.cs | 23 ++++++++++--------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 3c13cb95..0ea57782 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 6e3580e4..6e3e2c31 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -499,22 +499,33 @@ void ClearUpdateProcessor() User DecorateUser(User user) { - var newUser = new User(user); + IUserBuilder buildUser = null; if (UserMetadata.DeviceName != null) { - newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); + if (buildUser is null) + { + buildUser = User.Builder(user); + } + buildUser.Custom("device", UserMetadata.DeviceName); } if (UserMetadata.OSName != null) { - newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); + if (buildUser is null) + { + buildUser = User.Builder(user); + } + buildUser.Custom("os", UserMetadata.OSName); } // If you pass in a user with a null or blank key, one will be assigned to them. if (String.IsNullOrEmpty(user.Key)) { - newUser.Key = deviceInfo.UniqueDeviceId(); - newUser.Anonymous = true; + if (buildUser is null) + { + buildUser = User.Builder(user); + } + buildUser.Key(deviceInfo.UniqueDeviceId()).Anonymous(true); } - return newUser; + return buildUser is null ? user : buildUser.Build(); } public void Dispose() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 183c7dc5..02f3bd8e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -154,7 +154,8 @@ await WithServerAsync(async server => server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); - var anonUser = User.WithKey(null).AndAnonymous(true); + var name = "Sue"; + var anonUser = User.Builder((string)null).Name(name).Anonymous(true).Build(); // Note, on mobile platforms, the generated user key is the device ID and is stable; on other platforms, // it's a GUID that is cached in local storage. Calling ClearCachedClientId() resets the latter. @@ -165,6 +166,7 @@ await WithServerAsync(async server => { Assert.NotNull(client.User.Key); generatedKey = client.User.Key; + Assert.Equal(name, client.User.Name); } using (var client = await TestUtil.CreateClientAsync(config, anonUser)) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 0f8ebb7f..cf929fbb 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 4f2eda7c..6729ca2d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -194,16 +194,17 @@ public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() [Fact] public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() { - var user = User.WithKey("") - .AndSecondaryKey("secondary") - .AndIpAddress("10.0.0.1") - .AndCountry("US") - .AndFirstName("John") - .AndLastName("Doe") - .AndName("John Doe") - .AndAvatar("images.google.com/myAvatar") - .AndEmail("test@example.com") - .AndCustomAttribute("attr", "value"); + var user = User.Builder("") + .SecondaryKey("secondary") + .IPAddress("10.0.0.1") + .Country("US") + .FirstName("John") + .LastName("Doe") + .Name("John Doe") + .Avatar("images.google.com/myAvatar") + .Email("test@example.com") + .Custom("attr", "value") + .Build(); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") .WithDeviceInfo(new MockDeviceInfo(uniqueId)); @@ -218,7 +219,7 @@ public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() Assert.Equal(user.FirstName, newUser.FirstName); Assert.Equal(user.LastName, newUser.LastName); Assert.Equal(user.Name, newUser.Name); - Assert.Equal(user.IpAddress, newUser.IpAddress); + Assert.Equal(user.IPAddress, newUser.IPAddress); Assert.Equal(user.SecondaryKey, newUser.SecondaryKey); Assert.Equal(user.Custom["attr"], newUser.Custom["attr"]); Assert.True(newUser.Anonymous); From 8a6fe6acb14554e27f520242326ec928776aa4ab Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 24 Jul 2019 16:29:05 -0700 Subject: [PATCH 175/254] Moved some functions for greater clarity, fixed isInitialized behavior, fixed StartUpdateProcessorAsync hanging while offline --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 104 +++++++++--------- .../PlatformSpecific/Connectivity.android.cs | 4 - .../PlatformSpecific/Connectivity.shared.cs | 2 +- 3 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index e4d5058d..f0aebeed 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -55,22 +55,29 @@ public sealed class LdClient : ILdMobileClient SemaphoreSlim connectionLock; + bool online; + /// + public bool Online + { + get => online; + set + { + var doNotAwaitResult = SetOnlineAsync(value); + } + } + // private constructor prevents initialization of this class // without using WithConfigAnduser(config, user) LdClient() { } LdClient(Configuration configuration, User user) { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } if (user == null) { throw new ArgumentNullException("user"); } - Config = configuration; + Config = configuration ?? throw new ArgumentNullException("configuration"); connectionLock = new SemaphoreSlim(1, 1); @@ -217,17 +224,7 @@ static LdClient CreateInstance(Configuration configuration, User user) var c = new LdClient(configuration, user); Instance = c; Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); - return c; - } - - bool StartUpdateProcessor(TimeSpan maxWaitTime) - { - return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); - } - - Task StartUpdateProcessorAsync() - { - return updateProcessor.Start(); + return c; } void SetupConnectionManager() @@ -241,17 +238,6 @@ void SetupConnectionManager() online = connectionManager.IsConnected; } - bool online; - /// - public bool Online - { - get => online; - set - { - var doNotAwaitResult = SetOnlineAsync(value); - } - } - public async Task SetOnlineAsync(bool value) { await connectionLock.WaitAsync(); @@ -385,6 +371,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => result = new EvaluationDetail(defaultValue, null, new EvaluationReason.Error(EvaluationErrorKind.WRONG_TYPE)); } } + var featureEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, User, new EvaluationDetail(valueJson, flag.variation, flag.reason), defaultJson); eventProcessor.SendEvent(featureEvent); @@ -424,7 +411,7 @@ public void Track(string eventName) /// public bool Initialized() { - return Online && updateProcessor.Initialized(); + return Online; } /// @@ -469,6 +456,19 @@ public async Task IdentifyAsync(User user) eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(newUser)); } + bool StartUpdateProcessor(TimeSpan maxWaitTime) + { + return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); + } + + Task StartUpdateProcessorAsync() + { + if (Online) + return updateProcessor.Start(); + else + return Task.FromResult(true); + } + async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) { ClearAndSetUpdateProcessor(pollingInterval); @@ -490,24 +490,24 @@ void ClearUpdateProcessor() } } - User DecorateUser(User user) - { + User DecorateUser(User user) + { var newUser = new User(user); - if (UserMetadata.DeviceName != null) - { - newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); + if (UserMetadata.DeviceName != null) + { + newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); } - if (UserMetadata.OSName != null) - { - newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); + if (UserMetadata.OSName != null) + { + newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); } // If you pass in a user with a null or blank key, one will be assigned to them. - if (String.IsNullOrEmpty(user.Key)) + if (String.IsNullOrEmpty(user.Key)) { newUser.Key = deviceInfo.UniqueDeviceId(); - newUser.Anonymous = true; + newUser.Anonymous = true; } - return newUser; + return newUser; } void IDisposable.Dispose() @@ -547,8 +547,8 @@ public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener lis public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) { flagListenerManager.UnregisterListener(listener, flagKey); - } - + } + internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); @@ -556,19 +556,19 @@ internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventA internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) { - if (args.IsInBackground) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - if (Config.EnableBackgroundUpdating) - { - await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); - } + if (args.IsInBackground) + { + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + if (Config.EnableBackgroundUpdating) + { + await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); + } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } - else - { - ResetProcessorForForeground(); + else + { + ResetProcessorForForeground(); await RestartUpdateProcessorAsync(Config.PollingInterval); } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs index a074cc95..1413f2d0 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs @@ -86,9 +86,6 @@ static NetworkAccess PlatformNetworkAccess var info = manager.GetNetworkInfo(network); - if (info == null || !info.IsAvailable) - continue; - // Check to see if it has the internet capability if (!capabilities.HasCapability(NetCapability.Internet)) { @@ -119,7 +116,6 @@ void ProcessNetworkInfo(NetworkInfo info) { if (info == null || !info.IsAvailable) return; - if (info.IsConnected) currentAccess = IsBetterAccess(currentAccess, NetworkAccess.Internet); else if (info.IsConnectedOrConnecting) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs index 942350d2..ea018a7d 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.shared.cs @@ -49,7 +49,7 @@ internal static partial class Connectivity static event EventHandler ConnectivityChangedInternal; // a cache so that events aren't fired unnecessarily - // this is mainly an issue on Android, but we can stiil do this everywhere + // this is mainly an issue on Android, but we can still do this everywhere static NetworkAccess currentAccess; static List currentProfiles; From 970708983a74b3601d6df26b8a5fb69f90b46d60 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 24 Jul 2019 16:31:47 -0700 Subject: [PATCH 176/254] Fixed NullReferenceException in ResetProcessorForForeground --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 82 ++++++++++++------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 6e3580e4..9bc15db1 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -213,7 +213,7 @@ public static Task InitAsync(Configuration config, User user) static LdClient CreateInstance(Configuration configuration, User user) { - lock (createInstanceLock) + lock (createInstanceLock) { if (Instance != null) { @@ -223,8 +223,8 @@ static LdClient CreateInstance(Configuration configuration, User user) var c = new LdClient(configuration, user); Interlocked.CompareExchange(ref instance, c, null); Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); - return c; - } + return c; + } } bool StartUpdateProcessor(TimeSpan maxWaitTime) @@ -497,24 +497,24 @@ void ClearUpdateProcessor() } } - User DecorateUser(User user) - { + User DecorateUser(User user) + { var newUser = new User(user); - if (UserMetadata.DeviceName != null) - { - newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); + if (UserMetadata.DeviceName != null) + { + newUser = newUser.AndCustomAttribute("device", UserMetadata.DeviceName); } - if (UserMetadata.OSName != null) - { - newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); + if (UserMetadata.OSName != null) + { + newUser = newUser.AndCustomAttribute("os", UserMetadata.OSName); } // If you pass in a user with a null or blank key, one will be assigned to them. - if (String.IsNullOrEmpty(user.Key)) + if (String.IsNullOrEmpty(user.Key)) { newUser.Key = deviceInfo.UniqueDeviceId(); - newUser.Anonymous = true; + newUser.Anonymous = true; } - return newUser; + return newUser; } public void Dispose() @@ -538,9 +538,9 @@ void Dispose(bool disposing) } } - internal void DetachInstance() // exposed for testing - { - Interlocked.CompareExchange(ref instance, null, this); + internal void DetachInstance() // exposed for testing + { + Interlocked.CompareExchange(ref instance, null, this); } /// @@ -553,18 +553,18 @@ public Version Version } /// - public event EventHandler FlagChanged - { - add - { - flagChangedEventManager.FlagChanged += value; - } - remove - { - flagChangedEventManager.FlagChanged -= value; - } - } - + public event EventHandler FlagChanged + { + add + { + flagChangedEventManager.FlagChanged += value; + } + remove + { + flagChangedEventManager.FlagChanged -= value; + } + } + internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); @@ -572,19 +572,19 @@ internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventA internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) { - if (args.IsInBackground) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - if (Config.EnableBackgroundUpdating) - { - await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); - } + if (args.IsInBackground) + { + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + if (Config.EnableBackgroundUpdating) + { + await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); + } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } - else - { - ResetProcessorForForeground(); + else + { + ResetProcessorForForeground(); await RestartUpdateProcessorAsync(Config.PollingInterval); } } @@ -592,7 +592,7 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh void ResetProcessorForForeground() { string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); - if (didBackground.Equals("true")) + if (didBackground != null && didBackground.Equals("true")) { persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); ClearUpdateProcessor(); From 8e022418271dfc689d24e9567197e0dbea84a17d Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Thu, 25 Jul 2019 10:09:17 -0700 Subject: [PATCH 177/254] Added nameof(configuration), added Online check to StartUpdateProcessor --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index f0aebeed..9df5ef56 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -77,7 +77,7 @@ public bool Online throw new ArgumentNullException("user"); } - Config = configuration ?? throw new ArgumentNullException("configuration"); + Config = nameof(configuration) ?? throw new ArgumentNullException("configuration"); connectionLock = new SemaphoreSlim(1, 1); @@ -458,7 +458,10 @@ public async Task IdentifyAsync(User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { - return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); + if (Online) + return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); + else + return true; } Task StartUpdateProcessorAsync() From 4d36317101bd1c6cb9879e41d06c035a46e6a35e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 25 Jul 2019 10:21:11 -0700 Subject: [PATCH 178/254] use prebuilt Xamarin Android image (#55) --- .circleci/config.yml | 38 ++------------------------ scripts/check_xamarin_android_cache.sh | 11 -------- 2 files changed, 2 insertions(+), 47 deletions(-) delete mode 100755 scripts/check_xamarin_android_cache.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 42fffc9a..e26d43b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,52 +22,18 @@ jobs: test-android: docker: - - image: circleci/android:api-27 + - image: ldcircleci/ld-xamarin-android-linux steps: - checkout - - run: - name: Install Android SDK tools - command: | - sdkmanager "system-images;android-24;default;armeabi-v7a" || true - sdkmanager --licenses - - - run: - name: Install Mono tools - command: | - sudo apt -y install apt-transport-https dirmngr gnupg ca-certificates - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF - echo "deb https://download.mono-project.com/repo/debian stable-stretch/snapshots/5.20.1.19 main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && sudo apt update - sudo apt -y install mono-devel nuget libzip4 libpulse0 - # libpulse0 is required to run the emulator; the result are required to build the app - - - restore_cache: - key: xamarin-android-cache-v9-2-99-172 - - run: - name: Download Xamarin Android tools if not already cached - command: ./scripts/check_xamarin_android_cache.sh - - save_cache: - key: xamarin-android-cache-v9-2-99-172 - paths: - - ~/project/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release - - - run: - name: Move tools to expected locations - command: | - sudo mkdir "/usr/lib/xamarin.android" && sudo mkdir "/usr/lib/mono/xbuild/Xamarin/" - cd ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release && sudo cp -a "bin/Debug/lib/xamarin.android/." "/usr/lib/xamarin.android/" - rm -rf "/usr/lib/mono/xbuild/Xamarin/Android" && rm -rf "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - sudo ln -s "/usr/lib/xamarin.android/xbuild/Xamarin/Android/" "/usr/lib/mono/xbuild/Xamarin/Android" - sudo ln -s "/usr/lib/xamarin.android/xbuild-frameworks/MonoAndroid/" "/usr/lib/mono/xbuild-frameworks/MonoAndroid" - - run: name: Build SDK command: msbuild /restore /p:TargetFramework=MonoAndroid81 src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj - run: name: Build test project - command: xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release/bin/Debug/bin/xabuild /restore /t:SignAndroidPackage tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj + command: ~/xamarin-android/bin/Debug/bin/xabuild /restore /t:SignAndroidPackage tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj # Note, xabuild is just a wrapper for msbuild that adds various tools paths etc. necessary for building an # Android app. See: https://github.com/xamarin/xamarin-android/blob/master/tools/scripts/xabuild diff --git a/scripts/check_xamarin_android_cache.sh b/scripts/check_xamarin_android_cache.sh deleted file mode 100755 index 41eae00a..00000000 --- a/scripts/check_xamarin_android_cache.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# used only for CI build - -if [ -e "xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release" ]; then - echo "Xamarin Android cache exists" -else - wget https://jenkins.mono-project.com/view/Xamarin.Android/job/xamarin-android-linux/lastSuccessfulBuild/Azure/processDownloadRequest/xamarin-android/xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 - tar xjf ./xamarin.android-oss_v9.2.99.172_Linux-x86_64_master_d33bbd8e-Release.tar.bz2 - echo "Downloaded Xamarin Android from Mono Jenkins" -fi From 306eaaab4667dcf8cd0ab8a146a3e64795e49c32 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Thu, 25 Jul 2019 10:25:09 -0700 Subject: [PATCH 179/254] Applied nameof to incorrect configuration --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 1fce5b6a..ef84e39b 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -81,7 +81,7 @@ public bool Online throw new ArgumentNullException(nameof(user)); } - Config = nameof(configuration) ?? throw new ArgumentNullException("configuration"); + Config = configuration ?? throw new ArgumentNullException(nameof(configuration)); connectionLock = new SemaphoreSlim(1, 1); From 219ff8137d3902ef42612dca7ef91b60d11eb92c Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Thu, 25 Jul 2019 12:20:09 -0700 Subject: [PATCH 180/254] Added back null check in Android Connectivity --- .../PlatformSpecific/Connectivity.android.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs index 1413f2d0..c8c6945c 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Connectivity.android.cs @@ -86,6 +86,9 @@ static NetworkAccess PlatformNetworkAccess var info = manager.GetNetworkInfo(network); + if (info == null || !info.IsAvailable) + continue; + // Check to see if it has the internet capability if (!capabilities.HasCapability(NetCapability.Internet)) { From 55207d646656203a021be91b239fca7e14621394 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Thu, 25 Jul 2019 14:49:24 -0700 Subject: [PATCH 181/254] Reverted Initialized behavior, was a red herring during debugging --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index ef84e39b..a0c711de 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -418,7 +418,7 @@ public void Track(string eventName) /// public bool Initialized() { - return Online; + return Online && updateProcessor.Initialized(); } /// From ce8cf9d29aa6abe0e252e6718141915edf6adbf1 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Thu, 25 Jul 2019 14:55:39 -0700 Subject: [PATCH 182/254] Added volatile to online per review comment --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index a0c711de..b3edcbb6 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -59,7 +59,7 @@ public sealed class LdClient : ILdMobileClient readonly SemaphoreSlim connectionLock; - bool online; + volatile bool online; /// public bool Online { From ce865ad92a180b89bad7c5ec71d0a462620c0f40 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Sat, 27 Jul 2019 15:40:49 -0700 Subject: [PATCH 183/254] change tag of Xamarin Android Docker image --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e26d43b3..d4cd36d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: test-android: docker: - - image: ldcircleci/ld-xamarin-android-linux + - image: ldcircleci/ld-xamarin-android-linux:api27 steps: - checkout From 8264b3ce52172a392aa53803806da698960facde Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 29 Jul 2019 11:41:29 -0700 Subject: [PATCH 184/254] Add api28 docker tag to circleci android test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e26d43b3..180d308b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: test-android: docker: - - image: ldcircleci/ld-xamarin-android-linux + - image: ldcircleci/ld-xamarin-android-linux:api28 steps: - checkout From 52c15dab987a5808ce818fb73c210426517796f8 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 29 Jul 2019 11:45:46 -0700 Subject: [PATCH 185/254] Add api27 docker tag to circleci android test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 180d308b..d4cd36d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: test-android: docker: - - image: ldcircleci/ld-xamarin-android-linux:api28 + - image: ldcircleci/ld-xamarin-android-linux:api27 steps: - checkout From 50e4c810384e66db473738986a09c8fd5b69fe11 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 29 Jul 2019 15:29:48 -0700 Subject: [PATCH 186/254] Added tests to trigger StartUpdateProcessor and StartUpdateProcessorAsync while offline --- .../LDClientEndToEndTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 02f3bd8e..64d94b1e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -284,6 +284,62 @@ await WithServerAsync(async server => }); } + [Fact(Skip = SkipIfCannotCreateHttpServer)] + public void BackgroundOfflineClientUsesCachedFlagsSync() + { + WithServer(server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + using (var client = TestUtil.CreateClient(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + }); + } + + [Fact(Skip = SkipIfCannotCreateHttpServer)] + public async Task BackgroundOfflineClientUsesCachedFlagsAsync() + { + await WithServerAsync(async server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + }); + } + private Configuration BaseConfig(FluentMockServer server) { return Configuration.Default(_mobileKey) From 881cbdffa3a73144ad241c2c780996793076826f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 29 Jul 2019 16:12:58 -0700 Subject: [PATCH 187/254] use immutable user API --- src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 0ea57782..ce250990 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -23,7 +23,7 @@ - + From 080e12eb5e4690066cc10766fdd7d455de7db770 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 29 Jul 2019 16:15:29 -0700 Subject: [PATCH 188/254] update dependency --- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index cf929fbb..22e0c8a5 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + From d3f10fdeaab4816fdc18e068e0d3346d80a1df19 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 29 Jul 2019 16:32:34 -0700 Subject: [PATCH 189/254] fix encoded user constants in tests --- .../LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs | 2 +- .../MobileStreamingProcessorTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index 9788ef5d..2b53e47b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -10,7 +10,7 @@ public class FeatureFlagRequestorTests : BaseTest private const string _mobileKey = "FAKE_KEY"; private static readonly User _user = User.WithKey("foo"); - private const string _encodedUser = "eyJrZXkiOiJmb28iLCJjdXN0b20iOnt9fQ=="; + private const string _encodedUser = "eyJrZXkiOiJmb28iLCJhbm9ueW1vdXMiOmZhbHNlLCJjdXN0b20iOnt9fQ=="; // Note that in a real use case, the user encoding may vary depending on the target platform, because the SDK adds custom // user attributes like "os". But the lower-level FeatureFlagRequestor component does not do that. diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index a7f8b1d0..562753e4 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -18,7 +18,7 @@ public class MobileStreamingProcessorTests : BaseTest "}"; private readonly User user = User.WithKey("me"); - private const string encodedUser = "eyJrZXkiOiJtZSIsImN1c3RvbSI6e319"; + private const string encodedUser = "eyJrZXkiOiJtZSIsImFub255bW91cyI6ZmFsc2UsImN1c3RvbSI6e319"; private EventSourceMock mockEventSource; private TestEventSourceFactory eventSourceFactory; From 85f1c01290a1dec43f6fb3996e535ccee0f27370 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 29 Jul 2019 16:52:18 -0700 Subject: [PATCH 190/254] Changed misleading test names --- tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 64d94b1e..1ad970b9 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -285,7 +285,7 @@ await WithServerAsync(async server => } [Fact(Skip = SkipIfCannotCreateHttpServer)] - public void BackgroundOfflineClientUsesCachedFlagsSync() + public void OfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() { WithServer(server => { @@ -313,7 +313,7 @@ public void BackgroundOfflineClientUsesCachedFlagsSync() } [Fact(Skip = SkipIfCannotCreateHttpServer)] - public async Task BackgroundOfflineClientUsesCachedFlagsAsync() + public async Task OfflineClientUsesCachedFlagsAsyncAfterStartUpdateProcessorAsync() { await WithServerAsync(async server => { From df1f0e9609b594de3bdaae21f2dd09bb7ab5e97a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 29 Jul 2019 22:00:41 -0700 Subject: [PATCH 191/254] add tests for user platform metadata --- .../LaunchDarkly.XamarinSdk.csproj | 2 +- .../AndroidSpecificTests.cs | 23 +++++++++++++++++++ ...unchDarkly.XamarinSdk.Android.Tests.csproj | 1 + .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../IOsSpecificTests.cs | 23 +++++++++++++++++++ .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 1 + 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index ce250990..b13b3e72 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -23,7 +23,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs new file mode 100644 index 00000000..35fb99da --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs @@ -0,0 +1,23 @@ +using LaunchDarkly.Client; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class AndroidSpecificTests + { + [Fact] + public void UserHasOSAndDeviceAttributesForPlatform() + { + var baseUser = User.WithKey("key"); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}"); + using (var client = TestUtil.CreateClient(config, baseUser)) + { + var user = client.User; + Assert.Equal(baseUser.Key, user.Key); + Assert.Contains("os", user.Custom.Keys); + Assert.StartsWith("Android ", user.Custom["os"].AsString); + Assert.Contains("device", user.Custom.Keys); + } + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index 58182c3f..92602515 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -108,6 +108,7 @@ SharedTestCode\WireMockExtensions.cs + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 22e0c8a5..90d7d771 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs new file mode 100644 index 00000000..32d619ad --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -0,0 +1,23 @@ +using LaunchDarkly.Client; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class IOsSpecificTests + { + [Fact] + public void UserHasOSAndDeviceAttributesForPlatform() + { + var baseUser = User.WithKey("key"); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}"); + using (var client = TestUtil.CreateClient(config, baseUser)) + { + var user = client.User; + Assert.Equal(baseUser.Key, user.Key); + Assert.Contains("os", user.Custom.Keys); + Assert.StartsWith("iOs ", user.Custom["os"].AsString); + Assert.Contains("device", user.Custom.Keys); + } + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index b04ca764..db128f57 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -193,6 +193,7 @@ SharedTestCode\WireMockExtensions.cs + From 9ead7218dc95ce49adaeff90395be38767b34562 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 29 Jul 2019 23:03:14 -0700 Subject: [PATCH 192/254] typo --- .../IOsSpecificTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs index 32d619ad..c16ffe57 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -1,13 +1,13 @@ -using LaunchDarkly.Client; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class IOsSpecificTests - { - [Fact] - public void UserHasOSAndDeviceAttributesForPlatform() - { +using LaunchDarkly.Client; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class IOsSpecificTests + { + [Fact] + public void UserHasOSAndDeviceAttributesForPlatform() + { var baseUser = User.WithKey("key"); var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}"); using (var client = TestUtil.CreateClient(config, baseUser)) @@ -15,9 +15,9 @@ public void UserHasOSAndDeviceAttributesForPlatform() var user = client.User; Assert.Equal(baseUser.Key, user.Key); Assert.Contains("os", user.Custom.Keys); - Assert.StartsWith("iOs ", user.Custom["os"].AsString); + Assert.StartsWith("iOS ", user.Custom["os"].AsString); Assert.Contains("device", user.Custom.Keys); } - } - } -} + } + } +} From f7ddb29509518c0f37e3a288f3f681eb1ed58959 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 00:32:28 -0700 Subject: [PATCH 193/254] make Configuration immutable, add builder --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 731 ++++-------------- .../ConfigurationBuilder.cs | 448 +++++++++++ src/LaunchDarkly.XamarinSdk/Factory.cs | 26 +- .../IMobileConfiguration.cs | 21 +- .../LaunchDarkly.XamarinSdk.csproj | 9 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 116 +-- .../ConfigurationTest.cs | 31 +- .../FeatureFlagRequestorTests.cs | 19 +- .../LDClientEndToEndTests.cs | 34 +- .../LdClientEvaluationTests.cs | 2 +- .../LdClientEventTests.cs | 6 +- .../LdClientTests.cs | 51 +- .../MobileStreamingProcessorTests.cs | 48 +- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 17 +- 14 files changed, 806 insertions(+), 753 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index 763ddfb0..e684369f 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -1,674 +1,265 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Net.Http; -using Common.Logging; using LaunchDarkly.Client; namespace LaunchDarkly.Xamarin { /// - /// This class exposes advanced configuration options for . + /// This class exposes advanced configuration options for . /// + /// + /// Instances of Configuration are immutable once created. They can be created with the factory method + /// , or using a builder pattern with + /// or . + /// public class Configuration : IMobileConfiguration { - /// - /// The base URI of the LaunchDarkly server. - /// - public Uri BaseUri { get; internal set; } - /// - /// The base URL of the LaunchDarkly streaming server. - /// - public Uri StreamUri { get; internal set; } - /// - /// The base URL of the LaunchDarkly analytics event server. - /// - public Uri EventsUri { get; internal set; } - /// - /// The Mobile key for your LaunchDarkly environment. - /// - public string MobileKey { get; internal set; } - /// - /// The SDK key for your LaunchDarkly environment. This is the Mobile key. - /// - /// Returns the Mobile Key. - public string SdkKey { get { return MobileKey; } } - /// - /// Whether or not the streaming API should be used to receive flag updates. This is true by default. - /// Streaming should only be disabled on the advice of LaunchDarkly support. - /// - public bool IsStreamingEnabled { get; internal set; } - /// - /// The capacity of the events buffer. The client buffers up to this many events in - /// memory before flushing. If the capacity is exceeded before the buffer is flushed, - /// events will be discarded. Increasing the capacity means that events are less likely - /// to be discarded, at the cost of consuming more memory. - /// - public int EventQueueCapacity { get; internal set; } - /// - /// The time between flushes of the event buffer. Decreasing the flush interval means - /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. - /// - public TimeSpan EventQueueFrequency { get; internal set; } - /// - /// Enables event sampling if non-zero. When set to the default of zero, all analytics events are - /// sent back to LaunchDarkly. When greater than zero, there is a 1 in EventSamplingInterval - /// chance that events will be sent (example: if the interval is 20, on average 5% of events will be sent). - /// - public int EventSamplingInterval { get; internal set; } - /// - /// Set the polling interval (when streaming is disabled). The default value is 30 seconds. - /// - public TimeSpan PollingInterval { get; internal set; } - /// - /// The timeout when reading data from the EventSource API. The default value is 5 minutes. - /// - public TimeSpan ReadTimeout { get; internal set; } - /// - /// The reconnect base time for the streaming connection.The streaming connection - /// uses an exponential backoff algorithm (with jitter) for reconnects, but will start the - /// backoff with a value near the value specified here. The default value is 1 second. - /// - public TimeSpan ReconnectTime { get; internal set; } - /// - /// The connection timeout. The default value is 10 seconds. - /// - public TimeSpan HttpClientTimeout { get; internal set; } - /// - /// The object to be used for sending HTTP requests. This is exposed for testing purposes. - /// - public HttpClientHandler HttpClientHandler { get; internal set; } - /// - /// Whether or not this client is offline. If true, no calls to Launchdarkly will be made. - /// - public bool Offline { get; internal set; } + private readonly bool _allAttributesPrivate; + private readonly TimeSpan _backgroundPollingInterval; + private readonly Uri _baseUri; + private readonly TimeSpan _connectionTimeout; + private readonly bool _enableBackgroundUpdating; + private readonly bool _evaluationReasons; + private readonly TimeSpan _eventFlushInterval; + private readonly int _eventCapacity; + private readonly int _eventSamplingInterval; + private readonly Uri _eventsUri; + private readonly HttpClientHandler _httpClientHandler; + private readonly TimeSpan _httpClientTimeout; + private readonly bool _inlineUsersInEvents; + private readonly bool _isStreamingEnabled; + private readonly string _mobileKey; + private readonly bool _offline; + private readonly bool _persistFlagValues; + private readonly TimeSpan _pollingInterval; + private readonly ImmutableHashSet _privateAttributeNames; + private readonly TimeSpan _readTimeout; + private readonly TimeSpan _reconnectTime; + private readonly Uri _streamUri; + private readonly bool _useReport; + private readonly int _userKeysCapacity; + private readonly TimeSpan _userKeysFlushInterval; + + // Settable only for testing + internal readonly IConnectionManager _connectionManager; + internal readonly IDeviceInfo _deviceInfo; + internal readonly IEventProcessor _eventProcessor; + internal readonly IFlagCacheManager _flagCacheManager; + internal readonly IFlagChangedEventManager _flagChangedEventManager; + internal readonly IPersistentStorage _persistentStorage; + internal readonly Func _updateProcessorFactory; + /// /// Whether or not user attributes (other than the key) should be private (not sent to /// the LaunchDarkly server). If this is true, all of the user attributes will be private, /// not just the attributes specified with the AndPrivate... methods on the /// object. By default, this is false. /// - public bool AllAttributesPrivate { get; internal set; } + public bool AllAttributesPrivate => _allAttributesPrivate; + + /// + public TimeSpan BackgroundPollingInterval => _backgroundPollingInterval; + /// - /// Marks a set of attribute names as private. Any users sent to LaunchDarkly with this - /// configuration active will have attributes with these names removed, even if you did - /// not use the AndPrivate... methods on the object. - /// - public ISet PrivateAttributeNames { get; internal set; } - /// - /// The number of user keys that the event processor can remember at any one time, so that - /// duplicate user details will not be sent in analytics events. - /// - public int UserKeysCapacity { get; internal set; } - /// - /// The interval at which the event processor will reset its set of known user keys. The - /// default value is five minutes. - /// - public TimeSpan UserKeysFlushInterval { get; internal set; } - /// - /// True if full user details should be included in every analytics event. The default is false (events will - /// only include the user key, except for one "index" event that provides the full details for the user). + /// The base URI of the LaunchDarkly server. /// - public bool InlineUsersInEvents { get; internal set; } - /// - /// True if LaunchDarkly should provide additional information about how flag values were - /// calculated. The additional information will then be available through the client's "detail" - /// methods such as . Since this - /// increases the size of network requests, such information is not sent unless you set this option - /// to true. - /// - public bool EvaluationReasons { get; internal set; } - /// - public TimeSpan BackgroundPollingInterval { get; internal set; } + public Uri BaseUri => _baseUri; + /// public TimeSpan ConnectionTimeout { get; internal set; } + /// - public bool EnableBackgroundUpdating { get; internal set; } - /// - public bool UseReport { get; internal set; } - /// - public bool PersistFlagValues { get; internal set; } + public bool EnableBackgroundUpdating => _enableBackgroundUpdating; - internal IFlagCacheManager FlagCacheManager { get; set; } - internal IConnectionManager ConnectionManager { get; set; } - internal IEventProcessor EventProcessor { get; set; } - internal Func UpdateProcessorFactory { get; set; } - internal IPersistentStorage PersistentStorage { get; set; } - internal IDeviceInfo DeviceInfo { get; set; } - internal IFlagChangedEventManager FlagChangedEventManager { get; set; } + /// + public bool EvaluationReasons => _evaluationReasons; /// - /// Default value for . - /// - public static TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); - /// - /// Minimum value for . - /// - public static TimeSpan MinimumPollingInterval = TimeSpan.FromMinutes(5); - /// - /// Default value for . - /// - internal static readonly Uri DefaultUri = new Uri("https://app.launchdarkly.com"); - /// - /// Default value for . - /// - private static readonly Uri DefaultStreamUri = new Uri("https://clientstream.launchdarkly.com"); - /// - /// Default value for . - /// - private static readonly Uri DefaultEventsUri = new Uri("https://mobile.launchdarkly.com"); - /// - /// Default value for . - /// - private static readonly int DefaultEventQueueCapacity = 100; - /// - /// Default value for . - /// - private static readonly TimeSpan DefaultEventQueueFrequency = TimeSpan.FromSeconds(5); - /// - /// Default value for . - /// - private static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); - /// - /// Default value for . - /// - private static readonly TimeSpan DefaultReconnectTime = TimeSpan.FromSeconds(1); - /// - /// Default value for . - /// - private static readonly TimeSpan DefaultHttpClientTimeout = TimeSpan.FromSeconds(10); - /// - /// Default value for . - /// - private static readonly int DefaultUserKeysCapacity = 1000; - /// - /// Default value for . - /// - private static readonly TimeSpan DefaultUserKeysFlushInterval = TimeSpan.FromMinutes(5); - /// - /// The default value for . - /// - private static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(60); - /// - /// The minimum value for . - /// - public static readonly TimeSpan MinimumBackgroundPollingInterval = TimeSpan.FromMinutes(15); - /// - /// The default value for . + /// The time between flushes of the event buffer. Decreasing the flush interval means + /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. /// - private static readonly TimeSpan DefaultConnectionTimeout = TimeSpan.FromSeconds(10); + public TimeSpan EventFlushInterval => _eventFlushInterval; /// - /// Creates a configuration with all parameters set to the default. Use extension methods - /// to set additional parameters. + /// The capacity of the events buffer. The client buffers up to this many events in + /// memory before flushing. If the capacity is exceeded before the buffer is flushed, + /// events will be discarded. Increasing the capacity means that events are less likely + /// to be discarded, at the cost of consuming more memory. /// - /// the SDK key for your LaunchDarkly environment - /// a Configuration instance - public static Configuration Default(string mobileKey) - { - if (String.IsNullOrEmpty(mobileKey)) - { - throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required"); - } - var defaultConfiguration = new Configuration - { - BaseUri = DefaultUri, - StreamUri = DefaultStreamUri, - EventsUri = DefaultEventsUri, - EventQueueCapacity = DefaultEventQueueCapacity, - EventQueueFrequency = DefaultEventQueueFrequency, - PollingInterval = DefaultPollingInterval, - BackgroundPollingInterval = DefaultBackgroundPollingInterval, - ReadTimeout = DefaultReadTimeout, - ReconnectTime = DefaultReconnectTime, - HttpClientTimeout = DefaultHttpClientTimeout, - HttpClientHandler = new HttpClientHandler(), - Offline = false, - MobileKey = mobileKey, - IsStreamingEnabled = true, - AllAttributesPrivate = false, - PrivateAttributeNames = null, - UserKeysCapacity = DefaultUserKeysCapacity, - UserKeysFlushInterval = DefaultUserKeysFlushInterval, - InlineUsersInEvents = false, - EnableBackgroundUpdating = true, - UseReport = true, - PersistFlagValues = true - }; - - return defaultConfiguration; - } - } - - /// - /// Extension methods that can be called on a to add to its properties. - /// - public static class ConfigurationExtensions - { - private static readonly ILog Log = LogManager.GetLogger(typeof(ConfigurationExtensions)); + public int EventCapacity => _eventCapacity; /// - /// Sets the base URI of the LaunchDarkly server for this configuration. + /// Deprecated name for . /// - /// the configuration - /// the base URI as a string - /// the same Configuration instance - public static Configuration WithBaseUri(this Configuration configuration, string uri) - { - if (uri != null) - configuration.BaseUri = new Uri(uri); - - return configuration; - } + [Obsolete] + public int EventQueueCapacity => EventCapacity; /// - /// Sets the base URI of the LaunchDarkly server for this configuration. + /// Deprecated name for . /// - /// the configuration - /// the base URI - /// the same Configuration instance - public static Configuration WithBaseUri(this Configuration configuration, Uri uri) - { - if (uri != null) - configuration.BaseUri = uri; + [Obsolete] + public TimeSpan EventQueueFrequency => EventFlushInterval; - return configuration; - } - - /// - /// Sets the base URL of the LaunchDarkly streaming server for this configuration. + /// + /// Enables event sampling if non-zero. When set to the default of zero, all analytics events are + /// sent back to LaunchDarkly. When greater than zero, there is a 1 in EventSamplingInterval + /// chance that events will be sent (example: if the interval is 20, on average 5% of events will be sent). /// - /// the configuration - /// the stream URI as a string - /// the same Configuration instance - public static Configuration WithStreamUri(this Configuration configuration, string uri) - { - if (uri != null) - configuration.StreamUri = new Uri(uri); - - return configuration; - } + public int EventSamplingInterval => _eventSamplingInterval; /// - /// Sets the base URL of the LaunchDarkly streaming server for this configuration. + /// The base URL of the LaunchDarkly analytics event server. /// - /// the configuration - /// the stream URI - /// the same Configuration instance - public static Configuration WithStreamUri(this Configuration configuration, Uri uri) - { - if (uri != null) - configuration.StreamUri = uri; - - return configuration; - } - + public Uri EventsUri => _eventsUri; + /// - /// Sets the base URL of the LaunchDarkly analytics event server for this configuration. + /// The object to be used for sending HTTP requests. This is exposed for testing purposes. /// - /// the configuration - /// the events URI as a string - /// the same Configuration instance - public static Configuration WithEventsUri(this Configuration configuration, string uri) - { - if (uri != null) - configuration.EventsUri = new Uri(uri); - - return configuration; - } + public HttpClientHandler HttpClientHandler => _httpClientHandler; /// - /// Sets the base URL of the LaunchDarkly analytics event server for this configuration. + /// The connection timeout. The default value is 10 seconds. /// - /// the configuration - /// the events URI - /// the same Configuration instance - public static Configuration WithEventsUri(this Configuration configuration, Uri uri) - { - if (uri != null) - configuration.EventsUri = uri; + public TimeSpan HttpClientTimeout => _httpClientTimeout; - return configuration; - } + /// + /// True if full user details should be included in every analytics event. The default is false (events will + /// only include the user key, except for one "index" event that provides the full details for the user). + /// + public bool InlineUsersInEvents => _inlineUsersInEvents; /// - /// Sets the capacity of the events buffer. The client buffers up to this many events in - /// memory before flushing. If the capacity is exceeded before the buffer is flushed, - /// events will be discarded. Increasing the capacity means that events are less likely - /// to be discarded, at the cost of consuming more memory. + /// Whether or not the streaming API should be used to receive flag updates. This is true by default. + /// Streaming should only be disabled on the advice of LaunchDarkly support. /// - /// the configuration - /// - /// the same Configuration instance - public static Configuration WithEventQueueCapacity(this Configuration configuration, int eventQueueCapacity) - { - configuration.EventQueueCapacity = eventQueueCapacity; - return configuration; - } + public bool IsStreamingEnabled => _isStreamingEnabled; /// - /// Sets the time between flushes of the event buffer. Decreasing the flush interval means - /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. - /// - /// the configuration - /// the flush interval - /// the same Configuration instance - public static Configuration WithEventQueueFrequency(this Configuration configuration, TimeSpan frequency) - { - configuration.EventQueueFrequency = frequency; - return configuration; - } - - /// - /// Enables event sampling if non-zero. When set to the default of zero, all analytics events are - /// sent back to LaunchDarkly. When greater than zero, there is a 1 in EventSamplingInterval - /// chance that events will be sent (example: if the interval is 20, on average 5% of events will be sent). + /// The Mobile key for your LaunchDarkly environment. /// - /// the configuration - /// the sampling interval - /// the same Configuration instance - public static Configuration WithEventSamplingInterval(this Configuration configuration, int interval) - { - if (interval < 0) - { - Log.Warn("EventSamplingInterval cannot be less than zero."); - interval = 0; - } - configuration.EventSamplingInterval = interval; - return configuration; - } + public string MobileKey => _mobileKey; /// - /// Sets the polling interval (when streaming is disabled). Values less than the default of - /// 30 seconds will be changed to the default. + /// Whether or not this client is offline. If true, no calls to Launchdarkly will be made. /// - /// the configuration - /// the rule update polling interval - /// the same Configuration instance - public static Configuration WithPollingInterval(this Configuration configuration, TimeSpan pollingInterval) - { - if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) - { - Log.WarnFormat("PollingInterval cannot be less than the default of {0}.", Configuration.MinimumPollingInterval); - pollingInterval = Configuration.MinimumPollingInterval; - } - configuration.PollingInterval = pollingInterval; - return configuration; - } + public bool Offline => _offline; + + /// + public bool PersistFlagValues => _persistFlagValues; /// - /// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made. + /// Set the polling interval (when streaming is disabled). The default value is 30 seconds. /// - /// the configuration - /// true if the client should remain offline - /// the same Configuration instance - public static Configuration WithOffline(this Configuration configuration, bool offline) - { - configuration.Offline = offline; - return configuration; - } + public TimeSpan PollingInterval => _pollingInterval; /// - /// Sets the connection timeout. The default value is 10 seconds. + /// Marks a set of attribute names as private. Any users sent to LaunchDarkly with this + /// configuration active will have attributes with these names removed, even if you did + /// not use the AndPrivate... methods on the object. /// - /// the configuration - /// the connection timeout - /// the same Configuration instance - public static Configuration WithHttpClientTimeout(this Configuration configuration, TimeSpan timeSpan) - { - configuration.HttpClientTimeout = timeSpan; - return configuration; - } + public ISet PrivateAttributeNames => _privateAttributeNames; /// - /// Sets the timeout when reading data from the EventSource API. The default value is 5 minutes. + /// The timeout when reading data from the EventSource API. The default value is 5 minutes. /// - /// the configuration - /// the read timeout - /// the same Configuration instance - public static Configuration WithReadTimeout(this Configuration configuration, TimeSpan timeSpan) - { - configuration.ReadTimeout = timeSpan; - return configuration; - } + public TimeSpan ReadTimeout => _readTimeout; /// - /// Sets the reconnect base time for the streaming connection. The streaming connection + /// The reconnect base time for the streaming connection.The streaming connection /// uses an exponential backoff algorithm (with jitter) for reconnects, but will start the /// backoff with a value near the value specified here. The default value is 1 second. /// - /// the configuration - /// the reconnect time base value - /// the same Configuration instance - public static Configuration WithReconnectTime(this Configuration configuration, TimeSpan timeSpan) - { - configuration.ReconnectTime = timeSpan; - return configuration; - } - - /// - /// Sets the object to be used for sending HTTP requests. This is exposed for testing purposes. - /// - /// the configuration - /// the HttpClientHandler to use - /// the same Configuration instance - public static Configuration WithHttpClientHandler(this Configuration configuration, HttpClientHandler httpClientHandler) - { - configuration.HttpClientHandler = httpClientHandler; - return configuration; - } + public TimeSpan ReconnectTime => _reconnectTime; /// - /// Sets whether or not the streaming API should be used to receive flag updates. This - /// is true by default. Streaming should only be disabled on the advice of LaunchDarkly - /// support. + /// Alternate name for . /// - /// the configuration - /// true if the streaming API should be used - /// the same Configuration instance - public static Configuration WithIsStreamingEnabled(this Configuration configuration, bool enableStream) - { - configuration.IsStreamingEnabled = enableStream; - return configuration; - } + public string SdkKey => MobileKey; /// - /// Sets whether or not user attributes (other than the key) should be private (not sent to - /// the LaunchDarkly server). If this is true, all of the user attributes will be private, - /// not just the attributes specified with the AndPrivate... methods on the - /// object. By default, this is false. + /// The base URL of the LaunchDarkly streaming server. /// - /// the configuration - /// true if all attributes should be private - /// the same Configuration instance - public static Configuration WithAllAttributesPrivate(this Configuration configuration, bool allAttributesPrivate) - { - configuration.AllAttributesPrivate = allAttributesPrivate; - return configuration; - } + public Uri StreamUri => _streamUri; - /// - /// Marks an attribute name as private. Any users sent to LaunchDarkly with this - /// configuration active will have attributes with this name removed, even if you did - /// not use the AndPrivate... methods on the object. You may - /// call this method repeatedly to mark multiple attributes as private. - /// - /// the configuration - /// the attribute name - /// the same Configuration instance - public static Configuration WithPrivateAttributeName(this Configuration configuration, string attributeName) - { - if (configuration.PrivateAttributeNames == null) - { - configuration.PrivateAttributeNames = new HashSet(); - } - configuration.PrivateAttributeNames.Add(attributeName); - return configuration; - } + /// + public bool UseReport => _useReport; - /// Configuration - /// Sets the number of user keys that the event processor can remember at any one time, so that + /// + /// The number of user keys that the event processor can remember at any one time, so that /// duplicate user details will not be sent in analytics events. /// - /// the configuration - /// the user key cache capacity - /// the same Configuration instance - public static Configuration WithUserKeysCapacity(this Configuration configuration, int capacity) - { - configuration.UserKeysCapacity = capacity; - return configuration; - } + public int UserKeysCapacity => _userKeysCapacity; /// - /// Sets the interval at which the event processor will reset its set of known user keys. The + /// The interval at which the event processor will reset its set of known user keys. The /// default value is five minutes. - /// - /// the configuration - /// the flush interval - /// the same Configuration instance - public static Configuration WithUserKeysFlushInterval(this Configuration configuration, TimeSpan flushInterval) - { - configuration.UserKeysFlushInterval = flushInterval; - return configuration; - } - - /// - /// Sets whether to include full user details in every analytics event. The default is false (events will - /// only include the user key, except for one "index" event that provides the full details for the user). - /// - /// the configuration - /// true or false - /// the same Configuration instance - public static Configuration WithInlineUsersInEvents(this Configuration configuration, bool inlineUsers) - { - configuration.InlineUsersInEvents = inlineUsers; - return configuration; - } - - /// - /// Sets the IConnectionManager instance, used internally for stubbing mock instances. /// - /// Configuration. - /// Connection manager. - /// the same Configuration instance - public static Configuration WithConnectionManager(this Configuration configuration, IConnectionManager connectionManager) - { - configuration.ConnectionManager = connectionManager; - return configuration; - } - - /// - /// Sets the IEventProcessor instance, used internally for stubbing mock instances. - /// - /// Configuration. - /// Event processor. - /// the same Configuration instance - public static Configuration WithEventProcessor(this Configuration configuration, IEventProcessor eventProcessor) - { - configuration.EventProcessor = eventProcessor; - return configuration; - } + public TimeSpan UserKeysFlushInterval => _userKeysFlushInterval; /// - /// Determines whether to use the Report method for networking requests + /// Default value for . /// - /// Configuration. - /// If set to true use report. - /// the same Configuration instance - public static Configuration WithUseReport(this Configuration configuration, bool useReport) - { - configuration.UseReport = useReport; - return configuration; - } - + public static TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + /// - /// Set to true if LaunchDarkly should provide additional information about how flag values were - /// calculated. The additional information will then be available through the client's "detail" - /// methods such as . Since this - /// increases the size of network requests, such information is not sent unless you set this option - /// to true. - /// - /// Configuration. - /// True if evaluation reasons are desired. - /// the same Configuration instance - public static Configuration WithEvaluationReasons(this Configuration configuration, bool evaluationReasons) - { - configuration.EvaluationReasons = evaluationReasons; - return configuration; - } - - /// - /// Sets whether to enable background polling. + /// Minimum value for . /// - /// Configuration. - /// If set to true enable background updating. - /// the same Configuration instance - public static Configuration WithEnableBackgroundUpdating(this Configuration configuration, bool enableBackgroundUpdating) - { - configuration.EnableBackgroundUpdating = enableBackgroundUpdating; - return configuration; - } + public static TimeSpan MinimumPollingInterval = TimeSpan.FromMinutes(5); - /// - /// Sets the interval for background polling. - /// - /// Configuration. - /// Background polling internal. - /// the same Configuration instance - public static Configuration WithBackgroundPollingInterval(this Configuration configuration, TimeSpan backgroundPollingInternal) - { - if (backgroundPollingInternal.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) - { - Log.WarnFormat("BackgroundPollingInterval cannot be less than the default of {0}.", Configuration.MinimumBackgroundPollingInterval); - backgroundPollingInternal = Configuration.MinimumBackgroundPollingInterval; - } - configuration.BackgroundPollingInterval = backgroundPollingInternal; - return configuration; - } + internal static readonly Uri DefaultUri = new Uri("https://app.launchdarkly.com"); + internal static readonly Uri DefaultStreamUri = new Uri("https://clientstream.launchdarkly.com"); + internal static readonly Uri DefaultEventsUri = new Uri("https://mobile.launchdarkly.com"); + internal static readonly int DefaultEventCapacity = 100; + internal static readonly TimeSpan DefaultEventFlushInterval = TimeSpan.FromSeconds(5); + internal static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); + internal static readonly TimeSpan DefaultReconnectTime = TimeSpan.FromSeconds(1); + internal static readonly TimeSpan DefaultHttpClientTimeout = TimeSpan.FromSeconds(10); + internal static readonly int DefaultUserKeysCapacity = 1000; + internal static readonly TimeSpan DefaultUserKeysFlushInterval = TimeSpan.FromMinutes(5); + internal static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(60); + internal static readonly TimeSpan MinimumBackgroundPollingInterval = TimeSpan.FromMinutes(15); + internal static readonly TimeSpan DefaultConnectionTimeout = TimeSpan.FromSeconds(10); /// - /// Sets whether the SDK should save flag values for each user in persistent storage, so they will be - /// immediately available the next time the SDK is started for the same user. The default is . + /// Creates a configuration with all parameters set to the default. Use extension methods + /// to set additional parameters. /// - /// the configuration - /// true or false - /// the same Configuration instance - /// - public static Configuration WithPersistFlagValues(this Configuration configuration, bool persistFlagValues) - { - configuration.PersistFlagValues = persistFlagValues; - return configuration; - } - - // The following properties can only be set internally. They are used for providing stub implementations in unit tests. - - internal static Configuration WithDeviceInfo(this Configuration configuration, IDeviceInfo deviceInfo) + /// the SDK key for your LaunchDarkly environment + /// a Configuration instance + public static Configuration Default(string mobileKey) { - configuration.DeviceInfo = deviceInfo; - return configuration; + return Builder(mobileKey).Build(); } - internal static Configuration WithFlagChangedEventManager(this Configuration configuration, IFlagChangedEventManager flagChangedEventManager) - { - configuration.FlagChangedEventManager = flagChangedEventManager; - return configuration; - } + /// /// Creates a for constructing a configuration object using a fluent syntax. /// /// /// This is the only method for building a Configuration if you are setting properties /// besides the MobileKey. The ConfigurationBuilder has methods for setting any number of /// properties, after which you call to get the resulting /// Configuration instance. /// /// /// /// var config = Configuration.Builder("my-sdk-key") /// .EventQueueFrequency(TimeSpan.FromSeconds(90)) /// .StartWaitTime(TimeSpan.FromSeconds(5)) /// .Build(); /// /// /// the SDK key for your LaunchDarkly environment + /// a builder object public static IConfigurationBuilder Builder(string mobileKey) { + if (String.IsNullOrEmpty(mobileKey)) + { + throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required"); + } + return new ConfigurationBuilder(mobileKey); } - internal static Configuration WithFlagCacheManager(this Configuration configuration, IFlagCacheManager flagCacheManager) - { - configuration.FlagCacheManager = flagCacheManager; - return configuration; - } + internal static ConfigurationBuilder BuilderInternal(string mobileKey) { + return new ConfigurationBuilder(mobileKey); } - internal static Configuration WithPersistentStorage(this Configuration configuration, IPersistentStorage persistentStorage) + public static IConfigurationBuilder Builder(Configuration fromConfiguration) { - configuration.PersistentStorage = persistentStorage; - return configuration; + return new ConfigurationBuilder(fromConfiguration); } - internal static Configuration WithUpdateProcessorFactory(this Configuration configuration, Func factory) + internal Configuration(ConfigurationBuilder builder) { - configuration.UpdateProcessorFactory = factory; - return configuration; + _allAttributesPrivate = builder._allAttributesPrivate; _backgroundPollingInterval = builder._backgroundPollingInterval; _baseUri = builder._baseUri; _connectionTimeout = builder._connectionTimeout; _enableBackgroundUpdating = builder._enableBackgroundUpdating; _evaluationReasons = builder._evaluationReasons; _eventFlushInterval = builder._eventFlushInterval; _eventCapacity = builder._eventCapacity; _eventSamplingInterval = builder._eventSamplingInterval; _eventsUri = builder._eventsUri; _httpClientHandler = builder._httpClientHandler; _httpClientTimeout = builder._httpClientTimeout; _inlineUsersInEvents = builder._inlineUsersInEvents; _isStreamingEnabled = builder._isStreamingEnabled; _mobileKey = builder._mobileKey; _offline = builder._offline; _persistFlagValues = builder._persistFlagValues; _pollingInterval = builder._pollingInterval; _privateAttributeNames = builder._privateAttributeNames is null ? null : builder._privateAttributeNames.ToImmutableHashSet(); _readTimeout = builder._readTimeout; _reconnectTime = builder._reconnectTime; _streamUri = builder._streamUri; _useReport = builder._useReport; _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; + + _connectionManager = builder._connectionManager; + _deviceInfo = builder._deviceInfo; + _eventProcessor = builder._eventProcessor; + _flagCacheManager = builder._flagCacheManager; + _flagChangedEventManager = builder._flagChangedEventManager; + _persistentStorage = builder._persistentStorage; + _updateProcessorFactory = builder._updateProcessorFactory; } } } diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs new file mode 100644 index 00000000..f07b05aa --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Net.Http; +using Common.Logging; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin +{ + /// + /// A mutable object that uses the Builder pattern to specify properties for a object. + /// + /// + /// Obtain an instance of this class by calling . + /// + /// All of the builder methods for setting a configuration property return a reference to the same builder, so they can be + /// chained together. + /// + /// + /// + /// var config = Configuration.Builder("my-mobile-key").AllAttributesPrivate(true).EventCapacity(1000).Build(); + /// + /// + public interface IConfigurationBuilder + { + /// /// Creates a based on the properties that have been set on the builder. /// Modifying the builder after this point does not affect the returned Configuration. /// /// the configured Configuration object Configuration Build(); /// + /// Sets whether or not user attributes (other than the key) should be private (not sent to + /// the LaunchDarkly server). + /// + /// + /// If this is true, all of the user attributes will be private, not just the attributes specified with + /// on the object. + /// By default, this is false. + /// + /// true if all attributes should be private + /// the same builder + IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate); /// + /// Sets the interval for background polling. + /// + /// the background polling interval + /// the same builder + IConfigurationBuilder BackgroundPollingInterval(TimeSpan backgroundPollingInterval); /// + /// Sets the base URI of the LaunchDarkly server. + /// + /// the base URI + /// the same builder + IConfigurationBuilder BaseUri(Uri baseUri); /// /// Set to true if LaunchDarkly should provide additional information about how flag values were + /// calculated. + /// + /// + /// The additional information will then be available through the client's "detail" + /// methods such as . Since this + /// increases the size of network requests, such information is not sent unless you set this option + /// to true. + /// + /// True if evaluation reasons are desired. + /// the same builder + IConfigurationBuilder EvaluationReasons(bool evaluationReasons); + /// + /// Sets the capacity of the events buffer. + /// + /// + /// The client buffers up to this many events in memory before flushing. If the capacity is exceeded + /// before the buffer is flushed, events will be discarded. Increasing the capacity means that events + /// are less likely to be discarded, at the cost of consuming more memory. + /// + /// the capacity of the events buffer + /// the same builder + IConfigurationBuilder EventCapacity(int eventCapacity); /// + /// Sets the time between flushes of the event buffer. + /// + /// + /// Decreasing the flush interval means that the event buffer is less likely to reach capacity. The + /// default value is 5 seconds. + /// + /// the flush interval + /// the same builder + IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval); + /// + /// Enables event sampling if non-zero. + /// + /// + /// When set to the default of zero, all analytics events are sent back to LaunchDarkly. When greater + /// than zero, there is a 1 in EventSamplingInterval chance that events will be sent (example: + /// if the interval is 20, on average 5% of events will be sent). + /// + /// the sampling interval + /// the same builder + IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval); /// + /// Sets the base URL of the LaunchDarkly analytics event server. + /// + /// the events URI + /// the same builder + IConfigurationBuilder EventsUri(Uri eventsUri); + + /// + /// Sets the object to be used for sending HTTP requests. This is exposed for testing purposes. + /// + /// the HttpClientHandler to use + /// the same builder IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler); /// + /// Sets the connection timeout. The default value is 10 seconds. + /// + /// the connection timeout + /// the same builder + IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout); + + /// + /// Sets whether to include full user details in every analytics event. + /// + /// + /// The default is false: events will only include the user key, except for one "index" event that + /// provides the full details for the user. + /// + /// true or false + /// the same builder IConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents); + + /// + /// Sets whether or not the streaming API should be used to receive flag updates. + /// + /// + /// This is true by default. Streaming should only be disabled on the advice of LaunchDarkly support. + /// + /// true if the streaming API should be used + /// the same builder IConfigurationBuilder IsStreamingEnabled(bool isStreamingEnabled); + /// + /// + /// + /// + /// the same builder IConfigurationBuilder MobileKey(string mobileKey); /// + /// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made. + /// + /// true if the client should remain offline + /// the same builder + IConfigurationBuilder Offline(bool offline); + + /// + /// Sets whether the SDK should save flag values for each user in persistent storage, so they will be + /// immediately available the next time the SDK is started for the same user. The default is . + /// + /// true to save flag values + /// the same Configuration instance + /// the same builder + IConfigurationBuilder PersistFlagValues(bool persistFlagValues); /// + /// Sets the polling interval (when streaming is disabled). + /// + /// + /// Values less than the default of 30 seconds will be changed to the default. + /// + /// the rule update polling interval + /// the same builder + IConfigurationBuilder PollingInterval(TimeSpan pollingInterval); + + /// + /// Marks an attribute name as private. + /// + /// + /// Any users sent to LaunchDarkly with this configuration active will have attributes with this name + /// removed, even if you did not use the AndPrivate... methods on the object. + /// You may call this method repeatedly to mark multiple attributes as private. + /// + /// the attribute name + /// the same builder IConfigurationBuilder PrivateAttribute(string privateAtributeName); /// + /// Sets the timeout when reading data from the streaming connection. + /// + /// + /// The default value is 5 minutes. + /// + /// the read timeout + /// the same builder + IConfigurationBuilder ReadTimeout(TimeSpan readTimeout); /// + /// Sets the reconnect base time for the streaming connection. + /// + /// + /// The streaming connection uses an exponential backoff algorithm (with jitter) for reconnects, but + /// will start the backoff with a value near the value specified here. The default value is 1 second. + /// + /// the reconnect time base value + /// the same builder + IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime); /// + /// Sets the base URI of the LaunchDarkly streaming server. + /// + /// the stream URI + /// the same builder + IConfigurationBuilder StreamUri(Uri streamUri); + + /// + /// Determines whether to use the REPORT method for networking requests. + /// + /// whether to use REPORT mode + /// the same builder + IConfigurationBuilder UseReport(bool useReport); + + /// + /// Sets the number of user keys that the event processor can remember at any one time. + /// + /// + /// The event processor keeps track of recently seen user keys so that duplicate user details will not + /// be sent in analytics events. + /// + /// the user key cache capacity + /// the same builder IConfigurationBuilder UserKeysCapacity(int userKeysCapacity); /// + /// Sets the interval at which the event processor will clear its cache of known user keys. + /// + /// + /// The default value is five minutes. + /// + /// the flush interval + /// the same builder + IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterval); + } + + internal class ConfigurationBuilder : IConfigurationBuilder + { + private static readonly ILog Log = LogManager.GetLogger(typeof(ConfigurationBuilder)); + + internal bool _allAttributesPrivate = false; + internal TimeSpan _backgroundPollingInterval; + internal Uri _baseUri = Configuration.DefaultUri; + internal TimeSpan _connectionTimeout; + internal bool _enableBackgroundUpdating; + internal bool _evaluationReasons = false; + internal int _eventCapacity = Configuration.DefaultEventCapacity; + internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; + internal int _eventSamplingInterval = 0; + internal Uri _eventsUri = Configuration.DefaultEventsUri; + internal HttpClientHandler _httpClientHandler = new HttpClientHandler(); + internal TimeSpan _httpClientTimeout = Configuration.DefaultHttpClientTimeout; + internal bool _inlineUsersInEvents = false; + internal bool _isStreamingEnabled = true; + internal string _mobileKey; + internal bool _offline = false; + internal bool _persistFlagValues = true; + internal TimeSpan _pollingInterval = Configuration.DefaultPollingInterval; + internal HashSet _privateAttributeNames = null; + internal TimeSpan _readTimeout = Configuration.DefaultReadTimeout; + internal TimeSpan _reconnectTime = Configuration.DefaultReconnectTime; + internal Uri _streamUri = Configuration.DefaultStreamUri; + internal bool _useReport; + internal int _userKeysCapacity = Configuration.DefaultUserKeysCapacity; + internal TimeSpan _userKeysFlushInterval = Configuration.DefaultUserKeysFlushInterval; + + // Internal properties only settable for testing + internal IConnectionManager _connectionManager; + internal IDeviceInfo _deviceInfo; + internal IEventProcessor _eventProcessor; + internal IFlagCacheManager _flagCacheManager; + internal IFlagChangedEventManager _flagChangedEventManager; + internal IPersistentStorage _persistentStorage; + internal Func _updateProcessorFactory; + + internal ConfigurationBuilder(string mobileKey) { _mobileKey = mobileKey; + } internal ConfigurationBuilder(Configuration copyFrom) { _allAttributesPrivate = copyFrom.AllAttributesPrivate; _backgroundPollingInterval = copyFrom.BackgroundPollingInterval; _baseUri = copyFrom.BaseUri; _connectionTimeout = copyFrom.ConnectionTimeout; _enableBackgroundUpdating = copyFrom.EnableBackgroundUpdating; _evaluationReasons = copyFrom.EvaluationReasons; _eventCapacity = copyFrom.EventCapacity; _eventFlushInterval = copyFrom.EventFlushInterval; _eventSamplingInterval = copyFrom.EventSamplingInterval; _eventsUri = copyFrom.EventsUri; _httpClientHandler = copyFrom.HttpClientHandler; _httpClientTimeout = copyFrom.HttpClientTimeout; _inlineUsersInEvents = copyFrom.InlineUsersInEvents; _isStreamingEnabled = copyFrom.IsStreamingEnabled; _mobileKey = copyFrom.MobileKey; _offline = copyFrom.Offline; _persistFlagValues = copyFrom.PersistFlagValues; _pollingInterval = copyFrom.PollingInterval; _privateAttributeNames = copyFrom.PrivateAttributeNames is null ? null : new HashSet(copyFrom.PrivateAttributeNames); _readTimeout = copyFrom.ReadTimeout; _reconnectTime = copyFrom.ReconnectTime; _streamUri = copyFrom.StreamUri; _useReport = copyFrom.UseReport; _userKeysCapacity = copyFrom.UserKeysCapacity; _userKeysFlushInterval = copyFrom.UserKeysFlushInterval; } public Configuration Build() { return new Configuration(this); } + + public IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate) + { + _allAttributesPrivate = allAttributesPrivate; + return this; + } + + public IConfigurationBuilder BackgroundPollingInterval(TimeSpan backgroundPollingInterval) + { + if (backgroundPollingInterval.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) + { + Log.WarnFormat("BackgroundPollingInterval cannot be less than {0}", Configuration.MinimumBackgroundPollingInterval); + _backgroundPollingInterval = Configuration.MinimumBackgroundPollingInterval; + } + else + { + _backgroundPollingInterval = backgroundPollingInterval; + } + return this; + } + + public IConfigurationBuilder BaseUri(Uri baseUri) + { + _baseUri = baseUri; + return this; + } + + public IConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout) + { + _connectionTimeout = connectionTimeout; + return this; + } + + public IConfigurationBuilder EnableBackgroundUpdating(bool enableBackgroundUpdating) + { + _enableBackgroundUpdating = enableBackgroundUpdating; + return this; + } + + public IConfigurationBuilder EvaluationReasons(bool evaluationReasons) + { + _evaluationReasons = evaluationReasons; + return this; + } + + public IConfigurationBuilder EventCapacity(int eventCapacity) + { + _eventCapacity = eventCapacity; + return this; + } public IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval) + { + _eventFlushInterval = eventflushInterval; + return this; + } + + public IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval) + { + _eventSamplingInterval = eventSamplingInterval; + return this; + } public IConfigurationBuilder EventsUri(Uri eventsUri) + { + _eventsUri = eventsUri; + return this; + } + + public IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler) + { + _httpClientHandler = httpClientHandler; + return this; + } public IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout) + { + _httpClientTimeout = httpClientTimeout; + return this; + } + + public IConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents) + { + _inlineUsersInEvents = inlineUsersInEvents; + return this; + } + + public IConfigurationBuilder IsStreamingEnabled(bool isStreamingEnabled) + { + _isStreamingEnabled = isStreamingEnabled; + return this; + } + + public IConfigurationBuilder MobileKey(string mobileKey) + { + _mobileKey = mobileKey; + return this; + } public IConfigurationBuilder Offline(bool offline) { _offline = offline; return this; } public IConfigurationBuilder PersistFlagValues(bool persistFlagValues) + { + _persistFlagValues = persistFlagValues; + return this; + } public IConfigurationBuilder PollingInterval(TimeSpan pollingInterval) + { + if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) + { + Log.WarnFormat("PollingInterval cannot be less than {0}", Configuration.MinimumPollingInterval); + _pollingInterval = Configuration.MinimumPollingInterval; + } + else + { + _pollingInterval = pollingInterval; + } + return this; + } + + public IConfigurationBuilder PrivateAttribute(string privateAtributeName) + { + if (_privateAttributeNames is null) + { + _privateAttributeNames = new HashSet(); + } + _privateAttributeNames.Add(privateAtributeName); + return this; + } public IConfigurationBuilder ReadTimeout(TimeSpan readTimeout) + { + _readTimeout = readTimeout; + return this; + } public IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime) + { + _reconnectTime = reconnectTime; + return this; + } public IConfigurationBuilder StreamUri(Uri streamUri) + { + _streamUri = streamUri; + return this; + } + + public IConfigurationBuilder UseReport(bool useReport) + { + _useReport = useReport; + return this; + } + + public IConfigurationBuilder UserKeysCapacity(int userKeysCapacity) + { + _userKeysCapacity = userKeysCapacity; + return this; + } public IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterval) + { + _userKeysFlushInterval = userKeysFlushInterval; + return this; + } + + // The following properties are internal and settable only for testing. They are not part + // of the IConfigurationBuilder interface, so you must call the internal method + // Configuration.BuilderInternal() which exposes the internal ConfigurationBuilder, + // and then call these methods before you have called any of the public methods (since + // only these methods return ConfigurationBuilder rather than IConfigurationBuilder). + + internal ConfigurationBuilder ConnectionManager(IConnectionManager connectionManager) + { + _connectionManager = connectionManager; + return this; + } + + internal ConfigurationBuilder DeviceInfo(IDeviceInfo deviceInfo) + { + _deviceInfo = deviceInfo; + return this; + } + + internal ConfigurationBuilder EventProcessor(IEventProcessor eventProcessor) + { + _eventProcessor = eventProcessor; + return this; + } + + internal ConfigurationBuilder FlagCacheManager(IFlagCacheManager flagCacheManager) + { + _flagCacheManager = flagCacheManager; + return this; + } + + internal ConfigurationBuilder FlagChangedEventManager(IFlagChangedEventManager flagChangedEventManager) + { + _flagChangedEventManager = flagChangedEventManager; + return this; + } + + internal ConfigurationBuilder PersistentStorage(IPersistentStorage persistentStorage) + { + _persistentStorage = persistentStorage; + return this; + } + + internal ConfigurationBuilder UpdateProcessorFactory(Func updateProcessorFactory) + { + _updateProcessorFactory = updateProcessorFactory; + return this; + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index 924f59b2..7a8d6e59 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -15,9 +15,9 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura IFlagChangedEventManager flagChangedEventManager, User user) { - if (configuration.FlagCacheManager != null) + if (configuration._flagCacheManager != null) { - return configuration.FlagCacheManager; + return configuration._flagCacheManager; } else { @@ -29,10 +29,12 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura internal static IConnectionManager CreateConnectionManager(Configuration configuration) { - return configuration.ConnectionManager ?? new MobileConnectionManager(); + return configuration._connectionManager ?? new MobileConnectionManager(); } - internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, User user, IFlagCacheManager flagCacheManager, TimeSpan? overridePollingInterval) + internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, User user, + IFlagCacheManager flagCacheManager, TimeSpan? overridePollingInterval, + bool disableStreaming) { if (configuration.Offline) { @@ -40,12 +42,12 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return new NullUpdateProcessor(); } - if (configuration.UpdateProcessorFactory != null) + if (configuration._updateProcessorFactory != null) { - return configuration.UpdateProcessorFactory(configuration, flagCacheManager, user); + return configuration._updateProcessorFactory(configuration, flagCacheManager, user); } - if (configuration.IsStreamingEnabled) + if (configuration.IsStreamingEnabled && !disableStreaming) { return new MobileStreamingProcessor(configuration, flagCacheManager, user, null); } @@ -61,9 +63,9 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi internal static IEventProcessor CreateEventProcessor(Configuration configuration) { - if (configuration.EventProcessor != null) + if (configuration._eventProcessor != null) { - return configuration.EventProcessor; + return configuration._eventProcessor; } if (configuration.Offline) { @@ -76,17 +78,17 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration internal static IPersistentStorage CreatePersistentStorage(Configuration configuration) { - return configuration.PersistentStorage ?? new DefaultPersistentStorage(); + return configuration._persistentStorage ?? new DefaultPersistentStorage(); } internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) { - return configuration.DeviceInfo ?? new DefaultDeviceInfo(); + return configuration._deviceInfo ?? new DefaultDeviceInfo(); } internal static IFlagChangedEventManager CreateFlagChangedEventManager(Configuration configuration) { - return configuration.FlagChangedEventManager ?? new FlagChangedEventManager(); + return configuration._flagChangedEventManager ?? new FlagChangedEventManager(); } } } diff --git a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs index 8fb923a8..572d808f 100644 --- a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs +++ b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs @@ -8,38 +8,37 @@ public interface IMobileConfiguration : IBaseConfiguration /// /// The interval between feature flag updates when the app is running in the background. /// - /// The background polling interval. TimeSpan BackgroundPollingInterval { get; } /// /// When streaming mode is disabled, this is the interval between feature flag updates. /// - /// The feature flag polling interval. TimeSpan PollingInterval { get; } /// - /// Gets the connection timeout to the LaunchDarkly server + /// The connection timeout to the LaunchDarkly server. /// - /// The connection timeout. TimeSpan ConnectionTimeout { get; } /// /// Whether to enable feature flag updates when the app is running in the background. /// - /// true if disable background updating; otherwise, false. - bool EnableBackgroundUpdating { get; } + bool EnableBackgroundUpdating { get; } + + /// + /// The time between flushes of the event buffer. Decreasing the flush interval means + /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. + /// + TimeSpan EventFlushInterval { get; } /// - /// Whether to enable real-time streaming flag updates. When false, - /// feature flags are updated via polling. + /// Whether to enable real-time streaming flag updates. When false, feature flags are updated via polling. /// - /// true if streaming; otherwise, false. bool IsStreamingEnabled { get; } /// /// Whether to use the REPORT HTTP verb when fetching flags from LaunchDarkly. /// - /// true if use report; otherwise, false. bool UseReport { get; } /// @@ -49,7 +48,6 @@ public interface IMobileConfiguration : IBaseConfiguration /// increases the size of network requests, such information is not sent unless you set this option /// to true. /// - /// true if evaluation reasons are desired. bool EvaluationReasons { get; } /// @@ -63,7 +61,6 @@ public interface IMobileConfiguration : IBaseConfiguration /// API, which stores file data under the current account's home directory at /// ~/.local/share/IsolateStorage/. /// - /// true if flag values should be stored locally (the default). bool PersistFlagValues { get; } } } diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index ce250990..b2cf6f2c 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -3,7 +3,9 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; + + netstandard1.6;netstandard2.0;Xamarin.iOS10; 1.0.0-beta18 Library LaunchDarkly.XamarinSdk @@ -74,5 +76,10 @@ + + + + + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index dfb71833..e09962ff 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -19,8 +19,29 @@ public sealed class LdClient : ILdMobileClient { private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); - static volatile LdClient instance; - static readonly object createInstanceLock = new object(); + static volatile LdClient _instance; + static volatile User _user; + + static readonly object _createInstanceLock = new object(); + static readonly EventFactory _eventFactoryDefault = EventFactory.Default; + static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; + + readonly object _myLockObjForConnectionChange = new object(); + readonly object _myLockObjForUserUpdate = new object(); + + readonly Configuration _config; + readonly SemaphoreSlim _connectionLock; + + readonly IDeviceInfo deviceInfo; + readonly IConnectionManager connectionManager; + readonly IEventProcessor eventProcessor; + readonly IFlagCacheManager flagCacheManager; + internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing + readonly IPersistentStorage persister; + + // These LdClient fields are not readonly because they change according to online status + volatile IMobileUpdateProcessor updateProcessor; + volatile bool _disableStreaming; /// /// The singleton instance used by your application throughout its lifetime. Once this exists, you cannot @@ -30,38 +51,19 @@ public sealed class LdClient : ILdMobileClient /// to set this LdClient instance. /// /// The LdClient instance. - public static LdClient Instance => instance; + public static LdClient Instance => _instance; /// /// The Configuration instance used to setup the LdClient. /// /// The Configuration instance. - public Configuration Config { get; private set; } + public Configuration Config => _config; /// /// The User for the LdClient operations. /// /// The User. - public User User { get; private set; } - - readonly object myLockObjForConnectionChange = new object(); - readonly object myLockObjForUserUpdate = new object(); - - readonly IFlagCacheManager flagCacheManager; - readonly IConnectionManager connectionManager; - IMobileUpdateProcessor updateProcessor; // not readonly - may need to be recreated - readonly IEventProcessor eventProcessor; - readonly IPersistentStorage persister; - readonly IDeviceInfo deviceInfo; - readonly EventFactory eventFactoryDefault = EventFactory.Default; - readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; - internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing - - readonly SemaphoreSlim connectionLock; - - // private constructor prevents initialization of this class - // without using WithConfigAnduser(config, user) - LdClient() { } + public User User => _user; LdClient(Configuration configuration, User user) { @@ -74,29 +76,29 @@ public sealed class LdClient : ILdMobileClient throw new ArgumentNullException(nameof(user)); } - Config = configuration; + _config = configuration; - connectionLock = new SemaphoreSlim(1, 1); + _connectionLock = new SemaphoreSlim(1, 1); persister = Factory.CreatePersistentStorage(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagChangedEventManager = Factory.CreateFlagChangedEventManager(configuration); - User = DecorateUser(user); + _user = DecorateUser(user); flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagChangedEventManager, User); connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, null); + updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, null, false); eventProcessor = Factory.CreateEventProcessor(configuration); - eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(User)); + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); SetupConnectionManager(); BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; } /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// Creates and returns a new LdClient singleton instance, then starts the workflow for /// fetching feature flags. /// /// This constructor will wait and block on the current thread until initialization and the @@ -122,7 +124,7 @@ public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) } /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// Creates and returns a new LdClient singleton instance, then starts the workflow for /// fetching feature flags. This constructor should be used if you do not want to wait /// for the client to finish initializing and receive the first response /// from the LaunchDarkly service. @@ -143,7 +145,7 @@ public static async Task InitAsync(string mobileKey, User user) } /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// Creates and returns a new LdClient singleton instance, then starts the workflow for /// fetching Feature Flags. /// /// This constructor will wait and block on the current thread until initialization and the @@ -183,7 +185,7 @@ public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTim } /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// Creates and returns a new LdClient singleton instance, then starts the workflow for /// fetching Feature Flags. This constructor should be used if you do not want to wait /// for the IUpdateProcessor instance to finish initializing and receive the first response /// from the LaunchDarkly service. @@ -213,15 +215,15 @@ public static Task InitAsync(Configuration config, User user) static LdClient CreateInstance(Configuration configuration, User user) { - lock (createInstanceLock) + lock (_createInstanceLock) { - if (Instance != null) + if (_instance != null) { throw new Exception("LdClient instance already exists."); } var c = new LdClient(configuration, user); - Interlocked.CompareExchange(ref instance, c, null); + _instance = c; Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); return c; } @@ -261,7 +263,7 @@ public bool Online public async Task SetOnlineAsync(bool value) { - await connectionLock.WaitAsync(); + await _connectionLock.WaitAsync(); online = value; try { @@ -276,7 +278,7 @@ public async Task SetOnlineAsync(bool value) } finally { - connectionLock.Release(); + _connectionLock.Release(); } return; @@ -290,61 +292,61 @@ void MobileConnectionManager_ConnectionChanged(bool isOnline) /// public bool BoolVariation(string key, bool defaultValue = false) { - return VariationInternal(key, defaultValue, ValueTypes.Bool, eventFactoryDefault).Value; + return VariationInternal(key, defaultValue, ValueTypes.Bool, _eventFactoryDefault).Value; } /// public EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false) { - return VariationInternal(key, defaultValue, ValueTypes.Bool, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Bool, _eventFactoryWithReasons); } /// public string StringVariation(string key, string defaultValue) { - return VariationInternal(key, defaultValue, ValueTypes.String, eventFactoryDefault).Value; + return VariationInternal(key, defaultValue, ValueTypes.String, _eventFactoryDefault).Value; } /// public EvaluationDetail StringVariationDetail(string key, string defaultValue) { - return VariationInternal(key, defaultValue, ValueTypes.String, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.String, _eventFactoryWithReasons); } /// public float FloatVariation(string key, float defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueTypes.Float, eventFactoryDefault).Value; + return VariationInternal(key, defaultValue, ValueTypes.Float, _eventFactoryDefault).Value; } /// public EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueTypes.Float, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Float, _eventFactoryWithReasons); } /// public int IntVariation(string key, int defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueTypes.Int, eventFactoryDefault).Value; + return VariationInternal(key, defaultValue, ValueTypes.Int, _eventFactoryDefault).Value; } /// public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0) { - return VariationInternal(key, defaultValue, ValueTypes.Int, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Int, _eventFactoryWithReasons); } /// public JToken JsonVariation(string key, JToken defaultValue) { - return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryDefault).Value; + return VariationInternal(key, defaultValue, ValueTypes.Json, _eventFactoryDefault).Value; } /// public EvaluationDetail JsonVariationDetail(string key, JToken defaultValue) { - return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryWithReasons); + return VariationInternal(key, defaultValue, ValueTypes.Json, _eventFactoryWithReasons); } EvaluationDetail VariationInternal(string featureKey, T defaultValue, ValueType desiredType, EventFactory eventFactory) @@ -419,7 +421,7 @@ public IDictionary AllFlags() /// public void Track(string eventName, JToken data) { - eventProcessor.SendEvent(eventFactoryDefault.NewCustomEvent(eventName, User, data)); + eventProcessor.SendEvent(_eventFactoryDefault.NewCustomEvent(eventName, User, data)); } /// @@ -462,18 +464,18 @@ public async Task IdentifyAsync(User user) User newUser = DecorateUser(user); - await connectionLock.WaitAsync(); + await _connectionLock.WaitAsync(); try { - User = newUser; + _user = newUser; await RestartUpdateProcessorAsync(Config.PollingInterval); } finally { - connectionLock.Release(); + _connectionLock.Release(); } - eventProcessor.SendEvent(eventFactoryDefault.NewIdentifyEvent(newUser)); + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(newUser)); } async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) @@ -485,7 +487,7 @@ async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) void ClearAndSetUpdateProcessor(TimeSpan pollingInterval) { ClearUpdateProcessor(); - updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager, pollingInterval); + updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager, pollingInterval, _disableStreaming); } void ClearUpdateProcessor() @@ -551,7 +553,7 @@ void Dispose(bool disposing) internal void DetachInstance() // exposed for testing { - Interlocked.CompareExchange(ref instance, null, this); + Interlocked.CompareExchange(ref _instance, null, this); } /// @@ -586,7 +588,7 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh if (args.IsInBackground) { ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; + _disableStreaming = true; if (Config.EnableBackgroundUpdating) { await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); @@ -607,7 +609,7 @@ void ResetProcessorForForeground() { persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); ClearUpdateProcessor(); - Config.IsStreamingEnabled = true; + _disableStreaming = false; } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs index 641a3c22..b9f76e20 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs @@ -9,25 +9,27 @@ public class ConfigurationTest : BaseTest [Fact] public void CanOverrideConfiguration() { - var config = Configuration.Default("AnyOtherSdkKey") - .WithBaseUri("https://app.AnyOtherEndpoint.com") - .WithEventQueueCapacity(99) - .WithPollingInterval(TimeSpan.FromMinutes(45)); + var config = Configuration.Builder("AnyOtherSdkKey") + .BaseUri(new Uri("https://app.AnyOtherEndpoint.com")) + .EventCapacity(99) + .PollingInterval(TimeSpan.FromMinutes(45)) + .Build(); Assert.Equal(new Uri("https://app.AnyOtherEndpoint.com"), config.BaseUri); Assert.Equal("AnyOtherSdkKey", config.MobileKey); - Assert.Equal(99, config.EventQueueCapacity); + Assert.Equal(99, config.EventCapacity); Assert.Equal(TimeSpan.FromMinutes(45), config.PollingInterval); } [Fact] public void CanOverrideStreamConfiguration() { - var config = Configuration.Default("AnyOtherSdkKey") - .WithStreamUri("https://stream.AnyOtherEndpoint.com") - .WithIsStreamingEnabled(false) - .WithReadTimeout(TimeSpan.FromDays(1)) - .WithReconnectTime(TimeSpan.FromDays(1)); + var config = Configuration.Builder("AnyOtherSdkKey") + .StreamUri(new Uri("https://stream.AnyOtherEndpoint.com")) + .IsStreamingEnabled(false) + .ReadTimeout(TimeSpan.FromDays(1)) + .ReconnectTime(TimeSpan.FromDays(1)) + .Build(); Assert.Equal(new Uri("https://stream.AnyOtherEndpoint.com"), config.StreamUri); Assert.False(config.IsStreamingEnabled); @@ -50,7 +52,7 @@ public void MobileKeyCannotBeEmpty() [Fact] public void CannotSetTooSmallPollingInterval() { - var config = Configuration.Default("AnyOtherSdkKey").WithPollingInterval(TimeSpan.FromSeconds(299)); + var config = Configuration.Builder("AnyOtherSdkKey").PollingInterval(TimeSpan.FromSeconds(299)).Build(); Assert.Equal(TimeSpan.FromSeconds(300), config.PollingInterval); } @@ -58,7 +60,7 @@ public void CannotSetTooSmallPollingInterval() [Fact] public void CannotSetTooSmallBackgroundPollingInterval() { - var config = Configuration.Default("SdkKey").WithBackgroundPollingInterval(TimeSpan.FromSeconds(899)); + var config = Configuration.Builder("SdkKey").BackgroundPollingInterval(TimeSpan.FromSeconds(899)).Build(); Assert.Equal(TimeSpan.FromSeconds(900), config.BackgroundPollingInterval); } @@ -67,8 +69,9 @@ public void CannotSetTooSmallBackgroundPollingInterval() public void CanSetHttpClientHandler() { var handler = new HttpClientHandler(); - var config = Configuration.Default("AnyOtherSdkKey") - .WithHttpClientHandler(handler); + var config = Configuration.Builder("AnyOtherSdkKey") + .HttpClientHandler(handler) + .Build(); Assert.Equal(handler, config.HttpClientHandler); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index 2b53e47b..0f68f833 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using LaunchDarkly.Client; using Xunit; @@ -23,8 +24,8 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) - .WithUseReport(false); + var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + .UseReport(false).Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { @@ -49,8 +50,8 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) - .WithUseReport(false).WithEvaluationReasons(true); + var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + .UseReport(false).EvaluationReasons(true).Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { @@ -75,8 +76,8 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) - .WithUseReport(true); + var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + .UseReport(true).Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { @@ -104,8 +105,8 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - var config = Configuration.Default(_mobileKey).WithBaseUri(server.GetUrl()) - .WithUseReport(true).WithEvaluationReasons(true); + var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + .UseReport(true).EvaluationReasons(true).Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 02f3bd8e..07e39964 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -48,7 +48,7 @@ public void InitGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -65,7 +65,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -82,7 +82,7 @@ public void InitCanTimeOutSync() using (var log = new LogSinkScope()) { - var config = BaseConfig(server).WithIsStreamingEnabled(false); + var config = BaseConfig(server).IsStreamingEnabled(false).Build(); using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) { Assert.False(client.Initialized()); @@ -106,7 +106,7 @@ public void InitFailsOn401Sync(UpdateMode mode) { try { - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); using (var client = TestUtil.CreateClient(config, _user)) { } } catch (Exception e) @@ -132,7 +132,7 @@ await WithServerAsync(async server => using (var log = new LogSinkScope()) { - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); // Currently the behavior of LdClient.InitAsync is somewhat inconsistent with LdClient.Init if there is // an unrecoverable error: LdClient.Init throws an exception, but LdClient.InitAsync returns a task that @@ -153,7 +153,7 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); var name = "Sue"; var anonUser = User.Builder((string)null).Name(name).Anonymous(true).Build(); @@ -184,7 +184,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -212,7 +212,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(mode.IsStreaming); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -239,7 +239,7 @@ public void OfflineClientUsesCachedFlagsSync() { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyFlagValues(client, _flagData1); @@ -250,7 +250,7 @@ public void OfflineClientUsesCachedFlagsSync() // out, we should still see the earlier flag values. server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); using (var client = TestUtil.CreateClient(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); @@ -265,7 +265,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).WithUseReport(false).WithIsStreamingEnabled(false); + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyFlagValues(client, _flagData1); @@ -276,7 +276,7 @@ await WithServerAsync(async server => // out, we should still see the earlier flag values. server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Default(_mobileKey).WithOffline(true); + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); @@ -284,12 +284,12 @@ await WithServerAsync(async server => }); } - private Configuration BaseConfig(FluentMockServer server) + private IConfigurationBuilder BaseConfig(FluentMockServer server) { - return Configuration.Default(_mobileKey) - .WithBaseUri(server.GetUrl()) - .WithStreamUri(server.GetUrl()) - .WithEventProcessor(new MockEventProcessor()); + return Configuration.BuilderInternal(_mobileKey) + .EventProcessor(new MockEventProcessor()) + .BaseUri(new Uri(server.GetUrl())) + .StreamUri(new Uri(server.GetUrl())); } private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index 25a3cb10..6ecc0d4b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -12,7 +12,7 @@ public class LdClientEvaluationTests : BaseTest private static LdClient ClientWithFlagsJson(string flagsJson) { - var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson).Build(); return TestUtil.CreateClient(config, user); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index daa4cc6f..fa985a4d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -11,9 +11,9 @@ public class LdClientEventTests : BaseTest public LdClient MakeClient(User user, string flagsJson) { - Configuration config = TestUtil.ConfigWithFlagsJson(user, "appkey", flagsJson); - config.WithEventProcessor(eventProcessor); - return TestUtil.CreateClient(config, user); + var config = TestUtil.ConfigWithFlagsJson(user, "appkey", flagsJson); + config.EventProcessor(eventProcessor); + return TestUtil.CreateClient(config.Build(), user); } [Fact] diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 6729ca2d..d232fd4b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -14,7 +14,7 @@ public class DefaultLdClientTests : BaseTest LdClient Client() { - var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); return TestUtil.CreateClient(configuration, simpleUser); } @@ -27,21 +27,21 @@ public void CannotCreateClientWithNullConfig() [Fact] public void CannotCreateClientWithNullUser() { - Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); Assert.Throws(() => LdClient.Init(config, null, TimeSpan.Zero)); } [Fact] public void CannotCreateClientWithNegativeWaitTime() { - Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.FromMilliseconds(-2))); } [Fact] public void CanCreateClientWithInfiniteWaitTime() { - Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); using (var client = LdClient.Init(config, simpleUser, System.Threading.Timeout.InfiniteTimeSpan)) { } TestUtil.ClearClient(); } @@ -81,7 +81,7 @@ public void SharedClientIsTheOnlyClientAvailable() { TestUtil.WithClientLock(() => { - var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); using (var client = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.Zero)); @@ -96,7 +96,7 @@ public void CanCreateNewClientAfterDisposingOfSharedInstance() TestUtil.WithClientLock(() => { TestUtil.ClearClient(); - var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}").Build(); using (var client0 = LdClient.Init(config, simpleUser, TimeSpan.Zero)) { } Assert.Null(LdClient.Instance); // Dispose() is called automatically at end of "using" block @@ -109,7 +109,7 @@ public void ConnectionManagerShouldKnowIfOnlineOrNot() { using (var client = Client()) { - var connMgr = client.Config.ConnectionManager as MockConnectionManager; + var connMgr = client.Config._connectionManager as MockConnectionManager; connMgr.ConnectionChanged += (bool obj) => client.Online = obj; connMgr.Connect(true); Assert.False(client.IsOffline()); @@ -123,10 +123,10 @@ public void ConnectionChangeShouldStopUpdateProcessor() { var mockUpdateProc = new MockPollingProcessor(null); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithUpdateProcessorFactory(mockUpdateProc.AsFactory()); + .UpdateProcessorFactory(mockUpdateProc.AsFactory()).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { - var connMgr = client.Config.ConnectionManager as MockConnectionManager; + var connMgr = client.Config._connectionManager as MockConnectionManager; connMgr.ConnectionChanged += (bool obj) => client.Online = obj; connMgr.Connect(false); Assert.False(mockUpdateProc.IsRunning); @@ -139,7 +139,7 @@ public void UserWithNullKeyWillHaveUniqueKeySet() var userWithNullKey = User.WithKey(null); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, appKey, "{}") - .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, userWithNullKey)) { Assert.Equal(uniqueId, client.User.Key); @@ -153,7 +153,7 @@ public void UserWithEmptyKeyWillHaveUniqueKeySet() var userWithEmptyKey = User.WithKey(""); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, appKey, "{}") - .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, userWithEmptyKey)) { Assert.Equal(uniqueId, client.User.Key); @@ -167,7 +167,7 @@ public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() var userWithNullKey = User.WithKey(null); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { client.Identify(userWithNullKey); @@ -182,7 +182,7 @@ public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() var userWithEmptyKey = User.WithKey(""); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { client.Identify(userWithEmptyKey); @@ -207,7 +207,7 @@ public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() .Build(); var uniqueId = "some-unique-key"; var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { client.Identify(user); @@ -249,9 +249,10 @@ public void FlagsAreLoadedFromPersistentStorageByDefault() var flagsJson = "{\"flag\": {\"value\": 100}}"; storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithOffline(true) - .WithPersistentStorage(storage) - .WithFlagCacheManager(null); // use actual cache logic, not mock component (even though persistence layer is a mock) + .PersistentStorage(storage) + .FlagCacheManager(null) // use actual cache logic, not mock component (even though persistence layer is a mock) + .Offline(true) + .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { Assert.Equal(100, client.IntVariation("flag", 99)); @@ -265,10 +266,11 @@ public void FlagsAreNotLoadedFromPersistentStorageIfPersistFlagValuesIsFalse() var flagsJson = "{\"flag\": {\"value\": 100}}"; storage.Save(Constants.FLAGS_KEY_PREFIX + simpleUser.Key, flagsJson); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .WithOffline(true) - .WithPersistFlagValues(false) - .WithPersistentStorage(storage) - .WithFlagCacheManager(null); // use actual cache logic, not mock component (even though persistence layer is a mock) + .PersistentStorage(storage) + .FlagCacheManager(null) // use actual cache logic, not mock component (even though persistence layer is a mock) + .PersistFlagValues(false) + .Offline(true) + .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { Assert.Equal(99, client.IntVariation("flag", 99)); // returns default value @@ -281,9 +283,10 @@ public void FlagsAreSavedToPersistentStorageByDefault() var storage = new MockPersistentStorage(); var flagsJson = "{\"flag\": {\"value\": 100}}"; var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, flagsJson) - .WithPersistentStorage(storage) - .WithFlagCacheManager(null) - .WithUpdateProcessorFactory(MockPollingProcessor.Factory(flagsJson)); + .FlagCacheManager(null) + .UpdateProcessorFactory(MockPollingProcessor.Factory(flagsJson)) + .PersistentStorage(storage) + .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { var storedJson = storage.GetValue(Constants.FLAGS_KEY_PREFIX + simpleUser.Key); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index 562753e4..d1fcd60c 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -23,23 +23,25 @@ public class MobileStreamingProcessorTests : BaseTest private EventSourceMock mockEventSource; private TestEventSourceFactory eventSourceFactory; private IFlagCacheManager mockFlagCacheMgr; - private Configuration config; + private IConfigurationBuilder configBuilder; public MobileStreamingProcessorTests() { mockEventSource = new EventSourceMock(); eventSourceFactory = new TestEventSourceFactory(mockEventSource); mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); - config = Configuration.Default("someKey") - .WithConnectionManager(new MockConnectionManager(true)) - .WithIsStreamingEnabled(true) - .WithFlagCacheManager(mockFlagCacheMgr); + configBuilder = Configuration.BuilderInternal("someKey") + .ConnectionManager(new MockConnectionManager(true)) + .FlagCacheManager(mockFlagCacheMgr) + .IsStreamingEnabled(true); + } private IMobileUpdateProcessor MobileStreamingProcessorStarted() { - IMobileUpdateProcessor processor = new MobileStreamingProcessor(config, mockFlagCacheMgr, user, eventSourceFactory.Create()); + IMobileUpdateProcessor processor = new MobileStreamingProcessor(configBuilder.Build(), + mockFlagCacheMgr, user, eventSourceFactory.Create()); processor.Start(); return processor; } @@ -47,8 +49,8 @@ private IMobileUpdateProcessor MobileStreamingProcessorStarted() [Fact] public void StreamUriInGetModeHasUser() { - config.WithUseReport(false); - var streamingProcessor = MobileStreamingProcessorStarted(); + var config = configBuilder.UseReport(false).Build(); + MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(HttpMethod.Get, props.Method); Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + encodedUser), props.StreamUri); @@ -57,9 +59,8 @@ public void StreamUriInGetModeHasUser() [Fact] public void StreamUriInGetModeHasReasonsParameterIfConfigured() { - config.WithUseReport(false); - config.WithEvaluationReasons(true); - var streamingProcessor = MobileStreamingProcessorStarted(); + var config = configBuilder.UseReport(false).EvaluationReasons(true).Build(); + MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + encodedUser + "?withReasons=true"), props.StreamUri); } @@ -67,8 +68,8 @@ public void StreamUriInGetModeHasReasonsParameterIfConfigured() [Fact] public void StreamUriInReportModeHasNoUser() { - config.WithUseReport(true); - var streamingProcessor = MobileStreamingProcessorStarted(); + var config = configBuilder.UseReport(true).Build(); + MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(new HttpMethod("REPORT"), props.Method); Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH), props.StreamUri); @@ -77,18 +78,17 @@ public void StreamUriInReportModeHasNoUser() [Fact] public void StreamUriInReportModeHasReasonsParameterIfConfigured() { - config.WithUseReport(true); - config.WithEvaluationReasons(true); - var streamingProcessor = MobileStreamingProcessorStarted(); + var config = configBuilder.UseReport(true).EvaluationReasons(true).Build(); + MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + "?withReasons=true"), props.StreamUri); } [Fact] - public async void StreamRequestBodyInReportModeHasUser() + public async Task StreamRequestBodyInReportModeHasUser() { - config.WithUseReport(true); - var streamingProcessor = MobileStreamingProcessorStarted(); + configBuilder.UseReport(true); + MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; var body = Assert.IsType(props.RequestBody); var s = await body.ReadAsStringAsync(); @@ -98,7 +98,7 @@ public async void StreamRequestBodyInReportModeHasUser() [Fact] public void PUTstoresFeatureFlags() { - var streamingProcessor = MobileStreamingProcessorStarted(); + MobileStreamingProcessorStarted(); // should be empty before PUT message arrives var flagsInCache = mockFlagCacheMgr.FlagsForUser(user); Assert.Empty(flagsInCache); @@ -114,7 +114,7 @@ public void PUTstoresFeatureFlags() public void PATCHupdatesFeatureFlag() { // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); + MobileStreamingProcessorStarted(); PUTMessageSentToProcessor(); var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); Assert.Equal(15, intFlagFromPUT); @@ -132,7 +132,7 @@ public void PATCHupdatesFeatureFlag() public void PATCHdoesnotUpdateFlagIfVersionIsLower() { // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); + MobileStreamingProcessorStarted(); PUTMessageSentToProcessor(); var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); Assert.Equal(15, intFlagFromPUT); @@ -150,7 +150,7 @@ public void PATCHdoesnotUpdateFlagIfVersionIsLower() public void DELETEremovesFeatureFlag() { // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); + MobileStreamingProcessorStarted(); PUTMessageSentToProcessor(); var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); Assert.Equal(15, intFlagFromPUT); @@ -167,7 +167,7 @@ public void DELETEremovesFeatureFlag() public void DELTEdoesnotRemoveFeatureFlagIfVersionIsLower() { // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); + MobileStreamingProcessorStarted(); PUTMessageSentToProcessor(); var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); Assert.Equal(15, intFlagFromPUT); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index ca31976b..208f9a25 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -133,7 +133,7 @@ public static IDictionary DecodeFlagsJson(string flagsJson) return JsonConvert.DeserializeObject>(flagsJson); } - public static Configuration ConfigWithFlagsJson(User user, string appKey, string flagsJson) + internal static ConfigurationBuilder ConfigWithFlagsJson(User user, string appKey, string flagsJson) { var flags = DecodeFlagsJson(flagsJson); IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); @@ -142,14 +142,13 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string stubbedFlagCache.CacheFlagsForUser(flags, user); } - Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) - .WithConnectionManager(new MockConnectionManager(true)) - .WithEventProcessor(new MockEventProcessor()) - .WithUpdateProcessorFactory(MockPollingProcessor.Factory(null)) - .WithPersistentStorage(new MockPersistentStorage()) - .WithDeviceInfo(new MockDeviceInfo("")); - return configuration; + return Configuration.BuilderInternal(appKey) + .FlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) + .ConnectionManager(new MockConnectionManager(true)) + .EventProcessor(new MockEventProcessor()) + .UpdateProcessorFactory(MockPollingProcessor.Factory(null)) + .PersistentStorage(new MockPersistentStorage()) + .DeviceInfo(new MockDeviceInfo("")); } } } From 3614e66adfc3042a6bbebad46ef3f63af684fabc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 00:34:31 -0700 Subject: [PATCH 194/254] revert accidental changes --- .../LaunchDarkly.XamarinSdk.csproj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index b2cf6f2c..ce250990 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -3,9 +3,7 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - - netstandard1.6;netstandard2.0;Xamarin.iOS10; + netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; 1.0.0-beta18 Library LaunchDarkly.XamarinSdk @@ -76,10 +74,5 @@ - - - - - From c9c5a7490b27ee6bdc9a8c96b1ed1b817246d507 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 00:57:37 -0700 Subject: [PATCH 195/254] Change JToken to ImmutableJsonValue in public API --- src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs | 12 ++++++------ .../LaunchDarkly.XamarinSdk.csproj | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 16 ++++++++-------- src/LaunchDarkly.XamarinSdk/ValueType.cs | 7 ++++--- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LdClientEvaluationTests.cs | 10 +++++----- .../LdClientEventTests.cs | 2 +- 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index 21f33952..f750fc4f 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -92,30 +92,30 @@ public interface ILdMobileClient : ILdCommonClient EvaluationDetail IntVariationDetail(string key, int defaultValue = 0); /// - /// Returns the JToken value of a feature flag for a given flag key. + /// Returns the JSON value of a feature flag for a given flag key. /// /// the unique feature key for the feature flag /// the default value of the flag /// the variation for the selected user, or defaultValue if the flag is /// disabled in the LaunchDarkly control panel - JToken JsonVariation(string key, JToken defaultValue); + ImmutableJsonValue JsonVariation(string key, ImmutableJsonValue defaultValue); /// - /// Returns the JToken value of a feature flag for a given flag key, in an object that also + /// Returns the JSON value of a feature flag for a given flag key, in an object that also /// describes the way the value was determined. The Reason property in the result will /// also be included in analytics events, if you are capturing detailed event data for this flag. /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object - EvaluationDetail JsonVariationDetail(string key, JToken defaultValue); + EvaluationDetail JsonVariationDetail(string key, ImmutableJsonValue defaultValue); /// /// Tracks that current user performed an event for the given JToken value and given event name. /// /// the name of the event - /// a JSON string containing additional data associated with the event - void Track(string eventName, JToken data); + /// a JSON value containing additional data associated with the event + void Track(string eventName, ImmutableJsonValue data); /// /// Tracks that current user performed an event for the given event name. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index ce250990..b13b3e72 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index dfb71833..ebf631ec 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -335,14 +335,14 @@ public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0 return VariationInternal(key, defaultValue, ValueTypes.Int, eventFactoryWithReasons); } - /// - public JToken JsonVariation(string key, JToken defaultValue) + /// + public ImmutableJsonValue JsonVariation(string key, ImmutableJsonValue defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryDefault).Value; } - /// - public EvaluationDetail JsonVariationDetail(string key, JToken defaultValue) + /// + public EvaluationDetail JsonVariationDetail(string key, ImmutableJsonValue defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.Json, eventFactoryWithReasons); } @@ -416,16 +416,16 @@ public IDictionary AllFlags() .ToDictionary(p => p.Key, p => p.Value.value); } - /// - public void Track(string eventName, JToken data) + /// + public void Track(string eventName, ImmutableJsonValue data) { - eventProcessor.SendEvent(eventFactoryDefault.NewCustomEvent(eventName, User, data)); + eventProcessor.SendEvent(eventFactoryDefault.NewCustomEvent(eventName, User, data.AsJToken())); } /// public void Track(string eventName) { - Track(eventName, null); + Track(eventName, ImmutableJsonValue.FromJToken(null)); } /// diff --git a/src/LaunchDarkly.XamarinSdk/ValueType.cs b/src/LaunchDarkly.XamarinSdk/ValueType.cs index eef0c6ed..b8e89cf4 100644 --- a/src/LaunchDarkly.XamarinSdk/ValueType.cs +++ b/src/LaunchDarkly.XamarinSdk/ValueType.cs @@ -1,4 +1,5 @@ using System; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin @@ -72,10 +73,10 @@ private static ArgumentException BadTypeException() ValueToJson = value => value == null ? null : new JValue(value) }; - public static ValueType Json = new ValueType + public static ValueType Json = new ValueType { - ValueFromJson = json => json, - ValueToJson = value => value + ValueFromJson = json => ImmutableJsonValue.FromJToken(json), + ValueToJson = value => value.AsJToken() }; } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 22e0c8a5..90d7d771 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index 25a3cb10..bf5e4a8c 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -167,8 +167,7 @@ public void JsonVariationReturnsValue() string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue); using (var client = ClientWithFlagsJson(flagsJson)) { - var defaultValue = new JValue(3); - Assert.Equal(jsonValue, client.JsonVariation("flag-key", defaultValue)); + Assert.Equal(jsonValue, client.JsonVariation("flag-key", ImmutableJsonValue.FromJToken(3)).AsJToken()); } } @@ -177,7 +176,8 @@ public void JsonVariationReturnsDefaultForUnknownFlag() { using (var client = ClientWithFlagsJson("{}")) { - Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); + var defaultVal = ImmutableJsonValue.FromJToken(3); + Assert.Equal(defaultVal, client.JsonVariation(nonexistentFlagKey, defaultVal)); } } @@ -190,9 +190,9 @@ public void JsonVariationDetailReturnsValue() using (var client = ClientWithFlagsJson(flagsJson)) { var expected = new EvaluationDetail(jsonValue, 1, reason); - var result = client.JsonVariationDetail("flag-key", new JValue(3)); + var result = client.JsonVariationDetail("flag-key", ImmutableJsonValue.FromJToken(3)); // Note, JToken.Equals() doesn't work, so we need to test each property separately - Assert.True(JToken.DeepEquals(expected.Value, result.Value)); + Assert.True(JToken.DeepEquals(expected.Value, result.Value.AsJToken())); Assert.Equal(expected.VariationIndex, result.VariationIndex); Assert.Equal(expected.Reason, result.Reason); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index daa4cc6f..cf8755c1 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -35,7 +35,7 @@ public void TrackSendsCustomEvent() using (LdClient client = MakeClient(user, "{}")) { JToken data = new JValue("hi"); - client.Track("eventkey", data); + client.Track("eventkey", ImmutableJsonValue.FromJToken(data)); Assert.Collection(eventProcessor.Events, e => CheckIdentifyEvent(e, user), e => { From 9a02c6e01b4ae21db992ca802240a52fc5d4992d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 10:18:05 -0700 Subject: [PATCH 196/254] whitespace --- .../ConfigurationBuilder.cs | 162 ++++++++++++++---- 1 file changed, 131 insertions(+), 31 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index f07b05aa..63d80634 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -23,7 +23,14 @@ namespace LaunchDarkly.Xamarin /// public interface IConfigurationBuilder { - /// /// Creates a based on the properties that have been set on the builder. /// Modifying the builder after this point does not affect the returned Configuration. /// /// the configured Configuration object Configuration Build(); /// + /// + /// Creates a based on the properties that have been set on the builder. + /// Modifying the builder after this point does not affect the returned Configuration. + /// + /// the configured Configuration object + Configuration Build(); + + /// /// Sets whether or not user attributes (other than the key) should be private (not sent to /// the LaunchDarkly server). /// @@ -34,17 +41,24 @@ public interface IConfigurationBuilder /// /// true if all attributes should be private /// the same builder - IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate); /// + IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate); + + /// /// Sets the interval for background polling. /// /// the background polling interval /// the same builder - IConfigurationBuilder BackgroundPollingInterval(TimeSpan backgroundPollingInterval); /// + IConfigurationBuilder BackgroundPollingInterval(TimeSpan backgroundPollingInterval); + + /// /// Sets the base URI of the LaunchDarkly server. /// /// the base URI /// the same builder - IConfigurationBuilder BaseUri(Uri baseUri); /// /// Set to true if LaunchDarkly should provide additional information about how flag values were + IConfigurationBuilder BaseUri(Uri baseUri); + + /// + /// Set to true if LaunchDarkly should provide additional information about how flag values were /// calculated. /// /// @@ -56,7 +70,8 @@ public interface IConfigurationBuilder /// True if evaluation reasons are desired. /// the same builder IConfigurationBuilder EvaluationReasons(bool evaluationReasons); - /// + + /// /// Sets the capacity of the events buffer. /// /// @@ -66,7 +81,9 @@ public interface IConfigurationBuilder /// /// the capacity of the events buffer /// the same builder - IConfigurationBuilder EventCapacity(int eventCapacity); /// + IConfigurationBuilder EventCapacity(int eventCapacity); + + /// /// Sets the time between flushes of the event buffer. /// /// @@ -76,7 +93,7 @@ public interface IConfigurationBuilder /// the flush interval /// the same builder IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval); - /// + /// /// Enables event sampling if non-zero. /// /// @@ -86,18 +103,23 @@ public interface IConfigurationBuilder /// /// the sampling interval /// the same builder - IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval); /// + IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval); + + /// /// Sets the base URL of the LaunchDarkly analytics event server. /// /// the events URI /// the same builder - IConfigurationBuilder EventsUri(Uri eventsUri); - + IConfigurationBuilder EventsUri(Uri eventsUri); + /// /// Sets the object to be used for sending HTTP requests. This is exposed for testing purposes. /// /// the HttpClientHandler to use - /// the same builder IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler); /// + /// the same builder + IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler); + + /// /// Sets the connection timeout. The default value is 10 seconds. /// /// the connection timeout @@ -112,7 +134,8 @@ public interface IConfigurationBuilder /// provides the full details for the user. /// /// true or false - /// the same builder IConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents); + /// the same builder + IConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents); /// /// Sets whether or not the streaming API should be used to receive flag updates. @@ -121,12 +144,16 @@ public interface IConfigurationBuilder /// This is true by default. Streaming should only be disabled on the advice of LaunchDarkly support. /// /// true if the streaming API should be used - /// the same builder IConfigurationBuilder IsStreamingEnabled(bool isStreamingEnabled); - /// + /// the same builder + IConfigurationBuilder IsStreamingEnabled(bool isStreamingEnabled); + /// /// /// /// - /// the same builder IConfigurationBuilder MobileKey(string mobileKey); /// + /// the same builder + IConfigurationBuilder MobileKey(string mobileKey); + + /// /// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made. /// /// true if the client should remain offline @@ -140,7 +167,9 @@ public interface IConfigurationBuilder /// true to save flag values /// the same Configuration instance /// the same builder - IConfigurationBuilder PersistFlagValues(bool persistFlagValues); /// + IConfigurationBuilder PersistFlagValues(bool persistFlagValues); + + /// /// Sets the polling interval (when streaming is disabled). /// /// @@ -159,7 +188,10 @@ public interface IConfigurationBuilder /// You may call this method repeatedly to mark multiple attributes as private. /// /// the attribute name - /// the same builder IConfigurationBuilder PrivateAttribute(string privateAtributeName); /// + /// the same builder + IConfigurationBuilder PrivateAttribute(string privateAtributeName); + + /// /// Sets the timeout when reading data from the streaming connection. /// /// @@ -167,7 +199,9 @@ public interface IConfigurationBuilder /// /// the read timeout /// the same builder - IConfigurationBuilder ReadTimeout(TimeSpan readTimeout); /// + IConfigurationBuilder ReadTimeout(TimeSpan readTimeout); + + /// /// Sets the reconnect base time for the streaming connection. /// /// @@ -176,7 +210,9 @@ public interface IConfigurationBuilder /// /// the reconnect time base value /// the same builder - IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime); /// + IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime); + + /// /// Sets the base URI of the LaunchDarkly streaming server. /// /// the stream URI @@ -198,7 +234,10 @@ public interface IConfigurationBuilder /// be sent in analytics events. /// /// the user key cache capacity - /// the same builder IConfigurationBuilder UserKeysCapacity(int userKeysCapacity); /// + /// the same builder + IConfigurationBuilder UserKeysCapacity(int userKeysCapacity); + + /// /// Sets the interval at which the event processor will clear its cache of known user keys. /// /// @@ -248,8 +287,45 @@ internal class ConfigurationBuilder : IConfigurationBuilder internal IPersistentStorage _persistentStorage; internal Func _updateProcessorFactory; - internal ConfigurationBuilder(string mobileKey) { _mobileKey = mobileKey; - } internal ConfigurationBuilder(Configuration copyFrom) { _allAttributesPrivate = copyFrom.AllAttributesPrivate; _backgroundPollingInterval = copyFrom.BackgroundPollingInterval; _baseUri = copyFrom.BaseUri; _connectionTimeout = copyFrom.ConnectionTimeout; _enableBackgroundUpdating = copyFrom.EnableBackgroundUpdating; _evaluationReasons = copyFrom.EvaluationReasons; _eventCapacity = copyFrom.EventCapacity; _eventFlushInterval = copyFrom.EventFlushInterval; _eventSamplingInterval = copyFrom.EventSamplingInterval; _eventsUri = copyFrom.EventsUri; _httpClientHandler = copyFrom.HttpClientHandler; _httpClientTimeout = copyFrom.HttpClientTimeout; _inlineUsersInEvents = copyFrom.InlineUsersInEvents; _isStreamingEnabled = copyFrom.IsStreamingEnabled; _mobileKey = copyFrom.MobileKey; _offline = copyFrom.Offline; _persistFlagValues = copyFrom.PersistFlagValues; _pollingInterval = copyFrom.PollingInterval; _privateAttributeNames = copyFrom.PrivateAttributeNames is null ? null : new HashSet(copyFrom.PrivateAttributeNames); _readTimeout = copyFrom.ReadTimeout; _reconnectTime = copyFrom.ReconnectTime; _streamUri = copyFrom.StreamUri; _useReport = copyFrom.UseReport; _userKeysCapacity = copyFrom.UserKeysCapacity; _userKeysFlushInterval = copyFrom.UserKeysFlushInterval; } public Configuration Build() { return new Configuration(this); } + internal ConfigurationBuilder(string mobileKey) + { + _mobileKey = mobileKey; + } + + internal ConfigurationBuilder(Configuration copyFrom) + { + _allAttributesPrivate = copyFrom.AllAttributesPrivate; + _backgroundPollingInterval = copyFrom.BackgroundPollingInterval; + _baseUri = copyFrom.BaseUri; + _connectionTimeout = copyFrom.ConnectionTimeout; + _enableBackgroundUpdating = copyFrom.EnableBackgroundUpdating; + _evaluationReasons = copyFrom.EvaluationReasons; + _eventCapacity = copyFrom.EventCapacity; + _eventFlushInterval = copyFrom.EventFlushInterval; + _eventSamplingInterval = copyFrom.EventSamplingInterval; + _eventsUri = copyFrom.EventsUri; + _httpClientHandler = copyFrom.HttpClientHandler; + _httpClientTimeout = copyFrom.HttpClientTimeout; + _inlineUsersInEvents = copyFrom.InlineUsersInEvents; + _isStreamingEnabled = copyFrom.IsStreamingEnabled; + _mobileKey = copyFrom.MobileKey; + _offline = copyFrom.Offline; + _persistFlagValues = copyFrom.PersistFlagValues; + _pollingInterval = copyFrom.PollingInterval; + _privateAttributeNames = copyFrom.PrivateAttributeNames is null ? null : + new HashSet(copyFrom.PrivateAttributeNames); + _readTimeout = copyFrom.ReadTimeout; + _reconnectTime = copyFrom.ReconnectTime; + _streamUri = copyFrom.StreamUri; + _useReport = copyFrom.UseReport; + _userKeysCapacity = copyFrom.UserKeysCapacity; + _userKeysFlushInterval = copyFrom.UserKeysFlushInterval; + } + + public Configuration Build() + { + return new Configuration(this); + } public IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate) { @@ -299,7 +375,9 @@ public IConfigurationBuilder EventCapacity(int eventCapacity) { _eventCapacity = eventCapacity; return this; - } public IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval) + } + + public IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval) { _eventFlushInterval = eventflushInterval; return this; @@ -309,7 +387,9 @@ public IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval) { _eventSamplingInterval = eventSamplingInterval; return this; - } public IConfigurationBuilder EventsUri(Uri eventsUri) + } + + public IConfigurationBuilder EventsUri(Uri eventsUri) { _eventsUri = eventsUri; return this; @@ -319,7 +399,9 @@ public IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandl { _httpClientHandler = httpClientHandler; return this; - } public IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout) + } + + public IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout) { _httpClientTimeout = httpClientTimeout; return this; @@ -341,11 +423,21 @@ public IConfigurationBuilder MobileKey(string mobileKey) { _mobileKey = mobileKey; return this; - } public IConfigurationBuilder Offline(bool offline) { _offline = offline; return this; } public IConfigurationBuilder PersistFlagValues(bool persistFlagValues) + } + + public IConfigurationBuilder Offline(bool offline) + { + _offline = offline; + return this; + } + + public IConfigurationBuilder PersistFlagValues(bool persistFlagValues) { _persistFlagValues = persistFlagValues; return this; - } public IConfigurationBuilder PollingInterval(TimeSpan pollingInterval) + } + + public IConfigurationBuilder PollingInterval(TimeSpan pollingInterval) { if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) { @@ -367,15 +459,21 @@ public IConfigurationBuilder PrivateAttribute(string privateAtributeName) } _privateAttributeNames.Add(privateAtributeName); return this; - } public IConfigurationBuilder ReadTimeout(TimeSpan readTimeout) + } + + public IConfigurationBuilder ReadTimeout(TimeSpan readTimeout) { _readTimeout = readTimeout; return this; - } public IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime) + } + + public IConfigurationBuilder ReconnectTime(TimeSpan reconnectTime) { _reconnectTime = reconnectTime; return this; - } public IConfigurationBuilder StreamUri(Uri streamUri) + } + + public IConfigurationBuilder StreamUri(Uri streamUri) { _streamUri = streamUri; return this; @@ -391,7 +489,9 @@ public IConfigurationBuilder UserKeysCapacity(int userKeysCapacity) { _userKeysCapacity = userKeysCapacity; return this; - } public IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterval) + } + + public IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterval) { _userKeysFlushInterval = userKeysFlushInterval; return this; From 9fc6d84d43f399e8786a469ce286c64e406a1dda Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 10:18:56 -0700 Subject: [PATCH 197/254] whitespace --- src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index 63d80634..db585ec5 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -93,6 +93,7 @@ public interface IConfigurationBuilder /// the flush interval /// the same builder IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval); + /// /// Enables event sampling if non-zero. /// From 4e6e3fba797bf410a4b505d905095ecdd993af01 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 12:51:35 -0700 Subject: [PATCH 198/254] fix merge --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 276d35ba..d666600a 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -75,23 +75,6 @@ public bool Online } } - readonly object myLockObjForConnectionChange = new object(); - readonly object myLockObjForUserUpdate = new object(); - - readonly IFlagCacheManager flagCacheManager; - readonly IConnectionManager connectionManager; - IMobileUpdateProcessor updateProcessor; // not readonly - may need to be recreated - readonly IEventProcessor eventProcessor; - readonly IPersistentStorage persister; - readonly IDeviceInfo deviceInfo; - readonly EventFactory eventFactoryDefault = EventFactory.Default; - readonly EventFactory eventFactoryWithReasons = EventFactory.DefaultWithReasons; - internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing - - readonly SemaphoreSlim connectionLock; - - volatile bool online; - // private constructor prevents initialization of this class // without using WithConfigAnduser(config, user) LdClient() { } From e91bb1f4f32b2af38db164e73b3c519e61c96387 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 12:52:53 -0700 Subject: [PATCH 199/254] fix merge --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index d666600a..48f7dbf2 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -68,7 +68,7 @@ public sealed class LdClient : ILdMobileClient /// public bool Online { - get => online; + get => _online; set { var doNotAwaitResult = SetOnlineAsync(value); @@ -247,16 +247,16 @@ void SetupConnectionManager() Log.InfoFormat("The mobile client connection changed online to {0}", connectionManager.IsConnected); } - online = connectionManager.IsConnected; + _online = connectionManager.IsConnected; } public async Task SetOnlineAsync(bool value) { await _connectionLock.WaitAsync(); - online = value; + _online = value; try { - if (online) + if (_online) { await RestartUpdateProcessorAsync(Config.PollingInterval); } @@ -428,7 +428,7 @@ public bool Initialized() /// public bool IsOffline() { - return !online; + return !_online; } /// From a0ba32d41fc70dc546727345204211f6c31d7bc0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 12:55:23 -0700 Subject: [PATCH 200/254] fix merge --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 1 + .../AndroidSpecificTests.cs | 20 +++++++++---------- .../IOsSpecificTests.cs | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 48f7dbf2..9fed1adf 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -42,6 +42,7 @@ public sealed class LdClient : ILdMobileClient // These LdClient fields are not readonly because they change according to online status volatile IMobileUpdateProcessor updateProcessor; volatile bool _disableStreaming; + volatile bool _online; /// /// The singleton instance used by your application throughout its lifetime. Once this exists, you cannot diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs index 35fb99da..aa8ba571 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs @@ -8,16 +8,16 @@ public class AndroidSpecificTests [Fact] public void UserHasOSAndDeviceAttributesForPlatform() { - var baseUser = User.WithKey("key"); - var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}"); - using (var client = TestUtil.CreateClient(config, baseUser)) - { - var user = client.User; - Assert.Equal(baseUser.Key, user.Key); - Assert.Contains("os", user.Custom.Keys); - Assert.StartsWith("Android ", user.Custom["os"].AsString); - Assert.Contains("device", user.Custom.Keys); - } + var baseUser = User.WithKey("key"); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").build(); + using (var client = TestUtil.CreateClient(config, baseUser)) + { + var user = client.User; + Assert.Equal(baseUser.Key, user.Key); + Assert.Contains("os", user.Custom.Keys); + Assert.StartsWith("Android ", user.Custom["os"].AsString); + Assert.Contains("device", user.Custom.Keys); + } } } } diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs index c16ffe57..6032b61e 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -9,7 +9,7 @@ public class IOsSpecificTests public void UserHasOSAndDeviceAttributesForPlatform() { var baseUser = User.WithKey("key"); - var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}"); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").build(); using (var client = TestUtil.CreateClient(config, baseUser)) { var user = client.User; From f23bd49fb3800d7b361cdeff97402d402e4811d4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 30 Jul 2019 12:58:29 -0700 Subject: [PATCH 201/254] typo --- .../AndroidSpecificTests.cs | 2 +- tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs index aa8ba571..51490d24 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs @@ -9,7 +9,7 @@ public class AndroidSpecificTests public void UserHasOSAndDeviceAttributesForPlatform() { var baseUser = User.WithKey("key"); - var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").build(); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").Build(); using (var client = TestUtil.CreateClient(config, baseUser)) { var user = client.User; diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs index 6032b61e..cf97ed9f 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -9,7 +9,7 @@ public class IOsSpecificTests public void UserHasOSAndDeviceAttributesForPlatform() { var baseUser = User.WithKey("key"); - var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").build(); + var config = TestUtil.ConfigWithFlagsJson(baseUser, "mobileKey", "{}").Build(); using (var client = TestUtil.CreateClient(config, baseUser)) { var user = client.User; From eebea8999b8226ff3f523a541c3f3335389793d0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 10:58:35 -0700 Subject: [PATCH 202/254] add project property that's required by recent Xamarin Android versions --- src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 1 + .../LaunchDarkly.XamarinSdk.Android.Tests.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index b13b3e72..e2fd64f5 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -13,6 +13,7 @@ true latest True + False diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index 92602515..da88579f 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -22,6 +22,7 @@ Resources Assets true + False True From ff0e29c9915800f34ac017ec214f8a04579411b0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:31:58 -0700 Subject: [PATCH 203/254] debug logging of exception stacktrace --- src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index a630cf0c..8ef352c1 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -143,6 +143,7 @@ private void FireEvent(FlagChangedEventArgs eventArgs) catch (Exception e) { Log.Warn("Unexpected exception from FlagChanged event handler", e); + Log.Debug(e, e); } }); } From caef323d3756c6615ea5c3f79b10297ed14780ec Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:33:27 -0700 Subject: [PATCH 204/254] add internal property for detecting current build platform --- .../PlatformSpecific/UserMetadata.android.cs | 2 ++ .../PlatformSpecific/UserMetadata.ios.cs | 2 ++ .../UserMetadata.netstandard.cs | 2 ++ .../PlatformSpecific/UserMetadata.shared.cs | 4 +++- src/LaunchDarkly.XamarinSdk/PlatformType.cs | 24 +++++++++++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformType.cs diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs index b6b1bc02..6753aa8b 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.android.cs @@ -9,5 +9,7 @@ internal static partial class UserMetadata private static string PlatformOS => "Android " + Build.VERSION.SdkInt; + + private static PlatformType PlatformPlatformType => PlatformType.Android; } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs index ec7a1b29..5f5cd365 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.ios.cs @@ -26,5 +26,7 @@ private static string PlatformDevice private static string PlatformOS => "iOS " + UIDevice.CurrentDevice.SystemVersion; + + private static PlatformType PlatformPlatformType => PlatformType.IOs; } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs index 145b7ac4..6b6a2f07 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.netstandard.cs @@ -6,5 +6,7 @@ internal static partial class UserMetadata private static string PlatformDevice => null; private static string PlatformOS => null; + + private static PlatformType PlatformPlatformType => PlatformType.Standard; } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs index 7da32ec9..90152c3f 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/UserMetadata.shared.cs @@ -9,7 +9,7 @@ internal static partial class UserMetadata // to avoid having to recompute them many times. private static readonly string _os = PlatformOS; private static readonly string _device = PlatformDevice; - + /// /// Returns the string that should be passed in the "device" property for all users. /// @@ -21,5 +21,7 @@ internal static partial class UserMetadata /// /// The value for "os", or null if none. internal static string OSName => _os; + + internal static PlatformType PlatformType => PlatformPlatformType; } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformType.cs b/src/LaunchDarkly.XamarinSdk/PlatformType.cs new file mode 100644 index 00000000..3c5ab789 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformType.cs @@ -0,0 +1,24 @@ + +namespace LaunchDarkly.Xamarin +{ + /// + /// Values returned by . + /// + public enum PlatformType + { + /// + /// You are using the .NET Standard version of the SDK. + /// + Standard, + + /// + /// You are using the Android version of the SDK. + /// + Android, + + /// + /// You are using the iOS version of the SDK. + /// + IOs + } +} From 463df24289cd9079eb14378c996ead8d5cd79ac6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:35:10 -0700 Subject: [PATCH 205/254] misc doc comment improvements --- .../ILdMobileClient.cs | 89 +++++++++++++++---- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index f750fc4f..1f251f45 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -26,9 +26,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Returns the boolean value of a feature flag for a given flag key, in an object that also - /// describes the way the value was determined. The Reason property in the result will - /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// describes the way the value was determined. /// + /// + /// The Reason property in the result will also be included in analytics events, if you are + /// capturing detailed event data for this flag. + /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object @@ -45,9 +48,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Returns the string value of a feature flag for a given flag key, in an object that also - /// describes the way the value was determined. The Reason property in the result will - /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// describes the way the value was determined. /// + /// + /// The Reason property in the result will also be included in analytics events, if you are + /// capturing detailed event data for this flag. + /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object @@ -64,9 +70,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Returns the float value of a feature flag for a given flag key, in an object that also - /// describes the way the value was determined. The Reason property in the result will - /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// describes the way the value was determined. /// + /// + /// The Reason property in the result will also be included in analytics events, if you are + /// capturing detailed event data for this flag. + /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object @@ -83,9 +92,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Returns the integer value of a feature flag for a given flag key, in an object that also - /// describes the way the value was determined. The Reason property in the result will - /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// describes the way the value was determined. /// + /// + /// The Reason property in the result will also be included in analytics events, if you are + /// capturing detailed event data for this flag. + /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object @@ -102,23 +114,26 @@ public interface ILdMobileClient : ILdCommonClient /// /// Returns the JSON value of a feature flag for a given flag key, in an object that also - /// describes the way the value was determined. The Reason property in the result will - /// also be included in analytics events, if you are capturing detailed event data for this flag. + /// describes the way the value was determined. /// + /// + /// The Reason property in the result will also be included in analytics events, if you are + /// capturing detailed event data for this flag. + /// /// the unique feature key for the feature flag /// the default value of the flag /// an EvaluationDetail object EvaluationDetail JsonVariationDetail(string key, ImmutableJsonValue defaultValue); /// - /// Tracks that current user performed an event for the given JToken value and given event name. + /// Tracks that the current user performed an event for the given event name, with additional JSON data. /// /// the name of the event /// a JSON value containing additional data associated with the event void Track(string eventName, ImmutableJsonValue data); /// - /// Tracks that current user performed an event for the given event name. + /// Tracks that the current user performed an event for the given event name. /// /// the name of the event void Track(string eventName); @@ -179,18 +194,56 @@ public interface ILdMobileClient : ILdCommonClient event EventHandler FlagChanged; /// - /// Registers the user. - /// - /// This method will wait and block the current thread until the update processor has finished - /// initializing and received a response from the LaunchDarkly service. + /// Changes the current user. /// - /// the user to register + /// + /// This both sets the current user for the purpose of flag evaluations and also generates an analytics event to + /// tell LaunchDarkly about the user. + /// + /// Identify waits and blocks the current thread until the SDK has received feature flag values for the + /// new user from LaunchDarkly. If you do not want to wait, consider . + /// + /// the new user void Identify(User user); /// - /// Registers the user. + /// Changes the current user. /// + /// + /// This both sets the current user for the purpose of flag evaluations and also generates an analytics event to + /// tell LaunchDarkly about the user. + /// + /// IdentifyAsync is meant to be used from asynchronous code. It returns a Task that is resolved once the + /// SDK has received feature flag values for the new user from LaunchDarkly. + /// + /// + /// // Within asynchronous code, use await to wait for the task to be resolved + /// await client.IdentifyAsync(user); + /// + /// // Or, if you want to let the flag values be retrieved in the background instead of waiting: + /// Task.Run(() => client.IdentifyAsync(user)); + /// /// the user to register Task IdentifyAsync(User user); + + /// + /// Indicates which platform the SDK is built for. + /// + /// + /// This property is mainly useful for debugging. It does not indicate which platform you are actually running on, + /// but rather which variant of the SDK is currently in use. + /// + /// The LaunchDarkly.XamarinSdk package contains assemblies for multiple target platforms. In an Android + /// or iOS application, you will normally be using the Android or iOS variant of the SDK; that is done + /// automatically when you install the NuGet package. On all other platforms, you will get the .NET Standard + /// variant. + /// + /// The basic features of the SDK are the same in all of these variants; the difference is in platform-specific + /// behavior such as detecting when an application has gone into the background, detecting network connectivity, + /// and ensuring that code is executed on the UI thread if applicable for that platform. Therefore, if you find + /// that these platform-specific behaviors are not working correctly, you may want to check this property to + /// make sure you are not for some reason running the .NET Standard SDK on a phone. + /// + PlatformType PlatformType { get; } } } From ee9b86d100f198480007855e629d107e7a0928bf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:35:47 -0700 Subject: [PATCH 206/254] make AllFlags return immutable values; add PlatformType --- .../ILdMobileClient.cs | 25 +++++++++++-------- src/LaunchDarkly.XamarinSdk/LdClient.cs | 22 ++++++++++++++-- src/LaunchDarkly.XamarinSdk/ValueType.cs | 5 +++- .../LdClientEvaluationTests.cs | 4 +-- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index 1f251f45..d34e1e48 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -140,12 +140,12 @@ public interface ILdMobileClient : ILdCommonClient /// /// Gets or sets the online status of the client. - /// - /// The setter is equivalent to calling . If you are going from offline - /// to online, and you want to wait until the connection has been established, call - /// and then use await or call Wait() on - /// its return value. /// + /// + /// The setter is equivalent to calling ; if you are going from offline to + /// online, it does not wait until the connection has been established. If you want to wait for the + /// connection, call and then use await. + /// /// true if online; otherwise, false. bool Online { get; set; } @@ -153,17 +153,20 @@ public interface ILdMobileClient : ILdCommonClient /// Sets the client to be online or not. /// /// a Task - /// If set to true value. + /// true if the client should be online Task SetOnlineAsync(bool value); /// /// Returns a map from feature flag keys to feature flag values for the current user. - /// If the result of a flag's value would have returned the default variation, it will have a - /// null entry in the map. If the client is offline or has not been initialized, a null map will be returned. - /// This method will not send analytics events back to LaunchDarkly. /// - /// a map from feature flag keys to {@code JToken} for the current user - IDictionary AllFlags(); + /// + /// If the result of a flag's value would have returned the default variation, the value in the map will contain + /// null instead of a JToken. If the client is offline or has not been initialized, a null map will be returned. + /// + /// This method will not send analytics events back to LaunchDarkly. + /// + /// a map from feature flag keys to values for the current user + IDictionary AllFlags(); /// /// This event is triggered when the client has received an updated value for a feature flag. diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 9fed1adf..141fb0a3 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -391,7 +391,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => } /// - public IDictionary AllFlags() + public IDictionary AllFlags() { if (IsOffline()) { @@ -405,7 +405,10 @@ public IDictionary AllFlags() } return flagCacheManager.FlagsForUser(User) - .ToDictionary(p => p.Key, p => p.Value.value); + .ToDictionary(p => p.Key, p => new ImmutableJsonValue(p.Value.value)); + // Note that we are calling the ImmutableJsonValue constructor directly instead of using FromJToken() + // because we do not need it to deep-copy mutable values immediately - we know that *we* won't be + // modifying those values. It will deep-copy them if and when the application tries to access them. } /// @@ -571,6 +574,15 @@ public Version Version } } + /// + public PlatformType PlatformType + { + get + { + return PlatformSpecific.UserMetadata.PlatformType; + } + } + /// public event EventHandler FlagChanged { @@ -591,14 +603,20 @@ internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventA internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) { + Log.DebugFormat("Background mode is changing to {0}", args.IsInBackground); if (args.IsInBackground) { ClearUpdateProcessor(); _disableStreaming = true; if (Config.EnableBackgroundUpdating) { + Log.Debug("Background updating is enabled, starting polling processor"); await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); } + else + { + Log.Debug("Background updating is disabled"); + } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } else diff --git a/src/LaunchDarkly.XamarinSdk/ValueType.cs b/src/LaunchDarkly.XamarinSdk/ValueType.cs index b8e89cf4..fc20b299 100644 --- a/src/LaunchDarkly.XamarinSdk/ValueType.cs +++ b/src/LaunchDarkly.XamarinSdk/ValueType.cs @@ -75,8 +75,11 @@ private static ArgumentException BadTypeException() public static ValueType Json = new ValueType { - ValueFromJson = json => ImmutableJsonValue.FromJToken(json), + ValueFromJson = json => new ImmutableJsonValue(json), ValueToJson = value => value.AsJToken() + // Note that we are calling the ImmutableJsonValue constructor directly instead of using FromJToken() + // because we do not need it to deep-copy mutable values immediately - we know that *we* won't be + // modifying those values. It will deep-copy them if and when the application tries to access them. }; } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index fe7db1d5..6929cb19 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -206,8 +206,8 @@ public void AllFlagsReturnsAllFlagValues() { var result = client.AllFlags(); Assert.Equal(2, result.Count); - Assert.Equal(new JValue("a"), result["flag1"]); - Assert.Equal(new JValue("b"), result["flag2"]); + Assert.Equal("a", result["flag1"].AsString); + Assert.Equal("b", result["flag2"].AsString); } } From 7e840c04c71802d45501382ff1162b5456dd1885 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:43:21 -0700 Subject: [PATCH 207/254] more debug logging --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 141fb0a3..df4319b2 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -276,6 +276,7 @@ public async Task SetOnlineAsync(bool value) void MobileConnectionManager_ConnectionChanged(bool isOnline) { + Log.DebugFormat("Setting online to {0} due to a connectivity change event", isOnline); Online = isOnline; } From 1b428afa594529e7e3f254137e466e22771e243f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:52:13 -0700 Subject: [PATCH 208/254] change PlatformType to static, add tests --- .../ILdMobileClient.cs | 20 ----------- src/LaunchDarkly.XamarinSdk/LdClient.cs | 35 ++++++++++++++----- .../AndroidSpecificTests.cs | 6 ++++ .../IOsSpecificTests.cs | 6 ++++ 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs index d34e1e48..983034e4 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs @@ -228,25 +228,5 @@ public interface ILdMobileClient : ILdCommonClient /// /// the user to register Task IdentifyAsync(User user); - - /// - /// Indicates which platform the SDK is built for. - /// - /// - /// This property is mainly useful for debugging. It does not indicate which platform you are actually running on, - /// but rather which variant of the SDK is currently in use. - /// - /// The LaunchDarkly.XamarinSdk package contains assemblies for multiple target platforms. In an Android - /// or iOS application, you will normally be using the Android or iOS variant of the SDK; that is done - /// automatically when you install the NuGet package. On all other platforms, you will get the .NET Standard - /// variant. - /// - /// The basic features of the SDK are the same in all of these variants; the difference is in platform-specific - /// behavior such as detecting when an application has gone into the background, detecting network connectivity, - /// and ensuring that code is executed on the UI thread if applicable for that platform. Therefore, if you find - /// that these platform-specific behaviors are not working correctly, you may want to check this property to - /// make sure you are not for some reason running the .NET Standard SDK on a phone. - /// - PlatformType PlatformType { get; } } } diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index df4319b2..0f5cd331 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -74,6 +74,32 @@ public bool Online { var doNotAwaitResult = SetOnlineAsync(value); } + } + + /// + /// Indicates which platform the SDK is built for. + /// + /// + /// This property is mainly useful for debugging. It does not indicate which platform you are actually running on, + /// but rather which variant of the SDK is currently in use. + /// + /// The LaunchDarkly.XamarinSdk package contains assemblies for multiple target platforms. In an Android + /// or iOS application, you will normally be using the Android or iOS variant of the SDK; that is done + /// automatically when you install the NuGet package. On all other platforms, you will get the .NET Standard + /// variant. + /// + /// The basic features of the SDK are the same in all of these variants; the difference is in platform-specific + /// behavior such as detecting when an application has gone into the background, detecting network connectivity, + /// and ensuring that code is executed on the UI thread if applicable for that platform. Therefore, if you find + /// that these platform-specific behaviors are not working correctly, you may want to check this property to + /// make sure you are not for some reason running the .NET Standard SDK on a phone. + /// + public static PlatformType PlatformType + { + get + { + return PlatformSpecific.UserMetadata.PlatformType; + } } // private constructor prevents initialization of this class @@ -575,15 +601,6 @@ public Version Version } } - /// - public PlatformType PlatformType - { - get - { - return PlatformSpecific.UserMetadata.PlatformType; - } - } - /// public event EventHandler FlagChanged { diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs index 51490d24..337a3dc3 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs @@ -5,6 +5,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class AndroidSpecificTests { + [Fact] + public void SdkReturnsAndroidPlatformType() + { + Assert.Equal(PlatformType.Android, LdClient.PlatformType); + } + [Fact] public void UserHasOSAndDeviceAttributesForPlatform() { diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs index cf97ed9f..63bbb60d 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -5,6 +5,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class IOsSpecificTests { + [Fact] + public void SdkReturnsIOsPlatformType() + { + Assert.Equal(PlatformType.IOs, LdClient.PlatformType); + } + [Fact] public void UserHasOSAndDeviceAttributesForPlatform() { From aabdc9ebc4029044472fcd9901d62b26de25cd3b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 11:52:40 -0700 Subject: [PATCH 209/254] the IDE keeps wanting to refresh this file so let's just commit the changes --- .../Resources/Resource.designer.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs index f352a1ff..bdd99699 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs @@ -26,6 +26,107 @@ static Resource() public static void UpdateIdValues() { + global::LaunchDarkly.XamarinSdk.Resource.Attribute.font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.font; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderAuthority; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderCerts; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchStrategy; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchTimeout; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderPackage; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderQuery; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontWeight; + global::LaunchDarkly.XamarinSdk.Resource.Boolean.abc_action_bar_embed_tabs = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_action_bar_embed_tabs; + global::LaunchDarkly.XamarinSdk.Resource.Color.notification_action_color_filter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_action_color_filter; + global::LaunchDarkly.XamarinSdk.Resource.Color.notification_icon_bg_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_icon_bg_color; + global::LaunchDarkly.XamarinSdk.Resource.Color.ripple_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.ripple_material_light; + global::LaunchDarkly.XamarinSdk.Resource.Color.secondary_text_default_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_default_material_light; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_horizontal_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_vertical_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_horizontal_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_vertical_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_control_corner_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_control_corner_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_icon_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_text_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_big_circle_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_big_circle_margin; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_content_margin_start = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_content_margin_start; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_height; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_width; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_main_column_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_main_column_padding_top; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_media_narrow_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_media_narrow_margin; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_icon_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_side_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_side_padding_top; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_background_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_background_padding; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_size_as_large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_size_as_large; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_subtext_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_subtext_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad_large_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad_large_text; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_action_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_action_background; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_normal; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_pressed; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal_pressed; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_icon_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_icon_background; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_low_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_low_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_tile_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_tile_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notify_panel_notification_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notify_panel_notification_icon_bg; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_container; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_divider; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_image = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_image; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_text; + global::LaunchDarkly.XamarinSdk.Resource.Id.actions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.actions; + global::LaunchDarkly.XamarinSdk.Resource.Id.async = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.async; + global::LaunchDarkly.XamarinSdk.Resource.Id.blocking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.blocking; + global::LaunchDarkly.XamarinSdk.Resource.Id.chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.chronometer; + global::LaunchDarkly.XamarinSdk.Resource.Id.forever = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.forever; + global::LaunchDarkly.XamarinSdk.Resource.Id.icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon; + global::LaunchDarkly.XamarinSdk.Resource.Id.icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon_group; + global::LaunchDarkly.XamarinSdk.Resource.Id.info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.info; + global::LaunchDarkly.XamarinSdk.Resource.Id.italic = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.italic; + global::LaunchDarkly.XamarinSdk.Resource.Id.line1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line1; + global::LaunchDarkly.XamarinSdk.Resource.Id.line3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line3; + global::LaunchDarkly.XamarinSdk.Resource.Id.normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.normal; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_background; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column_container; + global::LaunchDarkly.XamarinSdk.Resource.Id.right_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_icon; + global::LaunchDarkly.XamarinSdk.Resource.Id.right_side = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_side; + global::LaunchDarkly.XamarinSdk.Resource.Id.tag_transition_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.tag_transition_group; + global::LaunchDarkly.XamarinSdk.Resource.Id.text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text; + global::LaunchDarkly.XamarinSdk.Resource.Id.text2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text2; + global::LaunchDarkly.XamarinSdk.Resource.Id.time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.time; + global::LaunchDarkly.XamarinSdk.Resource.Id.title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.title; + global::LaunchDarkly.XamarinSdk.Resource.Integer.status_bar_notification_info_maxnum = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.status_bar_notification_info_maxnum; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action_tombstone = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action_tombstone; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_custom_big = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_custom_big; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_icon_group; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_chronometer; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_time; + global::LaunchDarkly.XamarinSdk.Resource.String.status_bar_notification_info_overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.status_bar_notification_info_overflow; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Info; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Line2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Line2; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Time; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Title; + global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionContainer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionContainer; + global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionText; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderAuthority; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderCerts; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchStrategy; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchTimeout; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderPackage; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderQuery; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_font; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontWeight; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_font; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontWeight; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_in; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_out; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_grow_fade_in_from_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_grow_fade_in_from_bottom; From dd52e7473d3738891488dc8a08b95d14d63a904c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 18:44:15 -0700 Subject: [PATCH 210/254] use immutable JSON values in FlagChangedEvent --- .../FlagChangedEvent.cs | 19 ++++++++++++------- .../PlatformSpecific/Preferences.android.cs | 1 - .../FlagChangedEventTests.cs | 12 ++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index 8ef352c1..59b5282e 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Common.Logging; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin @@ -19,7 +20,7 @@ public class FlagChangedEventArgs /// The updated value of the flag for the current user. /// /// - /// Since flag values can be of any JSON type, this property is a . The properties + /// Since flag values can be of any JSON type, this property is an . The properties /// , , etc. are shortcuts for accessing its value with a /// specific data type. /// @@ -39,12 +40,12 @@ public class FlagChangedEventArgs /// would have specified "xyz" as a default value when evaluating the flag, so NewValue will simply /// be null. /// - public JToken NewValue { get; private set; } + public ImmutableJsonValue NewValue { get; private set; } /// /// The last known value of the flag for the current user prior to the update. /// - public JToken OldValue { get; private set; } + public ImmutableJsonValue OldValue { get; private set; } /// /// True if the flag was completely removed from the environment. @@ -77,11 +78,12 @@ public class FlagChangedEventArgs private T AsType(ValueType valueType, T defaultValue) { - if (NewValue != null) + var jt = NewValue.AsJToken(); + if (jt != null) { try { - return valueType.ValueFromJson(NewValue); + return valueType.ValueFromJson(jt); } catch (ArgumentException) {} } @@ -91,8 +93,11 @@ private T AsType(ValueType valueType, T defaultValue) internal FlagChangedEventArgs(string key, JToken newValue, JToken oldValue, bool flagWasDeleted) { Key = key; - NewValue = newValue; - OldValue = oldValue; + // Note that we're calling the ImmutableJsonValue constructor directly instead of using FromJToken(), + // because this is an internal value that we know we will not be modifying even if it is mutable. + // ImmutableJsonValue will take care of deep-copying the value if the application requests it. + NewValue = new ImmutableJsonValue(newValue); + OldValue = new ImmutableJsonValue(oldValue); FlagWasDeleted = flagWasDeleted; } } diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs index c958bb64..0325457b 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Preferences.android.cs @@ -95,7 +95,6 @@ static string PlatformGet(string key, string defaultValue, string sharedName) { lock (locker) { - object value = null; using (var sharedPreferences = GetSharedPreferences(sharedName)) { return sharedPreferences.GetString(key, defaultValue); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs index 01dcc176..a404b3c5 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs @@ -35,8 +35,8 @@ public void CanRegisterListeners() Assert.Equal(INT_FLAG, event2a.Key); Assert.Equal(7, event1a.NewIntValue); Assert.Equal(7, event2a.NewIntValue); - Assert.Equal(6, event1a.OldValue); - Assert.Equal(6, event2a.OldValue); + Assert.Equal(6, event1a.OldValue.AsInt); + Assert.Equal(6, event2a.OldValue.AsInt); Assert.False(event1a.FlagWasDeleted); Assert.False(event2a.FlagWasDeleted); @@ -44,8 +44,8 @@ public void CanRegisterListeners() Assert.Equal(DOUBLE_FLAG, event2b.Key); Assert.Equal(10.5, event1b.NewFloatValue); Assert.Equal(10.5, event2b.NewFloatValue); - Assert.Equal(9.5, event1b.OldValue); - Assert.Equal(9.5, event2b.OldValue); + Assert.Equal(9.5, event1b.OldValue.AsFloat); + Assert.Equal(9.5, event2b.OldValue.AsFloat); Assert.False(event1b.FlagWasDeleted); Assert.False(event2b.FlagWasDeleted); } @@ -66,7 +66,7 @@ public void CanUnregisterListeners() var e = listener2.Await(); Assert.Equal(INT_FLAG, e.Key); Assert.Equal(7, e.NewIntValue); - Assert.Equal(6, e.OldValue); + Assert.Equal(6, e.OldValue.AsInt); // This is pretty hacky, but since we're testing for the *lack* of a call, there's no signal we can wait on. Thread.Sleep(100); @@ -85,7 +85,7 @@ public void ListenerGetsUpdatedWhenManagerFlagDeleted() var e = listener.Await(); Assert.Equal(INT_FLAG, e.Key); - Assert.Equal(1, e.OldValue); + Assert.Equal(1, e.OldValue.AsInt); Assert.True(e.FlagWasDeleted); } From 850ce75f40a124b334ef2a2db1c53634688ccba5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jul 2019 22:39:07 -0700 Subject: [PATCH 211/254] Remove redundant convenience methods --- .../FlagChangedEvent.cs | 48 ++----------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index 59b5282e..3635d21a 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -20,9 +20,9 @@ public class FlagChangedEventArgs /// The updated value of the flag for the current user. /// /// - /// Since flag values can be of any JSON type, this property is an . The properties - /// , , etc. are shortcuts for accessing its value with a - /// specific data type. + /// Since flag values can be of any JSON type, this property is an . You + /// can use convenience properties of ImmutableJsonValue such as AsBool to convert it to a + /// primitive type, or AsJToken() for complex types. /// /// Flag evaluations always produce non-null values, but this property could still be null if the flag was /// completely deleted or if it could not be evaluated due to an error of some kind. @@ -33,12 +33,12 @@ public class FlagChangedEventArgs /// in the method call: /// /// - /// client.StringVariation("feature1", "xyz"); + /// client.StringVariation("feature1", "xyz"); /// /// /// But when a FlagChangedEvent is sent for the deletion of the flag, it has no way to know that you /// would have specified "xyz" as a default value when evaluating the flag, so NewValue will simply - /// be null. + /// contain a null. /// public ImmutableJsonValue NewValue { get; private set; } @@ -52,44 +52,6 @@ public class FlagChangedEventArgs /// public bool FlagWasDeleted { get; private set; } - /// - /// Shortcut for converting to a bool. Returns false if the value is null or is not a - /// boolean (will never throw an exception). - /// - public bool NewBoolValue => AsType(ValueTypes.Bool, false); - - /// - /// Shortcut for converting to a string. Returns null if the value is null or is not a - /// string (will never throw an exception). - /// - public string NewStringValue => AsType(ValueTypes.String, null); - - /// - /// Shortcut for converting to an int. Returns 0 if the value is null or is not - /// numeric (will never throw an exception). - /// - public int NewIntValue => AsType(ValueTypes.Int, 0); - - /// - /// Shortcut for converting to a float. Returns 0 if the value is null or is not - /// numeric (will never throw an exception). - /// - public float NewFloatValue => AsType(ValueTypes.Float, 0); - - private T AsType(ValueType valueType, T defaultValue) - { - var jt = NewValue.AsJToken(); - if (jt != null) - { - try - { - return valueType.ValueFromJson(jt); - } - catch (ArgumentException) {} - } - return defaultValue; - } - internal FlagChangedEventArgs(string key, JToken newValue, JToken oldValue, bool flagWasDeleted) { Key = key; From f986012dfb53a1a4c4d5423ea14c345787bca0b7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 1 Aug 2019 09:36:49 -0700 Subject: [PATCH 212/254] fix tests --- .../FlagCacheManagerTests.cs | 4 ++-- .../FlagChangedEventTests.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs index 9c8cef20..f568ec49 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagCacheManagerTests.cs @@ -81,7 +81,7 @@ public void UpdateFlagSendsFlagChangeEvent() var e = listener.Await(); Assert.Equal("int-flag", e.Key); - Assert.Equal(7, e.NewIntValue); + Assert.Equal(7, e.NewValue.AsInt); Assert.False(e.FlagWasDeleted); } @@ -113,7 +113,7 @@ public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() var e = listener.Await(); Assert.Equal("int-flag", e.Key); - Assert.Equal(5, e.NewIntValue); + Assert.Equal(5, e.NewValue.AsInt); Assert.False(e.FlagWasDeleted); } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs index a404b3c5..c56ff54d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FlagChangedEventTests.cs @@ -33,8 +33,8 @@ public void CanRegisterListeners() Assert.Equal(INT_FLAG, event1a.Key); Assert.Equal(INT_FLAG, event2a.Key); - Assert.Equal(7, event1a.NewIntValue); - Assert.Equal(7, event2a.NewIntValue); + Assert.Equal(7, event1a.NewValue.AsInt); + Assert.Equal(7, event2a.NewValue.AsInt); Assert.Equal(6, event1a.OldValue.AsInt); Assert.Equal(6, event2a.OldValue.AsInt); Assert.False(event1a.FlagWasDeleted); @@ -42,8 +42,8 @@ public void CanRegisterListeners() Assert.Equal(DOUBLE_FLAG, event1b.Key); Assert.Equal(DOUBLE_FLAG, event2b.Key); - Assert.Equal(10.5, event1b.NewFloatValue); - Assert.Equal(10.5, event2b.NewFloatValue); + Assert.Equal(10.5, event1b.NewValue.AsFloat); + Assert.Equal(10.5, event2b.NewValue.AsFloat); Assert.Equal(9.5, event1b.OldValue.AsFloat); Assert.Equal(9.5, event2b.OldValue.AsFloat); Assert.False(event1b.FlagWasDeleted); @@ -65,7 +65,7 @@ public void CanUnregisterListeners() var e = listener2.Await(); Assert.Equal(INT_FLAG, e.Key); - Assert.Equal(7, e.NewIntValue); + Assert.Equal(7, e.NewValue.AsInt); Assert.Equal(6, e.OldValue.AsInt); // This is pretty hacky, but since we're testing for the *lack* of a call, there's no signal we can wait on. From 590da3413bb98405cff4cceb051dcb435ff77bec Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 5 Aug 2019 12:32:09 -0700 Subject: [PATCH 213/254] remove deprecated stuff, update for CommonSdk 3.x API changes (#63) * remove deprecated stuff, update for CommonSdk 3.x API changes * rename ILdMobileClient to ILdClient * rename interface (missed commit) * rm IMobileConfiguration, make config classes sealed, rm SdkKey alias, update comments --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 208 ++++++++++++------ .../ConfigurationBuilder.cs | 66 +++--- src/LaunchDarkly.XamarinSdk/Factory.cs | 4 +- .../FeatureFlagRequestor.cs | 6 +- .../FlagChangedEvent.cs | 7 +- .../{ILdMobileClient.cs => ILdClient.cs} | 19 +- .../IMobileConfiguration.cs | 66 ------ .../LaunchDarkly.XamarinSdk.csproj | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 9 +- .../MobileStreamingProcessor.cs | 6 +- src/LaunchDarkly.XamarinSdk/ValueType.cs | 5 +- .../LDClientEndToEndTests.cs | 2 +- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LdClientEvaluationTests.cs | 7 +- .../LdClientEventTests.cs | 2 +- 15 files changed, 215 insertions(+), 196 deletions(-) rename src/LaunchDarkly.XamarinSdk/{ILdMobileClient.cs => ILdClient.cs} (95%) delete mode 100644 src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index e684369f..e9deb58e 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Net.Http; using LaunchDarkly.Client; +using LaunchDarkly.Common; namespace LaunchDarkly.Xamarin { @@ -14,7 +15,7 @@ namespace LaunchDarkly.Xamarin /// , or using a builder pattern with /// or . /// - public class Configuration : IMobileConfiguration + public sealed class Configuration { private readonly bool _allAttributesPrivate; private readonly TimeSpan _backgroundPollingInterval; @@ -24,7 +25,6 @@ public class Configuration : IMobileConfiguration private readonly bool _evaluationReasons; private readonly TimeSpan _eventFlushInterval; private readonly int _eventCapacity; - private readonly int _eventSamplingInterval; private readonly Uri _eventsUri; private readonly HttpClientHandler _httpClientHandler; private readonly TimeSpan _httpClientTimeout; @@ -53,13 +53,23 @@ public class Configuration : IMobileConfiguration /// /// Whether or not user attributes (other than the key) should be private (not sent to - /// the LaunchDarkly server). If this is true, all of the user attributes will be private, - /// not just the attributes specified with the AndPrivate... methods on the - /// object. By default, this is false. + /// the LaunchDarkly server). /// + /// + /// If this is true, all of the user attributes will be private, not just the attributes specified with + /// or with the + /// method in . + /// + /// By default, this is false. + /// public bool AllAttributesPrivate => _allAttributesPrivate; - /// + /// + /// The interval between feature flag updates when the application is running in the background. + /// + /// + /// This is only relevant on mobile platforms. + /// public TimeSpan BackgroundPollingInterval => _backgroundPollingInterval; /// @@ -67,47 +77,48 @@ public class Configuration : IMobileConfiguration /// public Uri BaseUri => _baseUri; - /// + /// + /// The connection timeout to the LaunchDarkly server. + /// public TimeSpan ConnectionTimeout { get; internal set; } - /// + /// + /// Whether to enable feature flag updates when the application is running in the background. + /// + /// + /// This is only relevant on mobile platforms. + /// public bool EnableBackgroundUpdating => _enableBackgroundUpdating; - /// - public bool EvaluationReasons => _evaluationReasons; - /// - /// The time between flushes of the event buffer. Decreasing the flush interval means - /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. + /// True if LaunchDarkly should provide additional information about how flag values were + /// calculated. /// - public TimeSpan EventFlushInterval => _eventFlushInterval; + /// + /// The additional information will then be available through the client's "detail" + /// methods such as . Since this + /// increases the size of network requests, such information is not sent unless you set this option + /// to true. + /// + public bool EvaluationReasons => _evaluationReasons; /// - /// The capacity of the events buffer. The client buffers up to this many events in - /// memory before flushing. If the capacity is exceeded before the buffer is flushed, - /// events will be discarded. Increasing the capacity means that events are less likely - /// to be discarded, at the cost of consuming more memory. + /// The capacity of the events buffer. /// + /// + /// The client buffers up to this many events in memory before flushing. If the capacity is exceeded + /// before the buffer is flushed, events will be discarded. Increasing the capacity means that events + /// are less likely to be discarded, at the cost of consuming more memory. + /// public int EventCapacity => _eventCapacity; /// - /// Deprecated name for . - /// - [Obsolete] - public int EventQueueCapacity => EventCapacity; - - /// - /// Deprecated name for . - /// - [Obsolete] - public TimeSpan EventQueueFrequency => EventFlushInterval; - - /// - /// Enables event sampling if non-zero. When set to the default of zero, all analytics events are - /// sent back to LaunchDarkly. When greater than zero, there is a 1 in EventSamplingInterval - /// chance that events will be sent (example: if the interval is 20, on average 5% of events will be sent). + /// The time between flushes of the event buffer. /// - public int EventSamplingInterval => _eventSamplingInterval; + /// + /// Decreasing the flush interval means that the event buffer is less likely to reach capacity. + /// + public TimeSpan EventFlushInterval => _eventFlushInterval; /// /// The base URL of the LaunchDarkly analytics event server. @@ -124,21 +135,29 @@ public class Configuration : IMobileConfiguration /// public TimeSpan HttpClientTimeout => _httpClientTimeout; - /// - /// True if full user details should be included in every analytics event. The default is false (events will - /// only include the user key, except for one "index" event that provides the full details for the user). + /// + /// Sets whether to include full user details in every analytics event. /// + /// + /// The default is false: events will only include the user key, except for one "index" event that + /// provides the full details for the user. + /// public bool InlineUsersInEvents => _inlineUsersInEvents; /// - /// Whether or not the streaming API should be used to receive flag updates. This is true by default. - /// Streaming should only be disabled on the advice of LaunchDarkly support. + /// Whether or not the streaming API should be used to receive flag updates. /// + /// + /// This is true by default. Streaming should only be disabled on the advice of LaunchDarkly support. + /// public bool IsStreamingEnabled => _isStreamingEnabled; /// - /// The Mobile key for your LaunchDarkly environment. + /// The key for your LaunchDarkly environment. /// + /// + /// This should be the "mobile key" field for the environment on your LaunchDarkly dashboard. + /// public string MobileKey => _mobileKey; /// @@ -146,55 +165,70 @@ public class Configuration : IMobileConfiguration /// public bool Offline => _offline; - /// + /// + /// Whether the SDK should save flag values for each user in persistent storage, so they will be + /// immediately available the next time the SDK is started for the same user. + /// + /// + /// The default is true. + /// public bool PersistFlagValues => _persistFlagValues; /// - /// Set the polling interval (when streaming is disabled). The default value is 30 seconds. + /// The polling interval (when streaming is disabled). /// public TimeSpan PollingInterval => _pollingInterval; /// - /// Marks a set of attribute names as private. Any users sent to LaunchDarkly with this - /// configuration active will have attributes with these names removed, even if you did - /// not use the AndPrivate... methods on the object. + /// Attribute names that have been marked as private for all users. /// + /// + /// Any users sent to LaunchDarkly with this configuration active will have attributes with this name + /// removed, even if you did not use the + /// method in . + /// public ISet PrivateAttributeNames => _privateAttributeNames; /// - /// The timeout when reading data from the EventSource API. The default value is 5 minutes. + /// The timeout when reading data from the streaming connection. /// public TimeSpan ReadTimeout => _readTimeout; /// - /// The reconnect base time for the streaming connection.The streaming connection - /// uses an exponential backoff algorithm (with jitter) for reconnects, but will start the - /// backoff with a value near the value specified here. The default value is 1 second. + /// The reconnect base time for the streaming connection. /// + /// + /// The streaming connection uses an exponential backoff algorithm (with jitter) for reconnects, but + /// will start the backoff with a value near the value specified here. The default value is 1 second. + /// public TimeSpan ReconnectTime => _reconnectTime; - /// - /// Alternate name for . - /// - public string SdkKey => MobileKey; - /// /// The base URL of the LaunchDarkly streaming server. /// public Uri StreamUri => _streamUri; - /// + /// + /// Whether to use the HTTP REPORT method for feature flag requests. + /// + /// + /// By default, polling and streaming connections are made with the GET method, witht the user data + /// encoded into the request URI. Using REPORT allows the user data to be sent in the request body instead. + /// However, some network gateways do not support REPORT. + /// public bool UseReport => _useReport; - /// - /// The number of user keys that the event processor can remember at any one time, so that - /// duplicate user details will not be sent in analytics events. - /// + /// + /// The number of user keys that the event processor can remember at any one time. + /// + /// + /// The event processor keeps track of recently seen user keys so that duplicate user details will not + /// be sent in analytics events. + /// public int UserKeysCapacity => _userKeysCapacity; /// - /// The interval at which the event processor will reset its set of known user keys. The - /// default value is five minutes. + /// The interval at which the event processor will reset its set of known user keys. /// public TimeSpan UserKeysFlushInterval => _userKeysFlushInterval; @@ -233,7 +267,7 @@ public static Configuration Default(string mobileKey) return Builder(mobileKey).Build(); } - /// /// Creates a for constructing a configuration object using a fluent syntax. /// /// /// This is the only method for building a Configuration if you are setting properties /// besides the MobileKey. The ConfigurationBuilder has methods for setting any number of /// properties, after which you call to get the resulting /// Configuration instance. /// /// /// /// var config = Configuration.Builder("my-sdk-key") /// .EventQueueFrequency(TimeSpan.FromSeconds(90)) /// .StartWaitTime(TimeSpan.FromSeconds(5)) /// .Build(); /// /// /// the SDK key for your LaunchDarkly environment + /// /// Creates a ConfigurationBuilder for constructing a configuration object using a fluent syntax. /// /// /// This is the only method for building a Configuration if you are setting properties /// besides the MobileKey. The ConfigurationBuilder has methods for setting any number of /// properties, after which you call to get the resulting /// Configuration instance. /// /// ///
        ///     var config = Configuration.Builder("my-sdk-key")
        ///         .EventQueueFrequency(TimeSpan.FromSeconds(90))
        ///         .StartWaitTime(TimeSpan.FromSeconds(5))
        ///         .Build();
        /// 
///
/// the mobile SDK key for your LaunchDarkly environment /// a builder object public static IConfigurationBuilder Builder(string mobileKey) { if (String.IsNullOrEmpty(mobileKey)) { @@ -241,9 +275,20 @@ public static Configuration Default(string mobileKey) } return new ConfigurationBuilder(mobileKey); } + /// + /// Exposed for test code that needs to access the internal methods of ConfigurationBuilder that + /// are not in . + /// + /// the mobile SDK key + /// a builder object internal static ConfigurationBuilder BuilderInternal(string mobileKey) { return new ConfigurationBuilder(mobileKey); } + /// + /// Creates a ConfigurationBuilder starting with the properties of an existing Configuration. + /// + /// the configuration to copy + /// a builder object public static IConfigurationBuilder Builder(Configuration fromConfiguration) { return new ConfigurationBuilder(fromConfiguration); @@ -251,7 +296,7 @@ public static IConfigurationBuilder Builder(Configuration fromConfiguration) internal Configuration(ConfigurationBuilder builder) { - _allAttributesPrivate = builder._allAttributesPrivate; _backgroundPollingInterval = builder._backgroundPollingInterval; _baseUri = builder._baseUri; _connectionTimeout = builder._connectionTimeout; _enableBackgroundUpdating = builder._enableBackgroundUpdating; _evaluationReasons = builder._evaluationReasons; _eventFlushInterval = builder._eventFlushInterval; _eventCapacity = builder._eventCapacity; _eventSamplingInterval = builder._eventSamplingInterval; _eventsUri = builder._eventsUri; _httpClientHandler = builder._httpClientHandler; _httpClientTimeout = builder._httpClientTimeout; _inlineUsersInEvents = builder._inlineUsersInEvents; _isStreamingEnabled = builder._isStreamingEnabled; _mobileKey = builder._mobileKey; _offline = builder._offline; _persistFlagValues = builder._persistFlagValues; _pollingInterval = builder._pollingInterval; _privateAttributeNames = builder._privateAttributeNames is null ? null : builder._privateAttributeNames.ToImmutableHashSet(); _readTimeout = builder._readTimeout; _reconnectTime = builder._reconnectTime; _streamUri = builder._streamUri; _useReport = builder._useReport; _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; + _allAttributesPrivate = builder._allAttributesPrivate; _backgroundPollingInterval = builder._backgroundPollingInterval; _baseUri = builder._baseUri; _connectionTimeout = builder._connectionTimeout; _enableBackgroundUpdating = builder._enableBackgroundUpdating; _evaluationReasons = builder._evaluationReasons; _eventFlushInterval = builder._eventFlushInterval; _eventCapacity = builder._eventCapacity; _eventsUri = builder._eventsUri; _httpClientHandler = builder._httpClientHandler; _httpClientTimeout = builder._httpClientTimeout; _inlineUsersInEvents = builder._inlineUsersInEvents; _isStreamingEnabled = builder._isStreamingEnabled; _mobileKey = builder._mobileKey; _offline = builder._offline; _persistFlagValues = builder._persistFlagValues; _pollingInterval = builder._pollingInterval; _privateAttributeNames = builder._privateAttributeNames is null ? null : builder._privateAttributeNames.ToImmutableHashSet(); _readTimeout = builder._readTimeout; _reconnectTime = builder._reconnectTime; _streamUri = builder._streamUri; _useReport = builder._useReport; _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; _connectionManager = builder._connectionManager; _deviceInfo = builder._deviceInfo; @@ -261,5 +306,42 @@ internal Configuration(ConfigurationBuilder builder) _persistentStorage = builder._persistentStorage; _updateProcessorFactory = builder._updateProcessorFactory; } + + internal IEventProcessorConfiguration EventProcessorConfiguration => new EventProcessorAdapter { Config = this }; + internal IHttpRequestConfiguration HttpRequestConfiguration => new HttpRequestAdapter { Config = this }; + internal IStreamManagerConfiguration StreamManagerConfiguration => new StreamManagerAdapter { Config = this }; + + private class EventProcessorAdapter : IEventProcessorConfiguration + { + internal Configuration Config { get; set; } + public bool AllAttributesPrivate => Config.AllAttributesPrivate; + public int EventCapacity => Config.EventCapacity; + public TimeSpan EventFlushInterval => Config.EventFlushInterval; + public Uri EventsUri => Config.EventsUri; + public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; + public bool InlineUsersInEvents => Config.InlineUsersInEvents; + public ISet PrivateAttributeNames => Config.PrivateAttributeNames; + public TimeSpan ReadTimeout => Config.ReadTimeout; + public TimeSpan ReconnectTime => Config.ReconnectTime; + public int UserKeysCapacity => Config.UserKeysCapacity; + public TimeSpan UserKeysFlushInterval => Config.UserKeysFlushInterval; + } + + private class HttpRequestAdapter : IHttpRequestConfiguration + { + internal Configuration Config { get; set; } + public string HttpAuthorizationKey => Config.MobileKey; + public HttpClientHandler HttpClientHandler => Config.HttpClientHandler; + } + + private class StreamManagerAdapter : IStreamManagerConfiguration + { + internal Configuration Config { get; set; } + public string HttpAuthorizationKey => Config.MobileKey; + public HttpClientHandler HttpClientHandler => Config.HttpClientHandler; + public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; + public TimeSpan ReadTimeout => Config.ReadTimeout; + public TimeSpan ReconnectTime => Config.ReconnectTime; + } } } diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index db585ec5..e1fcc82f 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Net.Http; using Common.Logging; using LaunchDarkly.Client; @@ -36,16 +35,21 @@ public interface IConfigurationBuilder ///
/// /// If this is true, all of the user attributes will be private, not just the attributes specified with - /// on the object. + /// or with the + /// method with . + /// /// By default, this is false. /// /// true if all attributes should be private /// the same builder IConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate); - /// - /// Sets the interval for background polling. - /// + /// + /// Sets the interval between feature flag updates when the application is running in the background. + /// + /// + /// This is only relevant on mobile platforms. + /// /// the background polling interval /// the same builder IConfigurationBuilder BackgroundPollingInterval(TimeSpan backgroundPollingInterval); @@ -63,7 +67,7 @@ public interface IConfigurationBuilder ///
/// /// The additional information will then be available through the client's "detail" - /// methods such as . Since this + /// methods such as . Since this /// increases the size of network requests, such information is not sent unless you set this option /// to true. /// @@ -94,18 +98,6 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval); - /// - /// Enables event sampling if non-zero. - /// - /// - /// When set to the default of zero, all analytics events are sent back to LaunchDarkly. When greater - /// than zero, there is a 1 in EventSamplingInterval chance that events will be sent (example: - /// if the interval is 20, on average 5% of events will be sent). - /// - /// the sampling interval - /// the same builder - IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval); - /// /// Sets the base URL of the LaunchDarkly analytics event server. /// @@ -147,9 +139,13 @@ public interface IConfigurationBuilder /// true if the streaming API should be used /// the same builder IConfigurationBuilder IsStreamingEnabled(bool isStreamingEnabled); + /// - /// + /// Sets the key for your LaunchDarkly environment. /// + /// + /// This should be the "mobile key" field for the environment on your LaunchDarkly dashboard. + /// /// /// the same builder IConfigurationBuilder MobileKey(string mobileKey); @@ -163,8 +159,11 @@ public interface IConfigurationBuilder /// /// Sets whether the SDK should save flag values for each user in persistent storage, so they will be - /// immediately available the next time the SDK is started for the same user. The default is . + /// immediately available the next time the SDK is started for the same user. /// + /// + /// The default is true. + /// /// true to save flag values /// the same Configuration instance /// the same builder @@ -181,16 +180,18 @@ public interface IConfigurationBuilder IConfigurationBuilder PollingInterval(TimeSpan pollingInterval); /// - /// Marks an attribute name as private. + /// Marks an attribute name as private for all users. /// /// /// Any users sent to LaunchDarkly with this configuration active will have attributes with this name - /// removed, even if you did not use the AndPrivate... methods on the object. + /// removed, even if you did not use the + /// method in . + /// /// You may call this method repeatedly to mark multiple attributes as private. /// - /// the attribute name + /// the attribute name /// the same builder - IConfigurationBuilder PrivateAttribute(string privateAtributeName); + IConfigurationBuilder PrivateAttribute(string privateAttributeName); /// /// Sets the timeout when reading data from the streaming connection. @@ -221,8 +222,13 @@ public interface IConfigurationBuilder IConfigurationBuilder StreamUri(Uri streamUri); /// - /// Determines whether to use the REPORT method for networking requests. + /// Sets whether to use the HTTP REPORT method for feature flag requests. /// + /// + /// By default, polling and streaming connections are made with the GET method, witht the user data + /// encoded into the request URI. Using REPORT allows the user data to be sent in the request body instead. + /// However, some network gateways do not support REPORT. + /// /// whether to use REPORT mode /// the same builder IConfigurationBuilder UseReport(bool useReport); @@ -249,7 +255,7 @@ public interface IConfigurationBuilder IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterval); } - internal class ConfigurationBuilder : IConfigurationBuilder + internal sealed class ConfigurationBuilder : IConfigurationBuilder { private static readonly ILog Log = LogManager.GetLogger(typeof(ConfigurationBuilder)); @@ -261,7 +267,6 @@ internal class ConfigurationBuilder : IConfigurationBuilder internal bool _evaluationReasons = false; internal int _eventCapacity = Configuration.DefaultEventCapacity; internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; - internal int _eventSamplingInterval = 0; internal Uri _eventsUri = Configuration.DefaultEventsUri; internal HttpClientHandler _httpClientHandler = new HttpClientHandler(); internal TimeSpan _httpClientTimeout = Configuration.DefaultHttpClientTimeout; @@ -303,7 +308,6 @@ internal ConfigurationBuilder(Configuration copyFrom) _evaluationReasons = copyFrom.EvaluationReasons; _eventCapacity = copyFrom.EventCapacity; _eventFlushInterval = copyFrom.EventFlushInterval; - _eventSamplingInterval = copyFrom.EventSamplingInterval; _eventsUri = copyFrom.EventsUri; _httpClientHandler = copyFrom.HttpClientHandler; _httpClientTimeout = copyFrom.HttpClientTimeout; @@ -384,12 +388,6 @@ public IConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval) return this; } - public IConfigurationBuilder EventSamplingInterval(int eventSamplingInterval) - { - _eventSamplingInterval = eventSamplingInterval; - return this; - } - public IConfigurationBuilder EventsUri(Uri eventsUri) { _eventsUri = eventsUri; diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index 7a8d6e59..043f854e 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -72,8 +72,8 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration return new NullEventProcessor(); } - HttpClient httpClient = Util.MakeHttpClient(configuration, MobileClientEnvironment.Instance); - return new DefaultEventProcessor(configuration, null, httpClient, Constants.EVENTS_PATH); + HttpClient httpClient = Util.MakeHttpClient(configuration.HttpRequestConfiguration, MobileClientEnvironment.Instance); + return new DefaultEventProcessor(configuration.EventProcessorConfiguration, null, httpClient, Constants.EVENTS_PATH); } internal static IPersistentStorage CreatePersistentStorage(Configuration configuration) diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs index 832b90b2..7e5afaa5 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs @@ -35,15 +35,15 @@ internal class FeatureFlagRequestor : IFeatureFlagRequestor private static readonly ILog Log = LogManager.GetLogger(typeof(FeatureFlagRequestor)); private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); - private readonly IMobileConfiguration _configuration; + private readonly Configuration _configuration; private readonly User _currentUser; private readonly HttpClient _httpClient; private volatile EntityTagHeaderValue _etag; - internal FeatureFlagRequestor(IMobileConfiguration configuration, User user) + internal FeatureFlagRequestor(Configuration configuration, User user) { this._configuration = configuration; - this._httpClient = Util.MakeHttpClient(configuration, MobileClientEnvironment.Instance); + this._httpClient = Util.MakeHttpClient(configuration.HttpRequestConfiguration, MobileClientEnvironment.Instance); this._currentUser = user; } diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index 3635d21a..b59f53e9 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -55,11 +55,8 @@ public class FlagChangedEventArgs internal FlagChangedEventArgs(string key, JToken newValue, JToken oldValue, bool flagWasDeleted) { Key = key; - // Note that we're calling the ImmutableJsonValue constructor directly instead of using FromJToken(), - // because this is an internal value that we know we will not be modifying even if it is mutable. - // ImmutableJsonValue will take care of deep-copying the value if the application requests it. - NewValue = new ImmutableJsonValue(newValue); - OldValue = new ImmutableJsonValue(oldValue); + NewValue = ImmutableJsonValue.FromSafeValue(newValue); + OldValue = ImmutableJsonValue.FromSafeValue(oldValue); FlagWasDeleted = flagWasDeleted; } } diff --git a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs b/src/LaunchDarkly.XamarinSdk/ILdClient.cs similarity index 95% rename from src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs rename to src/LaunchDarkly.XamarinSdk/ILdClient.cs index 983034e4..f14c2cc6 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdMobileClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdClient.cs @@ -1,20 +1,30 @@ using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; -using LaunchDarkly.Common; using System.Threading.Tasks; using LaunchDarkly.Client; namespace LaunchDarkly.Xamarin { - public interface ILdMobileClient : ILdCommonClient + public interface ILdClient : IDisposable { + /// + /// Returns the current version number of the LaunchDarkly client. + /// + Version Version { get; } + /// /// Tests whether the client is ready to be used. /// /// true if the client is ready, or false if it is still initializing bool Initialized(); + /// + /// Tests whether the client is being used in offline mode. + /// + /// true if the client is offline + bool IsOffline(); + /// /// Returns the boolean value of a feature flag for a given flag key. /// @@ -228,5 +238,10 @@ public interface ILdMobileClient : ILdCommonClient /// /// the user to register Task IdentifyAsync(User user); + + /// + /// Flushes all pending events. + /// + void Flush(); } } diff --git a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs b/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs deleted file mode 100644 index 572d808f..00000000 --- a/src/LaunchDarkly.XamarinSdk/IMobileConfiguration.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using LaunchDarkly.Common; - -namespace LaunchDarkly.Xamarin -{ - public interface IMobileConfiguration : IBaseConfiguration - { - /// - /// The interval between feature flag updates when the app is running in the background. - /// - TimeSpan BackgroundPollingInterval { get; } - - /// - /// When streaming mode is disabled, this is the interval between feature flag updates. - /// - TimeSpan PollingInterval { get; } - - /// - /// The connection timeout to the LaunchDarkly server. - /// - TimeSpan ConnectionTimeout { get; } - - /// - /// Whether to enable feature flag updates when the app is running in the background. - /// - bool EnableBackgroundUpdating { get; } - - /// - /// The time between flushes of the event buffer. Decreasing the flush interval means - /// that the event buffer is less likely to reach capacity. The default value is 5 seconds. - /// - TimeSpan EventFlushInterval { get; } - - /// - /// Whether to enable real-time streaming flag updates. When false, feature flags are updated via polling. - /// - bool IsStreamingEnabled { get; } - - /// - /// Whether to use the REPORT HTTP verb when fetching flags from LaunchDarkly. - /// - bool UseReport { get; } - - /// - /// True if LaunchDarkly should provide additional information about how flag values were - /// calculated. The additional information will then be available through the client's "detail" - /// methods such as . Since this - /// increases the size of network requests, such information is not sent unless you set this option - /// to true. - /// - bool EvaluationReasons { get; } - - /// - /// True if the SDK should save flag values for each user in persistent storage, so they will be - /// immediately available the next time the SDK is started for the same user. This is true by - /// default; set it to false to disable this behavior. - /// - /// - /// The implementation of persistent storage depends on the target platform. In Android and iOS, it - /// uses the standard user preferences mechanism. In .NET Standard, it uses the IsolatedStorageFile - /// API, which stores file data under the current account's home directory at - /// ~/.local/share/IsolateStorage/. - /// - bool PersistFlagValues { get; } - } -} diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index fe5d710a..39ffe9a3 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 0f5cd331..69b01990 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -15,7 +15,7 @@ namespace LaunchDarkly.Xamarin /// A client for the LaunchDarkly API. Client instances are thread-safe. Your application should instantiate /// a single LdClient for the lifetime of their application. /// - public sealed class LdClient : ILdMobileClient + public sealed class LdClient : ILdClient { private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); @@ -432,10 +432,7 @@ public IDictionary AllFlags() } return flagCacheManager.FlagsForUser(User) - .ToDictionary(p => p.Key, p => new ImmutableJsonValue(p.Value.value)); - // Note that we are calling the ImmutableJsonValue constructor directly instead of using FromJToken() - // because we do not need it to deep-copy mutable values immediately - we know that *we* won't be - // modifying those values. It will deep-copy them if and when the application tries to access them. + .ToDictionary(p => p.Key, p => ImmutableJsonValue.FromSafeValue(p.Value.value)); } /// @@ -447,7 +444,7 @@ public void Track(string eventName, ImmutableJsonValue data) /// public void Track(string eventName) { - Track(eventName, ImmutableJsonValue.FromJToken(null)); + Track(eventName, ImmutableJsonValue.Null); } /// diff --git a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs index 47a66423..5b21b4a8 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs @@ -16,12 +16,12 @@ internal class MobileStreamingProcessor : IMobileUpdateProcessor, IStreamProcess private static readonly ILog Log = LogManager.GetLogger(typeof(MobileStreamingProcessor)); private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); - private readonly IMobileConfiguration _configuration; + private readonly Configuration _configuration; private readonly IFlagCacheManager _cacheManager; private readonly User _user; private readonly StreamManager _streamManager; - internal MobileStreamingProcessor(IMobileConfiguration configuration, + internal MobileStreamingProcessor(Configuration configuration, IFlagCacheManager cacheManager, User user, StreamManager.EventSourceCreator eventSourceCreator) @@ -34,7 +34,7 @@ internal MobileStreamingProcessor(IMobileConfiguration configuration, _streamManager = new StreamManager(this, streamProperties, - _configuration, + _configuration.StreamManagerConfiguration, MobileClientEnvironment.Instance, eventSourceCreator); } diff --git a/src/LaunchDarkly.XamarinSdk/ValueType.cs b/src/LaunchDarkly.XamarinSdk/ValueType.cs index fc20b299..b8bd3f83 100644 --- a/src/LaunchDarkly.XamarinSdk/ValueType.cs +++ b/src/LaunchDarkly.XamarinSdk/ValueType.cs @@ -75,11 +75,8 @@ private static ArgumentException BadTypeException() public static ValueType Json = new ValueType { - ValueFromJson = json => new ImmutableJsonValue(json), + ValueFromJson = json => ImmutableJsonValue.FromSafeValue(json), ValueToJson = value => value.AsJToken() - // Note that we are calling the ImmutableJsonValue constructor directly instead of using FromJToken() - // because we do not need it to deep-copy mutable values immediately - we know that *we* won't be - // modifying those values. It will deep-copy them if and when the application tries to access them. }; } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index a830a66f..becc9d28 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -374,7 +374,7 @@ private void VerifyRequest(FluentMockServer server, UpdateMode mode) Assert.Null(req.Body); } - private void VerifyFlagValues(ILdMobileClient client, IDictionary flags) + private void VerifyFlagValues(ILdClient client, IDictionary flags) { Assert.True(client.Initialized()); foreach (var e in flags) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 90d7d771..cec59879 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs index 6929cb19..94b3e301 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEvaluationTests.cs @@ -167,7 +167,7 @@ public void JsonVariationReturnsValue() string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue); using (var client = ClientWithFlagsJson(flagsJson)) { - Assert.Equal(jsonValue, client.JsonVariation("flag-key", ImmutableJsonValue.FromJToken(3)).AsJToken()); + Assert.Equal(jsonValue, client.JsonVariation("flag-key", ImmutableJsonValue.Of(3)).AsJToken()); } } @@ -176,7 +176,7 @@ public void JsonVariationReturnsDefaultForUnknownFlag() { using (var client = ClientWithFlagsJson("{}")) { - var defaultVal = ImmutableJsonValue.FromJToken(3); + var defaultVal = ImmutableJsonValue.Of(3); Assert.Equal(defaultVal, client.JsonVariation(nonexistentFlagKey, defaultVal)); } } @@ -190,8 +190,7 @@ public void JsonVariationDetailReturnsValue() using (var client = ClientWithFlagsJson(flagsJson)) { var expected = new EvaluationDetail(jsonValue, 1, reason); - var result = client.JsonVariationDetail("flag-key", ImmutableJsonValue.FromJToken(3)); - // Note, JToken.Equals() doesn't work, so we need to test each property separately + var result = client.JsonVariationDetail("flag-key", ImmutableJsonValue.Of(3)); Assert.True(JToken.DeepEquals(expected.Value, result.Value.AsJToken())); Assert.Equal(expected.VariationIndex, result.VariationIndex); Assert.Equal(expected.Reason, result.Reason); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index 7f86bf55..db856cc6 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -35,7 +35,7 @@ public void TrackSendsCustomEvent() using (LdClient client = MakeClient(user, "{}")) { JToken data = new JValue("hi"); - client.Track("eventkey", ImmutableJsonValue.FromJToken(data)); + client.Track("eventkey", ImmutableJsonValue.Of(data)); Assert.Collection(eventProcessor.Events, e => CheckIdentifyEvent(e, user), e => { From 8fa98c7a5ad19fe0535e9adcbea5ccd997201ab4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 5 Aug 2019 12:40:51 -0700 Subject: [PATCH 214/254] make more things sealed and/or internal --- .../DefaultDeviceInfo.cs | 2 +- .../DefaultPersistentStorage.cs | 2 +- src/LaunchDarkly.XamarinSdk/Extensions.cs | 2 +- src/LaunchDarkly.XamarinSdk/FeatureFlag.cs | 2 +- .../FeatureFlagRequestor.cs | 17 +++++++---------- src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs | 2 +- src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs | 4 ++-- .../IMobileUpdateProcessor.cs | 2 +- src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs | 2 +- .../MobileConnectionManager.cs | 2 +- .../MobilePollingProcessor.cs | 2 +- .../MobileSideClientEnvironment.cs | 2 +- .../MobileStreamingProcessor.cs | 2 +- .../UserFlagDeviceCache.cs | 2 +- .../UserFlagInMemoryCache.cs | 2 +- src/LaunchDarkly.XamarinSdk/ValueType.cs | 2 +- tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 2 +- 17 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs b/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs index 704f5523..c46e50a4 100644 --- a/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs +++ b/src/LaunchDarkly.XamarinSdk/DefaultDeviceInfo.cs @@ -4,7 +4,7 @@ namespace LaunchDarkly.Xamarin { // This just delegates to the conditionally-compiled code in LaunchDarkly.Xamarin.PlatformSpecific. // The only reason it is a pluggable component is for unit tests; we don't currently expose IDeviceInfo. - public class DefaultDeviceInfo : IDeviceInfo + internal sealed class DefaultDeviceInfo : IDeviceInfo { public string UniqueDeviceId() { diff --git a/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs index 37af6cdf..175ddab9 100644 --- a/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs +++ b/src/LaunchDarkly.XamarinSdk/DefaultPersistentStorage.cs @@ -2,7 +2,7 @@ namespace LaunchDarkly.Xamarin { - internal class DefaultPersistentStorage : IPersistentStorage + internal sealed class DefaultPersistentStorage : IPersistentStorage { public void Save(string key, string value) { diff --git a/src/LaunchDarkly.XamarinSdk/Extensions.cs b/src/LaunchDarkly.XamarinSdk/Extensions.cs index 07d8e1ac..44562aba 100644 --- a/src/LaunchDarkly.XamarinSdk/Extensions.cs +++ b/src/LaunchDarkly.XamarinSdk/Extensions.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin { - public static class Extensions + internal static class Extensions { public static string Base64Encode(this string plainText) { diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlag.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlag.cs index b6b67d5c..ef0a1647 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlag.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlag.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin { - public class FeatureFlag : IEquatable + internal sealed class FeatureFlag : IEquatable { public JToken value; public int version; diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs index 7e5afaa5..dd840484 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs @@ -30,7 +30,7 @@ internal interface IFeatureFlagRequestor : IDisposable Task FeatureFlagsAsync(); } - internal class FeatureFlagRequestor : IFeatureFlagRequestor + internal sealed class FeatureFlagRequestor : IFeatureFlagRequestor { private static readonly ILog Log = LogManager.GetLogger(typeof(FeatureFlagRequestor)); private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); @@ -121,19 +121,16 @@ private async Task MakeRequest(HttpRequestMessage request) } } - protected virtual void Dispose(bool disposing) + // Sealed, non-derived class should implement Dispose() and finalize method, not Dispose(boolean) + public void Dispose() { - if (disposing) - { - _httpClient.Dispose(); - } + _httpClient.Dispose(); + GC.SuppressFinalize(this); } - // This code added to correctly implement the disposable pattern. - void IDisposable.Dispose() + ~FeatureFlagRequestor() { - Dispose(true); - GC.SuppressFinalize(this); + Dispose(); } } } diff --git a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs index a10d2bbc..af614883 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagCacheManager.cs @@ -6,7 +6,7 @@ namespace LaunchDarkly.Xamarin { - internal class FlagCacheManager : IFlagCacheManager + internal sealed class FlagCacheManager : IFlagCacheManager { private readonly IUserFlagCache inMemoryCache; private readonly IUserFlagCache deviceCache; diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index b59f53e9..1dec8f2d 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -9,7 +9,7 @@ namespace LaunchDarkly.Xamarin /// /// An event object that is sent to handlers for the event. /// - public class FlagChangedEventArgs + public sealed class FlagChangedEventArgs { /// /// The unique key of the feature flag whose value has changed. @@ -68,7 +68,7 @@ internal interface IFlagChangedEventManager void FlagWasUpdated(string flagKey, JToken newValue, JToken oldValue); } - internal class FlagChangedEventManager : IFlagChangedEventManager + internal sealed class FlagChangedEventManager : IFlagChangedEventManager { private static readonly ILog Log = LogManager.GetLogger(typeof(IFlagChangedEventManager)); diff --git a/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs index 925576fc..cbe0c73b 100644 --- a/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs @@ -26,7 +26,7 @@ internal interface IMobileUpdateProcessor : IDisposable /// /// Used when the client is offline or in LDD mode. /// - internal class NullUpdateProcessor : IMobileUpdateProcessor + internal sealed class NullUpdateProcessor : IMobileUpdateProcessor { Task IMobileUpdateProcessor.Start() { diff --git a/src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs index 99211ba4..2cecb540 100644 --- a/src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs +++ b/src/LaunchDarkly.XamarinSdk/IUserFlagCache.cs @@ -9,7 +9,7 @@ internal interface IUserFlagCache IDictionary RetrieveFlags(User user); } - internal class NullUserFlagCache : IUserFlagCache + internal sealed class NullUserFlagCache : IUserFlagCache { public void CacheFlagsForUser(IDictionary flags, User user) { } public IDictionary RetrieveFlags(User user) => null; diff --git a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs index 0de11a80..b576892a 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin { - internal class MobileConnectionManager : IConnectionManager + internal sealed class MobileConnectionManager : IConnectionManager { internal Action ConnectionChanged; diff --git a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs index 44668ae5..a76552f3 100644 --- a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs @@ -9,7 +9,7 @@ namespace LaunchDarkly.Xamarin { - internal class MobilePollingProcessor : IMobileUpdateProcessor + internal sealed class MobilePollingProcessor : IMobileUpdateProcessor { private static readonly ILog Log = LogManager.GetLogger(typeof(MobilePollingProcessor)); diff --git a/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs b/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs index f786d3c6..4a519a98 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileSideClientEnvironment.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin { - internal class MobileClientEnvironment : ClientEnvironment + internal sealed class MobileClientEnvironment : ClientEnvironment { internal static readonly MobileClientEnvironment Instance = new MobileClientEnvironment(); diff --git a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs index 5b21b4a8..89490a80 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs @@ -11,7 +11,7 @@ namespace LaunchDarkly.Xamarin { - internal class MobileStreamingProcessor : IMobileUpdateProcessor, IStreamProcessor + internal sealed class MobileStreamingProcessor : IMobileUpdateProcessor, IStreamProcessor { private static readonly ILog Log = LogManager.GetLogger(typeof(MobileStreamingProcessor)); private static readonly HttpMethod ReportMethod = new HttpMethod("REPORT"); diff --git a/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs index 29e616e6..a5c83b62 100644 --- a/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs +++ b/src/LaunchDarkly.XamarinSdk/UserFlagDeviceCache.cs @@ -7,7 +7,7 @@ namespace LaunchDarkly.Xamarin { - internal class UserFlagDeviceCache : IUserFlagCache + internal sealed class UserFlagDeviceCache : IUserFlagCache { private static readonly ILog Log = LogManager.GetLogger(typeof(UserFlagDeviceCache)); private readonly IPersistentStorage persister; diff --git a/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs b/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs index e9c9c553..2ebd2ce6 100644 --- a/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs +++ b/src/LaunchDarkly.XamarinSdk/UserFlagInMemoryCache.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin { - internal class UserFlagInMemoryCache : IUserFlagCache + internal sealed class UserFlagInMemoryCache : IUserFlagCache { // A map of the key (user.Key) and their featureFlags readonly ConcurrentDictionary JSONMap = diff --git a/src/LaunchDarkly.XamarinSdk/ValueType.cs b/src/LaunchDarkly.XamarinSdk/ValueType.cs index b8bd3f83..4222f1ff 100644 --- a/src/LaunchDarkly.XamarinSdk/ValueType.cs +++ b/src/LaunchDarkly.XamarinSdk/ValueType.cs @@ -4,7 +4,7 @@ namespace LaunchDarkly.Xamarin { - internal class ValueType + internal sealed class ValueType { public Func ValueFromJson { get; internal set; } public Func ValueToJson { get; internal set; } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 208f9a25..ca678113 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -128,7 +128,7 @@ public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? return JsonConvert.SerializeObject(o); } - public static IDictionary DecodeFlagsJson(string flagsJson) + internal static IDictionary DecodeFlagsJson(string flagsJson) { return JsonConvert.DeserializeObject>(flagsJson); } From 827243bdfd8925e34c3fbff8d9bacc359ce7cf3b Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 5 Aug 2019 21:26:11 -0700 Subject: [PATCH 215/254] Removed unused locks, simplified all flags to match Android behavior, changed variation internal to correct behavior and had to change unit test --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 71 +++++++------------ .../LDClientEndToEndTests.cs | 2 +- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 0f5cd331..cfa923d0 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -25,9 +25,6 @@ public sealed class LdClient : ILdMobileClient static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; - - readonly object _myLockObjForConnectionChange = new object(); - readonly object _myLockObjForUserUpdate = new object(); readonly Configuration _config; readonly SemaphoreSlim _connectionLock; @@ -74,32 +71,32 @@ public bool Online { var doNotAwaitResult = SetOnlineAsync(value); } - } - - /// - /// Indicates which platform the SDK is built for. - /// - /// - /// This property is mainly useful for debugging. It does not indicate which platform you are actually running on, - /// but rather which variant of the SDK is currently in use. - /// - /// The LaunchDarkly.XamarinSdk package contains assemblies for multiple target platforms. In an Android - /// or iOS application, you will normally be using the Android or iOS variant of the SDK; that is done - /// automatically when you install the NuGet package. On all other platforms, you will get the .NET Standard - /// variant. - /// - /// The basic features of the SDK are the same in all of these variants; the difference is in platform-specific - /// behavior such as detecting when an application has gone into the background, detecting network connectivity, - /// and ensuring that code is executed on the UI thread if applicable for that platform. Therefore, if you find - /// that these platform-specific behaviors are not working correctly, you may want to check this property to - /// make sure you are not for some reason running the .NET Standard SDK on a phone. - /// + } + + /// + /// Indicates which platform the SDK is built for. + /// + /// + /// This property is mainly useful for debugging. It does not indicate which platform you are actually running on, + /// but rather which variant of the SDK is currently in use. + /// + /// The LaunchDarkly.XamarinSdk package contains assemblies for multiple target platforms. In an Android + /// or iOS application, you will normally be using the Android or iOS variant of the SDK; that is done + /// automatically when you install the NuGet package. On all other platforms, you will get the .NET Standard + /// variant. + /// + /// The basic features of the SDK are the same in all of these variants; the difference is in platform-specific + /// behavior such as detecting when an application has gone into the background, detecting network connectivity, + /// and ensuring that code is executed on the UI thread if applicable for that platform. Therefore, if you find + /// that these platform-specific behaviors are not working correctly, you may want to check this property to + /// make sure you are not for some reason running the .NET Standard SDK on a phone. + /// public static PlatformType PlatformType - { - get - { - return PlatformSpecific.UserMetadata.PlatformType; - } + { + get + { + return PlatformSpecific.UserMetadata.PlatformType; + } } // private constructor prevents initialization of this class @@ -374,7 +371,7 @@ EvaluationDetail VariationInternal(string featureKey, T defaultValue, Valu EvaluationDetail errorResult(EvaluationErrorKind kind) => new EvaluationDetail(defaultValue, null, new EvaluationReason.Error(kind)); - if (!Initialized()) + if (flagCacheManager is null || eventProcessor is null) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); @@ -420,22 +417,8 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => /// public IDictionary AllFlags() { - if (IsOffline()) - { - Log.Warn("AllFlags() was called when client is in offline mode. Returning null."); - return null; - } - if (!Initialized()) - { - Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); - return null; - } - return flagCacheManager.FlagsForUser(User) - .ToDictionary(p => p.Key, p => new ImmutableJsonValue(p.Value.value)); - // Note that we are calling the ImmutableJsonValue constructor directly instead of using FromJToken() - // because we do not need it to deep-copy mutable values immediately - we know that *we* won't be - // modifying those values. It will deep-copy them if and when the application tries to access them. + .ToDictionary(p => p.Key, p => ImmutableJsonValue.FromSafeValue(p.Value.value)); } /// diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index a830a66f..382f0d8b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -86,7 +86,7 @@ public void InitCanTimeOutSync() using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) { Assert.False(client.Initialized()); - Assert.Null(client.StringVariation(_flagData1.First().Key, null)); + Assert.Equal("value1", client.StringVariation(_flagData1.First().Key, null)); Assert.Contains(log.Messages, m => m.Level == LogLevel.Warn && m.Text == "Client did not successfully initialize within 200 milliseconds."); } From 3a7bb49d18c6362ce1e752bf55bc3f0a02c26d7a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 6 Aug 2019 13:35:02 -0700 Subject: [PATCH 216/254] bump CommonSdk version --- src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 2 +- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 39ffe9a3..3341e1df 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -24,7 +24,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index cec59879..733625e8 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + From b7b4e9f151d9d7e9d948cfa6ccadd12675df8ce4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 6 Aug 2019 13:44:54 -0700 Subject: [PATCH 217/254] 1.0.0-beta20 --- CHANGELOG.md | 25 +++++++++++++++++++ .../LaunchDarkly.XamarinSdk.csproj | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72aa6537..a978d1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to the LaunchDarkly Client-side SDK for Xamarin will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [1.0.0-beta20] - 2019-08-06 +### Added: +- `Configuration.Builder` provides a fluent builder pattern for constructing `Configuration` objects. This is now the only method for building a configuration if you want to set properties other than the SDK key. +- `ImmutableJsonValue.Null` (equivalent to `ImmutableJsonValue.Of(null)`). +- `LdClient.PlatformType`. +- Verbose debug logging for stream connections. + +### Changed: +- `Configuration` objects are now immutable. +- In `Configuration`, `EventQueueCapacity` and `EventQueueFrency` have been renamed to `EventCapacity` and `EventFlushInterval`, for consistency with other LaunchDarkly SDKs. +- `ImmutableJsonValue.FromJToken()` was renamed to `ImmutableJsonValue.Of()`. +- In `FlagChangedEventArgs`, `NewValue` and `OldValue` now have the type `ImmutableJsonValue` instead of `JToken`. +- `ILdMobileClient` is now named `ILdClient`. + +### Fixed: +- Fixed a bug where setting a user's custom attribute to a null value could cause an exception during JSON serialization of event data. + +### Removed: +- `ConfigurationExtensions` (use `Configuration.Builder`). +- `Configuration.SamplingInterval`. +- `UserExtensions` (use `User.Builder`). +- `User` constructors (use `User.WithKey` or `User.Builder`). +- `User` property setters. +- `IBaseConfiguration` and `ICommonLdClient` interfaces. + ## [1.0.0-beta19] - 2019-07-31 ### Added: - `User.Builder` provides a fluent builder pattern for constructing `User` objects. This is now the only method for building a user if you want to set properties other than `Key`. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 3341e1df..2407f099 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -4,7 +4,7 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - 1.0.0-beta19 + 1.0.0-beta20 Library LaunchDarkly.XamarinSdk LaunchDarkly.XamarinSdk From 94a88b03b0f8a16912066d8e093804ea4b16104d Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 6 Aug 2019 14:36:13 -0700 Subject: [PATCH 218/254] Added streaming mode check when entering background, added unit tests for backgrounding offline behavior, removed redundant class names in BackgroundDetection.android --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 7 +- .../BackgroundDetection.android.cs | 4 +- .../LDClientEndToEndTests.cs | 64 +++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 69b01990..aa925e75 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -497,7 +497,7 @@ public async Task IdentifyAsync(User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { - if (Online) + if (Online && !Config.Offline) return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); else return true; @@ -505,7 +505,7 @@ bool StartUpdateProcessor(TimeSpan maxWaitTime) Task StartUpdateProcessorAsync() { - if (Online) + if (Online && !Config.Offline) return updateProcessor.Start(); else return Task.FromResult(true); @@ -632,7 +632,8 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh { Log.Debug("Background updating is disabled"); } - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + if (Config.IsStreamingEnabled) + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } else { diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs index 3412324e..a3ddc5d1 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.android.cs @@ -35,12 +35,12 @@ public void OnActivityDestroyed(Activity activity) public void OnActivityPaused(Activity activity) { - BackgroundDetection.UpdateBackgroundMode(true); + UpdateBackgroundMode(true); } public void OnActivityResumed(Activity activity) { - BackgroundDetection.UpdateBackgroundMode(false); + UpdateBackgroundMode(false); } public void OnActivitySaveInstanceState(Activity activity, Bundle outState) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index becc9d28..bc3b1cfe 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -340,6 +340,70 @@ await WithServerAsync(async server => }); } + [Fact(Skip = SkipIfCannotCreateHttpServer)] + public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() + { + WithServer(server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); + using (var client = TestUtil.CreateClient(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + } + }); + } + + [Fact(Skip = SkipIfCannotCreateHttpServer)] + public async Task BackgroundOfflineClientUsesCachedFlagsAsyncAfterStartUpdateProcessorAsync() + { + await WithServerAsync(async server => + { + SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + + var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. + + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + }); + } + private IConfigurationBuilder BaseConfig(FluentMockServer server) { return Configuration.BuilderInternal(_mobileKey) From 0268dd326323aacd7b0845bfe8bfd26185620caf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 6 Aug 2019 15:06:41 -0700 Subject: [PATCH 219/254] 1.0.0-beta21 --- CHANGELOG.md | 5 ++++- src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 4 ++-- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a978d1bf..12e0ed1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the LaunchDarkly Client-side SDK for Xamarin will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). -## [1.0.0-beta20] - 2019-08-06 +## [1.0.0-beta21] - 2019-08-06 ### Added: - `Configuration.Builder` provides a fluent builder pattern for constructing `Configuration` objects. This is now the only method for building a configuration if you want to set properties other than the SDK key. - `ImmutableJsonValue.Null` (equivalent to `ImmutableJsonValue.Of(null)`). @@ -28,6 +28,9 @@ This project adheres to [Semantic Versioning](http://semver.org). - `User` property setters. - `IBaseConfiguration` and `ICommonLdClient` interfaces. +## [1.0.0-beta20] - 2019-08-06 +Incomplete release, replaced by beta21. + ## [1.0.0-beta19] - 2019-07-31 ### Added: - `User.Builder` provides a fluent builder pattern for constructing `User` objects. This is now the only method for building a user if you want to set properties other than `Key`. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 2407f099..e26f1498 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -4,7 +4,7 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - 1.0.0-beta20 + 1.0.0-beta21 Library LaunchDarkly.XamarinSdk LaunchDarkly.XamarinSdk @@ -24,7 +24,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 733625e8..118cb7d8 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + From da45bd4acc21a6b79dbe6dc178d0f9698f897c8e Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 6 Aug 2019 16:46:47 -0700 Subject: [PATCH 220/254] Removed redundant config online check in start update processor, removed persister BACKGROUNDED_WHILE_STREAMING in favor of static var --- src/LaunchDarkly.XamarinSdk/Constants.cs | 1 - src/LaunchDarkly.XamarinSdk/LdClient.cs | 15 +-- .../Resources/Resource.designer.cs | 101 ------------------ 3 files changed, 8 insertions(+), 109 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Constants.cs b/src/LaunchDarkly.XamarinSdk/Constants.cs index 16d2a154..cdea0913 100644 --- a/src/LaunchDarkly.XamarinSdk/Constants.cs +++ b/src/LaunchDarkly.XamarinSdk/Constants.cs @@ -21,6 +21,5 @@ internal static class Constants public const string PING = "ping"; public const string EVENTS_PATH = "/mobile/events/bulk"; public const string UNIQUE_ID_KEY = "unique_id_key"; - public const string BACKGROUNDED_WHILE_STREAMING = "didEnterBackgroundKey"; } } diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index aa925e75..56c4dcaf 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -22,6 +22,8 @@ public sealed class LdClient : ILdClient static volatile LdClient _instance; static volatile User _user; + static bool backgroundedWhileStreaming; + static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; @@ -497,7 +499,7 @@ public async Task IdentifyAsync(User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { - if (Online && !Config.Offline) + if (Online) return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); else return true; @@ -505,7 +507,7 @@ bool StartUpdateProcessor(TimeSpan maxWaitTime) Task StartUpdateProcessorAsync() { - if (Online && !Config.Offline) + if (Online) return updateProcessor.Start(); else return Task.FromResult(true); @@ -633,7 +635,7 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh Log.Debug("Background updating is disabled"); } if (Config.IsStreamingEnabled) - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + backgroundedWhileStreaming = true; } else { @@ -644,10 +646,9 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh void ResetProcessorForForeground() { - string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); - if (didBackground != null && didBackground.Equals("true")) - { - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); + if (backgroundedWhileStreaming) + { + backgroundedWhileStreaming = false; ClearUpdateProcessor(); _disableStreaming = false; } diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs index bdd99699..f352a1ff 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs @@ -26,107 +26,6 @@ static Resource() public static void UpdateIdValues() { - global::LaunchDarkly.XamarinSdk.Resource.Attribute.font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.font; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderAuthority; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderCerts; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchStrategy; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchTimeout; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderPackage; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderQuery; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontStyle; - global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontWeight; - global::LaunchDarkly.XamarinSdk.Resource.Boolean.abc_action_bar_embed_tabs = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_action_bar_embed_tabs; - global::LaunchDarkly.XamarinSdk.Resource.Color.notification_action_color_filter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_action_color_filter; - global::LaunchDarkly.XamarinSdk.Resource.Color.notification_icon_bg_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_icon_bg_color; - global::LaunchDarkly.XamarinSdk.Resource.Color.ripple_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.ripple_material_light; - global::LaunchDarkly.XamarinSdk.Resource.Color.secondary_text_default_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_default_material_light; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_horizontal_material; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_vertical_material; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_horizontal_material; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_vertical_material; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_control_corner_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_control_corner_material; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_icon_size; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_text_size; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_big_circle_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_big_circle_margin; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_content_margin_start = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_content_margin_start; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_height; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_width; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_main_column_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_main_column_padding_top; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_media_narrow_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_media_narrow_margin; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_icon_size; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_side_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_side_padding_top; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_background_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_background_padding; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_size_as_large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_size_as_large; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_subtext_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_subtext_size; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad; - global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad_large_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad_large_text; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_action_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_action_background; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_normal; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_pressed; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal_pressed; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_icon_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_icon_background; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_bg; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_low_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_low_bg; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_tile_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_tile_bg; - global::LaunchDarkly.XamarinSdk.Resource.Drawable.notify_panel_notification_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notify_panel_notification_icon_bg; - global::LaunchDarkly.XamarinSdk.Resource.Id.action_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_container; - global::LaunchDarkly.XamarinSdk.Resource.Id.action_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_divider; - global::LaunchDarkly.XamarinSdk.Resource.Id.action_image = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_image; - global::LaunchDarkly.XamarinSdk.Resource.Id.action_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_text; - global::LaunchDarkly.XamarinSdk.Resource.Id.actions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.actions; - global::LaunchDarkly.XamarinSdk.Resource.Id.async = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.async; - global::LaunchDarkly.XamarinSdk.Resource.Id.blocking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.blocking; - global::LaunchDarkly.XamarinSdk.Resource.Id.chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.chronometer; - global::LaunchDarkly.XamarinSdk.Resource.Id.forever = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.forever; - global::LaunchDarkly.XamarinSdk.Resource.Id.icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon; - global::LaunchDarkly.XamarinSdk.Resource.Id.icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon_group; - global::LaunchDarkly.XamarinSdk.Resource.Id.info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.info; - global::LaunchDarkly.XamarinSdk.Resource.Id.italic = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.italic; - global::LaunchDarkly.XamarinSdk.Resource.Id.line1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line1; - global::LaunchDarkly.XamarinSdk.Resource.Id.line3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line3; - global::LaunchDarkly.XamarinSdk.Resource.Id.normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.normal; - global::LaunchDarkly.XamarinSdk.Resource.Id.notification_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_background; - global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column; - global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column_container; - global::LaunchDarkly.XamarinSdk.Resource.Id.right_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_icon; - global::LaunchDarkly.XamarinSdk.Resource.Id.right_side = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_side; - global::LaunchDarkly.XamarinSdk.Resource.Id.tag_transition_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.tag_transition_group; - global::LaunchDarkly.XamarinSdk.Resource.Id.text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text; - global::LaunchDarkly.XamarinSdk.Resource.Id.text2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text2; - global::LaunchDarkly.XamarinSdk.Resource.Id.time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.time; - global::LaunchDarkly.XamarinSdk.Resource.Id.title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.title; - global::LaunchDarkly.XamarinSdk.Resource.Integer.status_bar_notification_info_maxnum = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.status_bar_notification_info_maxnum; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action_tombstone = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action_tombstone; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_custom_big = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_custom_big; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_icon_group; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_chronometer; - global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_time; - global::LaunchDarkly.XamarinSdk.Resource.String.status_bar_notification_info_overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.status_bar_notification_info_overflow; - global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification; - global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Info; - global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Line2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Line2; - global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Time; - global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Title; - global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionContainer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionContainer; - global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionText; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderAuthority; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderCerts; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchStrategy; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchTimeout; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderPackage; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderQuery; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_font; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontStyle; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontWeight; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_font; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontStyle; - global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontWeight; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_in; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_out; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_grow_fade_in_from_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_grow_fade_in_from_bottom; From dbba4668139bfe79145f0f88004eb7f4bf7a1976 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 6 Aug 2019 17:29:33 -0700 Subject: [PATCH 221/254] Simplified backgroundedWhileStreaming, made backgroundedWhileStreaming private not static, added braces to if --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 56c4dcaf..4de32752 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -22,7 +22,7 @@ public sealed class LdClient : ILdClient static volatile LdClient _instance; static volatile User _user; - static bool backgroundedWhileStreaming; + private bool backgroundedWhileStreaming; static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; @@ -634,24 +634,20 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh { Log.Debug("Background updating is disabled"); } - if (Config.IsStreamingEnabled) - backgroundedWhileStreaming = true; + if (Config.IsStreamingEnabled) + { + backgroundedWhileStreaming = true; + } } else { - ResetProcessorForForeground(); + if (backgroundedWhileStreaming) + { + backgroundedWhileStreaming = false; + _disableStreaming = false; + } await RestartUpdateProcessorAsync(Config.PollingInterval); } } - - void ResetProcessorForForeground() - { - if (backgroundedWhileStreaming) - { - backgroundedWhileStreaming = false; - ClearUpdateProcessor(); - _disableStreaming = false; - } - } } } From 90555501c3ed2d271120f917370c24df8005d4ff Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Tue, 6 Aug 2019 17:35:34 -0700 Subject: [PATCH 222/254] Remove unnecessary backgroundedWhileStreaming, always reset _disableStreaming to false on foreground --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 4de32752..017e1fa5 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -22,8 +22,6 @@ public sealed class LdClient : ILdClient static volatile LdClient _instance; static volatile User _user; - private bool backgroundedWhileStreaming; - static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; @@ -634,18 +632,10 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh { Log.Debug("Background updating is disabled"); } - if (Config.IsStreamingEnabled) - { - backgroundedWhileStreaming = true; - } } else { - if (backgroundedWhileStreaming) - { - backgroundedWhileStreaming = false; - _disableStreaming = false; - } + _disableStreaming = false; await RestartUpdateProcessorAsync(Config.PollingInterval); } } From 70389ca3938808db36e3ae806101231ca29d9e97 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Fri, 9 Aug 2019 14:23:38 -0700 Subject: [PATCH 223/254] Changed Initialized behavior to new spec, added testing, changed comment explanation for Initialized --- src/LaunchDarkly.XamarinSdk/ILdClient.cs | 14 +++++- src/LaunchDarkly.XamarinSdk/LdClient.cs | 46 ++++++++++++++----- .../LDClientEndToEndTests.cs | 12 ++++- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdClient.cs b/src/LaunchDarkly.XamarinSdk/ILdClient.cs index f14c2cc6..706b1090 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdClient.cs @@ -14,9 +14,19 @@ public interface ILdClient : IDisposable Version Version { get; } /// - /// Tests whether the client is ready to be used. + /// When you first start the client, once Init or InitAsync has returned, + /// Initialized() should be true if and only if either 1. it connected to LaunchDarkly + /// and successfully retrieved flags, or 2. it started in offline mode so + /// there's no need to connect to LaunchDarkly. So if the client timed out trying to + /// connect to LD, then Initialized is false (even if we do have cached flags). + /// If the client connected and got a 401 error, Initialized is false. + /// This serves the purpose of letting the app know that + /// there was a problem of some kind. Initialized() will be temporarily false during the + /// time in between calling Identify and receiving the new user's flags. There is one case where + /// Initialized() could become false: if you switch users with Identify and the client is unable + /// to get the new user's flags from LauncDarkly. /// - /// true if the client is ready, or false if it is still initializing + /// true if the client has connected to LaunchDarkly and has flags or if the config is set offline, or false if it couldn't connect. bool Initialized(); /// diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index c05af439..15fdd915 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -22,6 +22,8 @@ public sealed class LdClient : ILdClient static volatile LdClient _instance; static volatile User _user; + static bool initialized; + static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; @@ -37,7 +39,7 @@ public sealed class LdClient : ILdClient readonly IPersistentStorage persister; // These LdClient fields are not readonly because they change according to online status - volatile IMobileUpdateProcessor updateProcessor; + internal volatile IMobileUpdateProcessor updateProcessor; volatile bool _disableStreaming; volatile bool _online; @@ -95,7 +97,7 @@ public static PlatformType PlatformType { get { - return PlatformSpecific.UserMetadata.PlatformType; + return UserMetadata.PlatformType; } } @@ -114,6 +116,11 @@ public static PlatformType PlatformType _connectionLock = new SemaphoreSlim(1, 1); + if (configuration.Offline) + { + initialized = true; + } + persister = Factory.CreatePersistentStorage(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagChangedEventManager = Factory.CreateFlagChangedEventManager(configuration); @@ -371,12 +378,6 @@ EvaluationDetail VariationInternal(string featureKey, T defaultValue, Valu EvaluationDetail errorResult(EvaluationErrorKind kind) => new EvaluationDetail(defaultValue, null, new EvaluationReason.Error(kind)); - if (flagCacheManager is null || eventProcessor is null) - { - Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); - return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); - } - var flag = flagCacheManager.FlagForUser(featureKey, User); if (flag == null) { @@ -436,7 +437,7 @@ public void Track(string eventName) /// public bool Initialized() { - return Online && updateProcessor.Initialized(); + return initialized; } /// @@ -471,6 +472,7 @@ public async Task IdentifyAsync(User user) try { _user = newUser; + initialized = false; await RestartUpdateProcessorAsync(Config.PollingInterval); } finally @@ -484,17 +486,39 @@ public async Task IdentifyAsync(User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { if (Online) - return AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); + { + var successfulConnection = AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); + if (successfulConnection) + { + initialized = true; + } + else + { + initialized = false; + } + return successfulConnection; + } else + { return true; + } } Task StartUpdateProcessorAsync() { if (Online) - return updateProcessor.Start(); + { + var successfulConnection = updateProcessor.Start(); + if (successfulConnection.IsCompleted) + { + initialized = true; + } + return successfulConnection; + } else + { return Task.FromResult(true); + } } async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 5faa44f2..18fa5da4 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -85,6 +85,7 @@ public void InitCanTimeOutSync() var config = BaseConfig(server).IsStreamingEnabled(false).Build(); using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) { + Assert.False(Initialized(client)); Assert.False(client.Initialized()); Assert.Equal("value1", client.StringVariation(_flagData1.First().Key, null)); Assert.Contains(log.Messages, m => m.Level == LogLevel.Warn && @@ -139,6 +140,7 @@ await WithServerAsync(async server => // will complete successfully with an uninitialized client. using (var client = await TestUtil.CreateClientAsync(config, _user)) { + Assert.False(Initialized(client)); Assert.False(client.Initialized()); } } @@ -243,6 +245,7 @@ public void OfflineClientUsesCachedFlagsSync() using (var client = TestUtil.CreateClient(config, _user)) { VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); } // At this point the SDK should have written the flags to persistent storage for this user key. @@ -254,6 +257,7 @@ public void OfflineClientUsesCachedFlagsSync() using (var client = TestUtil.CreateClient(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); } }); } @@ -280,6 +284,7 @@ await WithServerAsync(async server => using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); } }); } @@ -340,6 +345,11 @@ await WithServerAsync(async server => }); } + private bool Initialized(LdClient client) + { + return client.Online && client.updateProcessor.Initialized(); + } + private IConfigurationBuilder BaseConfig(FluentMockServer server) { return Configuration.BuilderInternal(_mobileKey) @@ -376,7 +386,7 @@ private void VerifyRequest(FluentMockServer server, UpdateMode mode) private void VerifyFlagValues(ILdClient client, IDictionary flags) { - Assert.True(client.Initialized()); + Assert.True(Initialized((LdClient) client)); foreach (var e in flags) { Assert.Equal(e.Value, client.StringVariation(e.Key, null)); From 02fa10cd36b58262d1e395d5586ec26a0c5fedc9 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Fri, 9 Aug 2019 15:50:45 -0700 Subject: [PATCH 224/254] Fix merge bug with constant that doesn't exist anymore --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index a2eedc98..34cb3cca 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -642,7 +642,6 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh { Log.Debug("Background updating is disabled"); } - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } else { From a0c6ae2421eaeb53fe9df81174e390f5aed58780 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Fri, 9 Aug 2019 16:48:47 -0700 Subject: [PATCH 225/254] Simplified initialized assignment, removed unnecssary static keyword --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 34cb3cca..6f6463dd 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -22,7 +22,7 @@ public sealed class LdClient : ILdClient static volatile LdClient _instance; static volatile User _user; - static bool initialized; + bool initialized; static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; @@ -488,14 +488,7 @@ bool StartUpdateProcessor(TimeSpan maxWaitTime) if (Online) { var successfulConnection = AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); - if (successfulConnection) - { - initialized = true; - } - else - { - initialized = false; - } + initialized = successfulConnection; return successfulConnection; } else From 6018077394f3e0ea17774166cb8989e1905e1bd9 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Fri, 9 Aug 2019 16:51:11 -0700 Subject: [PATCH 226/254] Improved Initialized comment in ILdClient --- src/LaunchDarkly.XamarinSdk/ILdClient.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ILdClient.cs b/src/LaunchDarkly.XamarinSdk/ILdClient.cs index 706b1090..ed8a251f 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdClient.cs @@ -14,6 +14,9 @@ public interface ILdClient : IDisposable Version Version { get; } /// + /// Returns a boolean value indicating LaunchDarkly connection and flag state within the client. + /// + /// /// When you first start the client, once Init or InitAsync has returned, /// Initialized() should be true if and only if either 1. it connected to LaunchDarkly /// and successfully retrieved flags, or 2. it started in offline mode so @@ -22,10 +25,10 @@ public interface ILdClient : IDisposable /// If the client connected and got a 401 error, Initialized is false. /// This serves the purpose of letting the app know that /// there was a problem of some kind. Initialized() will be temporarily false during the - /// time in between calling Identify and receiving the new user's flags. There is one case where - /// Initialized() could become false: if you switch users with Identify and the client is unable - /// to get the new user's flags from LauncDarkly. - /// + /// time in between calling Identify and receiving the new user's flags. It will also be false + /// if you switch users with Identify and the client is unable + /// to get the new user's flags from LaunchDarkly. + /// /// true if the client has connected to LaunchDarkly and has flags or if the config is set offline, or false if it couldn't connect. bool Initialized(); From 4b8c8efc65732ecc4d8463b8a8ce0246f0bd18ae Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 12 Aug 2019 12:18:11 -0700 Subject: [PATCH 227/254] don't use default .NET HTTP handler (#67) --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 120 ++++++++++++------ .../ConfigurationBuilder.cs | 21 ++- .../LaunchDarkly.XamarinSdk.csproj | 2 +- .../PlatformSpecific/Http.android.cs | 11 ++ .../PlatformSpecific/Http.ios.cs | 10 ++ .../PlatformSpecific/Http.netstandard.cs | 9 ++ .../PlatformSpecific/Http.shared.cs | 25 ++++ .../ConfigurationTest.cs | 6 +- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LdClientEventTests.cs | 2 +- .../Info.plist | 101 ++++++++------- .../Properties/AssemblyInfo.cs | 6 +- 12 files changed, 218 insertions(+), 97 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.android.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.ios.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.netstandard.cs create mode 100644 src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.shared.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index e9deb58e..d3279706 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -26,7 +26,7 @@ public sealed class Configuration private readonly TimeSpan _eventFlushInterval; private readonly int _eventCapacity; private readonly Uri _eventsUri; - private readonly HttpClientHandler _httpClientHandler; + private readonly HttpMessageHandler _httpMessageHandler; private readonly TimeSpan _httpClientTimeout; private readonly bool _inlineUsersInEvents; private readonly bool _isStreamingEnabled; @@ -64,12 +64,12 @@ public sealed class Configuration /// public bool AllAttributesPrivate => _allAttributesPrivate; - /// - /// The interval between feature flag updates when the application is running in the background. - /// - /// - /// This is only relevant on mobile platforms. - /// + /// + /// The interval between feature flag updates when the application is running in the background. + /// + /// + /// This is only relevant on mobile platforms. + /// public TimeSpan BackgroundPollingInterval => _backgroundPollingInterval; /// @@ -77,17 +77,17 @@ public sealed class Configuration /// public Uri BaseUri => _baseUri; - /// - /// The connection timeout to the LaunchDarkly server. - /// + /// + /// The connection timeout to the LaunchDarkly server. + /// public TimeSpan ConnectionTimeout { get; internal set; } - /// - /// Whether to enable feature flag updates when the application is running in the background. - /// - /// - /// This is only relevant on mobile platforms. - /// + /// + /// Whether to enable feature flag updates when the application is running in the background. + /// + /// + /// This is only relevant on mobile platforms. + /// public bool EnableBackgroundUpdating => _enableBackgroundUpdating; /// @@ -126,9 +126,9 @@ public sealed class Configuration public Uri EventsUri => _eventsUri; /// - /// The object to be used for sending HTTP requests. This is exposed for testing purposes. + /// The object to be used for sending HTTP requests, if a specific implementation is desired. /// - public HttpClientHandler HttpClientHandler => _httpClientHandler; + public HttpMessageHandler HttpMessageHandler => _httpMessageHandler; /// /// The connection timeout. The default value is 10 seconds. @@ -142,7 +142,7 @@ public sealed class Configuration /// The default is false: events will only include the user key, except for one "index" event that /// provides the full details for the user. /// - public bool InlineUsersInEvents => _inlineUsersInEvents; + public bool InlineUsersInEvents => _inlineUsersInEvents; /// /// Whether or not the streaming API should be used to receive flag updates. @@ -187,7 +187,7 @@ public sealed class Configuration /// removed, even if you did not use the /// method in . /// - public ISet PrivateAttributeNames => _privateAttributeNames; + public IImmutableSet PrivateAttributeNames => _privateAttributeNames; /// /// The timeout when reading data from the streaming connection. @@ -227,8 +227,8 @@ public sealed class Configuration /// public int UserKeysCapacity => _userKeysCapacity; - /// - /// The interval at which the event processor will reset its set of known user keys. + /// + /// The interval at which the event processor will reset its set of known user keys. /// public TimeSpan UserKeysFlushInterval => _userKeysFlushInterval; @@ -236,9 +236,9 @@ public sealed class Configuration /// Default value for . /// public static TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); - - /// - /// Minimum value for . + + /// + /// Minimum value for . /// public static TimeSpan MinimumPollingInterval = TimeSpan.FromMinutes(5); @@ -267,13 +267,33 @@ public static Configuration Default(string mobileKey) return Builder(mobileKey).Build(); } - /// /// Creates a ConfigurationBuilder for constructing a configuration object using a fluent syntax. /// /// /// This is the only method for building a Configuration if you are setting properties /// besides the MobileKey. The ConfigurationBuilder has methods for setting any number of /// properties, after which you call to get the resulting /// Configuration instance. /// /// ///
        ///     var config = Configuration.Builder("my-sdk-key")
        ///         .EventQueueFrequency(TimeSpan.FromSeconds(90))
        ///         .StartWaitTime(TimeSpan.FromSeconds(5))
        ///         .Build();
        /// 
///
/// the mobile SDK key for your LaunchDarkly environment - /// a builder object public static IConfigurationBuilder Builder(string mobileKey) { - if (String.IsNullOrEmpty(mobileKey)) - { - throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required"); + /// + /// Creates a ConfigurationBuilder for constructing a configuration object using a fluent syntax. + /// + /// + /// This is the only method for building a Configuration if you are setting properties + /// besides the MobileKey. The ConfigurationBuilder has methods for setting any number of + /// properties, after which you call to get the resulting + /// Configuration instance. + /// + /// + ///
+        ///     var config = Configuration.Builder("my-sdk-key")
+        ///         .EventQueueFrequency(TimeSpan.FromSeconds(90))
+        ///         .StartWaitTime(TimeSpan.FromSeconds(5))
+        ///         .Build();
+        /// 
+ ///
+ /// the mobile SDK key for your LaunchDarkly environment + /// a builder object + public static IConfigurationBuilder Builder(string mobileKey) + { + if (String.IsNullOrEmpty(mobileKey)) + { + throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required"); } - return new ConfigurationBuilder(mobileKey); } + return new ConfigurationBuilder(mobileKey); + } /// /// Exposed for test code that needs to access the internal methods of ConfigurationBuilder that @@ -281,8 +301,10 @@ public static Configuration Default(string mobileKey) /// /// the mobile SDK key /// a builder object - internal static ConfigurationBuilder BuilderInternal(string mobileKey) { - return new ConfigurationBuilder(mobileKey); } + internal static ConfigurationBuilder BuilderInternal(string mobileKey) + { + return new ConfigurationBuilder(mobileKey); + } /// /// Creates a ConfigurationBuilder starting with the properties of an existing Configuration. @@ -296,7 +318,31 @@ public static IConfigurationBuilder Builder(Configuration fromConfiguration) internal Configuration(ConfigurationBuilder builder) { - _allAttributesPrivate = builder._allAttributesPrivate; _backgroundPollingInterval = builder._backgroundPollingInterval; _baseUri = builder._baseUri; _connectionTimeout = builder._connectionTimeout; _enableBackgroundUpdating = builder._enableBackgroundUpdating; _evaluationReasons = builder._evaluationReasons; _eventFlushInterval = builder._eventFlushInterval; _eventCapacity = builder._eventCapacity; _eventsUri = builder._eventsUri; _httpClientHandler = builder._httpClientHandler; _httpClientTimeout = builder._httpClientTimeout; _inlineUsersInEvents = builder._inlineUsersInEvents; _isStreamingEnabled = builder._isStreamingEnabled; _mobileKey = builder._mobileKey; _offline = builder._offline; _persistFlagValues = builder._persistFlagValues; _pollingInterval = builder._pollingInterval; _privateAttributeNames = builder._privateAttributeNames is null ? null : builder._privateAttributeNames.ToImmutableHashSet(); _readTimeout = builder._readTimeout; _reconnectTime = builder._reconnectTime; _streamUri = builder._streamUri; _useReport = builder._useReport; _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; + _allAttributesPrivate = builder._allAttributesPrivate; + _backgroundPollingInterval = builder._backgroundPollingInterval; + _baseUri = builder._baseUri; + _connectionTimeout = builder._connectionTimeout; + _enableBackgroundUpdating = builder._enableBackgroundUpdating; + _evaluationReasons = builder._evaluationReasons; + _eventFlushInterval = builder._eventFlushInterval; + _eventCapacity = builder._eventCapacity; + _eventsUri = builder._eventsUri; + _httpMessageHandler = builder._httpMessageHandler; + _httpClientTimeout = builder._httpClientTimeout; + _inlineUsersInEvents = builder._inlineUsersInEvents; + _isStreamingEnabled = builder._isStreamingEnabled; + _mobileKey = builder._mobileKey; + _offline = builder._offline; + _persistFlagValues = builder._persistFlagValues; + _pollingInterval = builder._pollingInterval; + _privateAttributeNames = builder._privateAttributeNames is null ? null : + builder._privateAttributeNames.ToImmutableHashSet(); + _readTimeout = builder._readTimeout; + _reconnectTime = builder._reconnectTime; + _streamUri = builder._streamUri; + _useReport = builder._useReport; + _userKeysCapacity = builder._userKeysCapacity; + _userKeysFlushInterval = builder._userKeysFlushInterval; _connectionManager = builder._connectionManager; _deviceInfo = builder._deviceInfo; @@ -320,7 +366,7 @@ private class EventProcessorAdapter : IEventProcessorConfiguration public Uri EventsUri => Config.EventsUri; public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; public bool InlineUsersInEvents => Config.InlineUsersInEvents; - public ISet PrivateAttributeNames => Config.PrivateAttributeNames; + public IImmutableSet PrivateAttributeNames => Config.PrivateAttributeNames; public TimeSpan ReadTimeout => Config.ReadTimeout; public TimeSpan ReconnectTime => Config.ReconnectTime; public int UserKeysCapacity => Config.UserKeysCapacity; @@ -331,14 +377,14 @@ private class HttpRequestAdapter : IHttpRequestConfiguration { internal Configuration Config { get; set; } public string HttpAuthorizationKey => Config.MobileKey; - public HttpClientHandler HttpClientHandler => Config.HttpClientHandler; + public HttpMessageHandler HttpMessageHandler => Config.HttpMessageHandler; } private class StreamManagerAdapter : IStreamManagerConfiguration { internal Configuration Config { get; set; } public string HttpAuthorizationKey => Config.MobileKey; - public HttpClientHandler HttpClientHandler => Config.HttpClientHandler; + public HttpMessageHandler HttpMessageHandler => Config.HttpMessageHandler; public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; public TimeSpan ReadTimeout => Config.ReadTimeout; public TimeSpan ReconnectTime => Config.ReconnectTime; diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index e1fcc82f..84212c7a 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -106,11 +106,18 @@ public interface IConfigurationBuilder IConfigurationBuilder EventsUri(Uri eventsUri); /// - /// Sets the object to be used for sending HTTP requests. This is exposed for testing purposes. + /// Sets the object to be used for sending HTTP requests, if a specific implementation is desired. /// - /// the HttpClientHandler to use + /// + /// This is exposed mainly for testing purposes; you should not normally need to change it. + /// By default, on mobile platforms it will use the appropriate native HTTP handler for the + /// current platform, if any (e.g. Xamarin.Android.Net.AndroidClientHandler). If this is + /// null, the SDK will call the default constructor without + /// specifying a handler, which may or may not result in using a native HTTP handler. + /// + /// the HttpMessageHandler to use /// the same builder - IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler); + IConfigurationBuilder HttpMessageHandler(HttpMessageHandler httpMessageHandler); /// /// Sets the connection timeout. The default value is 10 seconds. @@ -268,7 +275,7 @@ internal sealed class ConfigurationBuilder : IConfigurationBuilder internal int _eventCapacity = Configuration.DefaultEventCapacity; internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; internal Uri _eventsUri = Configuration.DefaultEventsUri; - internal HttpClientHandler _httpClientHandler = new HttpClientHandler(); + internal HttpMessageHandler _httpMessageHandler = PlatformSpecific.Http.GetHttpMessageHandler(); // see Http.shared.cs internal TimeSpan _httpClientTimeout = Configuration.DefaultHttpClientTimeout; internal bool _inlineUsersInEvents = false; internal bool _isStreamingEnabled = true; @@ -309,7 +316,7 @@ internal ConfigurationBuilder(Configuration copyFrom) _eventCapacity = copyFrom.EventCapacity; _eventFlushInterval = copyFrom.EventFlushInterval; _eventsUri = copyFrom.EventsUri; - _httpClientHandler = copyFrom.HttpClientHandler; + _httpMessageHandler = copyFrom.HttpMessageHandler; _httpClientTimeout = copyFrom.HttpClientTimeout; _inlineUsersInEvents = copyFrom.InlineUsersInEvents; _isStreamingEnabled = copyFrom.IsStreamingEnabled; @@ -394,9 +401,9 @@ public IConfigurationBuilder EventsUri(Uri eventsUri) return this; } - public IConfigurationBuilder HttpClientHandler(HttpClientHandler httpClientHandler) + public IConfigurationBuilder HttpMessageHandler(HttpMessageHandler httpMessageHandler) { - _httpClientHandler = httpClientHandler; + _httpMessageHandler = httpMessageHandler; return this; } diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index e26f1498..1aafbd69 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.android.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.android.cs new file mode 100644 index 00000000..adc54f51 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.android.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using Xamarin.Android.Net; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class Http + { + private static HttpMessageHandler PlatformCreateHttpMessageHandler() => + new AndroidClientHandler(); + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.ios.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.ios.cs new file mode 100644 index 00000000..0076e11d --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.ios.cs @@ -0,0 +1,10 @@ +using System.Net.Http; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class Http + { + private static HttpMessageHandler PlatformCreateHttpMessageHandler() => + new NSUrlSessionHandler(); + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.netstandard.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.netstandard.cs new file mode 100644 index 00000000..4465fd65 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.netstandard.cs @@ -0,0 +1,9 @@ +using System.Net.Http; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class Http + { + private static HttpMessageHandler PlatformCreateHttpMessageHandler() => null; + } +} diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.shared.cs new file mode 100644 index 00000000..ad70e696 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/Http.shared.cs @@ -0,0 +1,25 @@ +using System.Net.Http; + +namespace LaunchDarkly.Xamarin.PlatformSpecific +{ + internal static partial class Http + { + private static HttpMessageHandler _httpMessageHandler = PlatformCreateHttpMessageHandler(); + + /// + /// If our default configuration should use a specific + /// implementation, returns that implementation. + /// + /// + /// The handler is not stateful, so it can be a shared instance. If we don't need to use a + /// specific implementation, this returns null. This is just the default for + /// , so the application can still override it. If it is + /// null and the application lets it remain null, then Xamarin will make its + /// own decision based on logic we don't have access to; in practice that seems to result + /// in picking the same handler that our platform-specific logic would specify, but that + /// may be dependent on project configuration, so we decided to explicitly set a default. + /// + /// an HTTP handler implementation or null + public static HttpMessageHandler GetHttpMessageHandler() => _httpMessageHandler; + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs index b9f76e20..53e0438a 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/ConfigurationTest.cs @@ -66,14 +66,14 @@ public void CannotSetTooSmallBackgroundPollingInterval() } [Fact] - public void CanSetHttpClientHandler() + public void CanSetHttpMessageHandler() { var handler = new HttpClientHandler(); var config = Configuration.Builder("AnyOtherSdkKey") - .HttpClientHandler(handler) + .HttpMessageHandler(handler) .Build(); - Assert.Equal(handler, config.HttpClientHandler); + Assert.Equal(handler, config.HttpMessageHandler); } } } \ No newline at end of file diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 118cb7d8..646d691d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index db856cc6..7f86bf55 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -35,7 +35,7 @@ public void TrackSendsCustomEvent() using (LdClient client = MakeClient(user, "{}")) { JToken data = new JValue("hi"); - client.Track("eventkey", ImmutableJsonValue.Of(data)); + client.Track("eventkey", ImmutableJsonValue.FromJToken(data)); Assert.Collection(eventProcessor.Events, e => CheckIdentifyEvent(e, user), e => { diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist index e17844fd..b14a26b7 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Info.plist @@ -1,48 +1,61 @@ - + - CFBundleDisplayName - LaunchDarkly.XamarinSdk.iOS.Tests - CFBundleIdentifier - com.launchdarkly.XamarinSdkTests - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 10.0 - UIDeviceFamily - - 1 - 2 - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIMainStoryboardFile~ipad - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/AppIcon.appiconset + CFBundleDisplayName + LaunchDarkly.XamarinSdk.iOS.Tests + CFBundleIdentifier + com.launchdarkly.XamarinSdkTests + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 10.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIMainStoryboardFile~ipad + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset + CFBundleName + LaunchDarkly.XamarinSdk.iOS.Tests + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + - \ No newline at end of file + diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs index b2eb5309..f68b19b0 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("LaunchDarkly.XamarinSdk.iOS.Toasts")] +[assembly: AssemblyTitle("LaunchDarkly.XamarinSdk.iOS.Tests")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("LaunchDarkly.XamarinSdk.iOS.Toasts")] -[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyProduct("LaunchDarkly.XamarinSdk.iOS.Tests")] +[assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] From f6d44e2fd211231c80cb4e0c02142854c479b8b4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 12 Aug 2019 13:08:42 -0700 Subject: [PATCH 228/254] fix test state contamination in cached flag tests --- .../LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 6 + .../LDClientEndToEndTests.cs | 247 ++++++++++++------ 2 files changed, 166 insertions(+), 87 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs index e020d44e..007dab21 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Common.Logging; +using LaunchDarkly.Client; using WireMock.Server; using Xunit; @@ -30,6 +31,11 @@ public void Dispose() TestUtil.ClearClient(); } + protected void ClearCachedFlags(User user) + { + PlatformSpecific.Preferences.Clear(Constants.FLAGS_KEY_PREFIX + user.Key); + } + protected void WithServer(Action a) { var s = MakeServer(); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index a9c29ae8..de480274 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -87,7 +87,7 @@ public void InitCanTimeOutSync() { Assert.False(Initialized(client)); Assert.False(client.Initialized()); - Assert.Equal("value1", client.StringVariation(_flagData1.First().Key, null)); + Assert.Null(client.StringVariation(_flagData1.First().Key, null)); Assert.Contains(log.Messages, m => m.Level == LogLevel.Warn && m.Text == "Client did not successfully initialize within 200 milliseconds."); } @@ -241,23 +241,35 @@ public void OfflineClientUsesCachedFlagsSync() { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = TestUtil.CreateClient(config, _user)) + ClearCachedFlags(_user); + try { - VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = TestUtil.CreateClient(config, _user)) + { + VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = TestUtil.CreateClient(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); + } + } + finally { - VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); + ClearCachedFlags(_user); } }); } @@ -269,22 +281,34 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = await TestUtil.CreateClientAsync(config, _user)) + ClearCachedFlags(_user); + try { - VerifyFlagValues(client, _flagData1); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + VerifyFlagValues(client, _flagData1); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + VerifyFlagValues(client, _flagData1); + Assert.True(client.Initialized()); + } + } + finally { - VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); + ClearCachedFlags(_user); } }); } @@ -296,23 +320,35 @@ public void OfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = TestUtil.CreateClient(config, _user)) + ClearCachedFlags(_user); + try { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = TestUtil.CreateClient(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = TestUtil.CreateClient(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + } + finally { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); + ClearCachedFlags(_user); } }); } @@ -324,23 +360,35 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = await TestUtil.CreateClientAsync(config, _user)) + ClearCachedFlags(_user); + try { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + } + finally { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); + ClearCachedFlags(_user); } }); } @@ -357,27 +405,39 @@ public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor( { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = TestUtil.CreateClient(config, _user)) + ClearCachedFlags(_user); + try { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = TestUtil.CreateClient(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = TestUtil.CreateClient(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = TestUtil.CreateClient(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + } + } + finally { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); + ClearCachedFlags(_user); } }); } @@ -389,27 +449,39 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); - using (var client = await TestUtil.CreateClientAsync(config, _user)) + ClearCachedFlags(_user); + try { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + var config = BaseConfig(server) + .UseReport(false) + .IsStreamingEnabled(false) + .PersistFlagValues(true) + .Build(); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // At this point the SDK should have written the flags to persistent storage for this user key. + // We'll now start over, but with a server that doesn't respond immediately. When the client times + // out, we should still see the earlier flag values. - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + server.Reset(); // the offline client shouldn't be making any requests, but just in case + var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); + using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) + { + BackgroundDetection.UpdateBackgroundMode(true); + VerifyFlagValues(client, _flagData1); + BackgroundDetection.UpdateBackgroundMode(false); + VerifyFlagValues(client, _flagData1); + } + } + finally { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); + ClearCachedFlags(_user); } }); } @@ -419,7 +491,8 @@ private IConfigurationBuilder BaseConfig(FluentMockServer server) return Configuration.BuilderInternal(_mobileKey) .EventProcessor(new MockEventProcessor()) .BaseUri(new Uri(server.GetUrl())) - .StreamUri(new Uri(server.GetUrl())); + .StreamUri(new Uri(server.GetUrl())) + .PersistFlagValues(false); // unless we're specifically testing flag caching, this helps to prevent test state contamination } private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) From 2c3e65271a74fdf6f0ca77d1d8b34c83aaa8fb90 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 12 Aug 2019 13:49:22 -0700 Subject: [PATCH 229/254] Addressing PR feedback about keeping warn logging for variation when client isn't initialized --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 6f6463dd..f53a5e2c 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -381,10 +381,18 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => var flag = flagCacheManager.FlagForUser(featureKey, User); if (flag == null) { - Log.InfoFormat("Unknown feature flag {0}; returning default value", featureKey); - eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, - EvaluationErrorKind.FLAG_NOT_FOUND)); - return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); + if (!Initialized()) + { + Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); + return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); + } + else + { + Log.InfoFormat("Unknown feature flag {0}; returning default value", featureKey); + eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, + EvaluationErrorKind.FLAG_NOT_FOUND)); + return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); + } } featureFlagEvent = new FeatureFlagEvent(featureKey, flag); @@ -393,6 +401,10 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => if (flag.value == null || flag.value.Type == JTokenType.Null) { valueJson = defaultJson; + if (!Initialized()) + { + Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); + } result = new EvaluationDetail(defaultValue, flag.variation, flag.reason); } else From 4de70dfdbc92161f328e24cb83ab6a3be98b21e0 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Mon, 12 Aug 2019 14:19:00 -0700 Subject: [PATCH 230/254] Removed warn log from incorrect default return, added check for when returning cached value but client is not initialized --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index f53a5e2c..56251f8f 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -383,7 +383,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => { if (!Initialized()) { - Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); + Log.Warn("LaunchDarkly client has not yet been initialized. Returning default value"); return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); } else @@ -394,6 +394,13 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); } } + else + { + if (!Initialized()) + { + Log.Warn("LaunchDarkly client has not yet been initialized. Returning cached value"); + } + } featureFlagEvent = new FeatureFlagEvent(featureKey, flag); EvaluationDetail result; @@ -401,10 +408,6 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => if (flag.value == null || flag.value.Type == JTokenType.Null) { valueJson = defaultJson; - if (!Initialized()) - { - Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); - } result = new EvaluationDetail(defaultValue, flag.variation, flag.reason); } else From b586979c3d4237b9c45b68f2a3ff068a8d52b713 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Aug 2019 18:40:56 -0700 Subject: [PATCH 231/254] send event for evaluation even if client isn't inited --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 + .../LdClientEventTests.cs | 57 +++++++++++++++++++ .../MockComponents.cs | 26 ++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 56251f8f..f9e5243c 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -384,6 +384,8 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => if (!Initialized()) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default value"); + eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, + EvaluationErrorKind.CLIENT_NOT_READY)); return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); } else diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index 7f86bf55..039d1821 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -138,6 +138,33 @@ public void VariationSendsFeatureEventForUnknownFlag() Assert.Null(fe.Reason); }); } + } + + [Fact] + public void VariationSendsFeatureEventForUnknownFlagWhenClientIsNotInitialized() + { + var config = TestUtil.ConfigWithFlagsJson(user, "appkey", "") + .UpdateProcessorFactory(MockUpdateProcessorThatNeverInitializes.Factory()) + .EventProcessor(eventProcessor); + config.EventProcessor(eventProcessor); + + using (LdClient client = TestUtil.CreateClient(config.Build(), user)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("b", result); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + Assert.Null(fe.Reason); + }); + } } [Fact] @@ -219,6 +246,36 @@ public void VariationDetailSendsFeatureEventWithReasonForUnknownFlag() Assert.Equal(expectedReason, fe.Reason); }); } + } + + [Fact] + public void VariationSendsFeatureEventWithReasonForUnknownFlagWhenClientIsNotInitialized() + { + var config = TestUtil.ConfigWithFlagsJson(user, "appkey", "") + .UpdateProcessorFactory(MockUpdateProcessorThatNeverInitializes.Factory()) + .EventProcessor(eventProcessor); + config.EventProcessor(eventProcessor); + + using (LdClient client = TestUtil.CreateClient(config.Build(), user)) + { + EvaluationDetail result = client.StringVariationDetail("flag", "b"); + var expectedReason = new EvaluationReason.Error(EvaluationErrorKind.CLIENT_NOT_READY); + Assert.Equal("b", result.Value); + Assert.Equal(expectedReason, result.Reason); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + Assert.False(fe.TrackEvents); + Assert.Null(fe.DebugEventsUntilDate); + Assert.Equal(expectedReason, fe.Reason); + }); + } } private void CheckIdentifyEvent(Event e, User u) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index 69f96cd0..e96ba01d 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -104,7 +104,7 @@ public FeatureFlag FlagForUser(string flagKey, User user) { var flags = FlagsForUser(user); FeatureFlag featureFlag; - if (flags.TryGetValue(flagKey, out featureFlag)) + if (flags != null && flags.TryGetValue(flagKey, out featureFlag)) { return featureFlag; } @@ -208,4 +208,28 @@ public Task Start() return Task.FromResult(true); } } + + internal class MockUpdateProcessorThatNeverInitializes : IMobileUpdateProcessor + { + public static Func Factory() + { + return (config, manager, user) => new MockUpdateProcessorThatNeverInitializes(); + } + + public bool IsRunning => false; + + public void Dispose() + { + } + + public bool Initialized() + { + return false; + } + + public Task Start() + { + return new TaskCompletionSource().Task; // will never be completed + } + } } From 218637466bead4d87f45ea16be63347527d0b9c0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Aug 2019 19:19:21 -0700 Subject: [PATCH 232/254] make sure Identify/IdentifyAsync only completes when we have new flags --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 10 +-- .../LdClientTests.cs | 75 ++++++++++++++++++- .../MockComponents.cs | 26 +++++++ .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 22 +++--- 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index f9e5243c..2a73159b 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -514,20 +514,20 @@ bool StartUpdateProcessor(TimeSpan maxWaitTime) } } - Task StartUpdateProcessorAsync() + async Task StartUpdateProcessorAsync() { if (Online) { - var successfulConnection = updateProcessor.Start(); - if (successfulConnection.IsCompleted) + var successfulConnection = await updateProcessor.Start(); + if (successfulConnection) { initialized = true; } return successfulConnection; } else - { - return Task.FromResult(true); + { + return true; } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index d232fd4b..877e273e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Client; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -7,7 +9,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class DefaultLdClientTests : BaseTest + public class LdClientTests : BaseTest { static readonly string appKey = "some app key"; static readonly User simpleUser = User.WithKey("user-key"); @@ -55,7 +57,76 @@ public void IdentifyUpdatesTheUser() client.Identify(updatedUser); Assert.Equal(client.User.Key, updatedUser.Key); // don't compare entire user, because SDK may have added device/os attributes } - } + } + + [Fact] + public Task IdentifyAsyncCompletesOnlyWhenNewFlagsAreAvailable() + => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => client.IdentifyAsync(user)); + + [Fact] + public Task IdentifySyncCompletesOnlyWhenNewFlagsAreAvailable() + => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => Task.Run(() => client.Identify(user))); + + private async Task IdentifyCompletesOnlyWhenNewFlagsAreAvailable(Func identifyTask) + { + var userA = User.WithKey("a"); + var userB = User.WithKey("b"); + + var flagKey = "flag"; + var userAFlags = TestUtil.MakeSingleFlagData(flagKey, "a-value"); + var userBFlags = TestUtil.MakeSingleFlagData(flagKey, "b-value"); + + var startedIdentifyUserB = new SemaphoreSlim(0, 1); + var canFinishIdentifyUserB = new SemaphoreSlim(0, 1); + var finishedIdentifyUserB = new SemaphoreSlim(0, 1); + + Func updateProcessorFactory = (c, flags, user) => + new MockUpdateProcessorFromLambda(user, async () => + { + switch (user.Key) + { + case "a": + flags.CacheFlagsFromService(userAFlags, user); + break; + + case "b": + startedIdentifyUserB.Release(); + await canFinishIdentifyUserB.WaitAsync(); + flags.CacheFlagsFromService(userBFlags, user); + break; + } + }); + + var config = TestUtil.ConfigWithFlagsJson(userA, appKey, "{}") + .UpdateProcessorFactory(updateProcessorFactory) + .Build(); + + ClearCachedFlags(userA); + ClearCachedFlags(userB); + + using (var client = await LdClient.InitAsync(config, userA)) + { + Assert.True(client.Initialized()); + Assert.Equal("a-value", client.StringVariation(flagKey, null)); + + var identifyUserBTask = Task.Run(async () => + { + await identifyTask(client, userB); + finishedIdentifyUserB.Release(); + }); + + await startedIdentifyUserB.WaitAsync(); + + Assert.False(client.Initialized()); + Assert.Null(client.StringVariation(flagKey, null)); + + canFinishIdentifyUserB.Release(); + await finishedIdentifyUserB.WaitAsync(); + + Assert.True(client.Initialized()); + Assert.Equal("b-value", client.StringVariation(flagKey, null)); + } + } [Fact] public void IdentifyWithNullUserThrowsException() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index e96ba01d..25e94762 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -209,6 +209,32 @@ public Task Start() } } + internal class MockUpdateProcessorFromLambda : IMobileUpdateProcessor + { + private readonly User _user; + private readonly Func _startFn; + private bool _initialized; + + public MockUpdateProcessorFromLambda(User user, Func startFn) + { + _user = user; + _startFn = startFn; + } + + public Task Start() + { + return _startFn().ContinueWith(t => + { + _initialized = true; + return true; + }); + } + + public bool Initialized() => _initialized; + + public void Dispose() { } + } + internal class MockUpdateProcessorThatNeverInitializes : IMobileUpdateProcessor { public static Func Factory() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index ca678113..4b5e3ce3 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -113,19 +113,15 @@ public static void ClearClient() }); } - public static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) - { - JObject fo = new JObject { { "value", value } }; - if (variation != null) - { - fo["variation"] = new JValue(variation.Value); - } - if (reason != null) - { - fo["reason"] = JToken.FromObject(reason); - } - JObject o = new JObject { { flagKey, fo } }; - return JsonConvert.SerializeObject(o); + internal static Dictionary MakeSingleFlagData(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) + { + var flag = new FeatureFlag { value = value, variation = variation, reason = reason }; + return new Dictionary { { flagKey, flag } }; + } + + internal static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) + { + return JsonConvert.SerializeObject(MakeSingleFlagData(flagKey, value, variation, reason)); } internal static IDictionary DecodeFlagsJson(string flagsJson) From 82c087e469603120642407cedf2fa835dc3170c5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Aug 2019 10:04:34 -0700 Subject: [PATCH 233/254] add helper scripts --- .gitignore | 1 + CONTRIBUTING.md | 10 ++++++++++ scripts/build-test-package.sh | 32 ++++++++++++++++++++++++++++++++ scripts/update-version.sh | 12 ++++++++++++ 4 files changed, 55 insertions(+) create mode 100755 scripts/build-test-package.sh create mode 100755 scripts/update-version.sh diff --git a/.gitignore b/.gitignore index af1d0262..a13ec91a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ [Bb]in/ packages/ TestResults/ +test-packages/ # globs Makefile.in diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51fbe5eb..f6fa0311 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,3 +50,13 @@ You can run the mobile test projects from Visual Studio (the iOS tests require M Note that the mobile unit tests currently do not cover background-mode behavior or connectivity detection. ### Packaging/releasing + +Run `./scripts/update-version.sh ` to set the project version. + +Run `./scripts/release.sh` to build the project for all target frameworks and upload the package to NuGet. You must have already configured the necessary NuGet key locally. + +To verify that the package can be built without uploading it, run `./scripts/package.sh`. + +### Building a temporary package + +If you need to build a `.nupkg` for testing another application (in cases where linking directly to this project is not an option), run `./scripts/build-test-package.sh`. This will create a package with a unique version string in `./test-packages`. You can then set your other project to use `test-packages` as a NuGet package source. diff --git a/scripts/build-test-package.sh b/scripts/build-test-package.sh new file mode 100755 index 00000000..73482a2b --- /dev/null +++ b/scripts/build-test-package.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +# build-test-package.sh +# Temporarily changes the project version to a unique prerelease version string based on the current +# date and time, builds a .nupkg package, and places the package in ./test-packages. You can then use +# the test-packages directory as a package source to use this package in our testing tools. + +TEST_VERSION="0.0.1-$(date +%Y%m%d.%H%M%S)" +PROJECT_FILE=src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +SAVE_PROJECT_FILE="${PROJECT_FILE}.orig" +TEST_PACKAGE_DIR=./test-packages + +mkdir -p "${TEST_PACKAGE_DIR}" + +cp "${PROJECT_FILE}" "${SAVE_PROJECT_FILE}" + +"$(dirname "$0")/update-version.sh" "${TEST_VERSION}" + +trap 'mv "${SAVE_PROJECT_FILE}" "${PROJECT_FILE}"' EXIT + +msbuild /restore + +NUPKG_FILE="src/LaunchDarkly.XamarinSdk/bin/Debug/LaunchDarkly.XamarinSdk.${TEST_VERSION}.nupkg" +if [ -f "${NUPKG_FILE}" ]; then + mv "${NUPKG_FILE}" "${TEST_PACKAGE_DIR}" + echo; echo; echo "Success! Created test package version ${TEST_VERSION} in ${TEST_PACKAGE_DIR}" +else + echo; echo "Unknown problem - did not build the expected package file" + exit 1 +fi diff --git a/scripts/update-version.sh b/scripts/update-version.sh new file mode 100755 index 00000000..cbe23cdc --- /dev/null +++ b/scripts/update-version.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# update-version.sh +# Updates the version string in the project file. + +NEW_VERSION="$1" + +PROJECT_FILE=./src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +TEMP_FILE="${PROJECT_FILE}.tmp" + +sed "s#^\( *\)[^<]*#\1${NEW_VERSION}#g" "${PROJECT_FILE}" > "${TEMP_FILE}" +mv "${TEMP_FILE}" "${PROJECT_FILE}" From 972fc8759e312c1c3ad5e45ceb8b45880ef62f1e Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 21 Aug 2019 12:04:41 -0700 Subject: [PATCH 234/254] Changed Online to use WaitSafely instead of storing result in a var --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index f9e5243c..9e52270d 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -71,7 +71,7 @@ public bool Online get => _online; set { - var doNotAwaitResult = SetOnlineAsync(value); + AsyncUtils.WaitSafely(() => SetOnlineAsync(value)); } } From e0acb8f956784339d5fb9677083d500abc157146 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 21 Aug 2019 14:36:47 -0700 Subject: [PATCH 235/254] Added identical value check to SetOnlineAsync --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 9a3d6299..93122746 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -283,6 +283,10 @@ void SetupConnectionManager() public async Task SetOnlineAsync(bool value) { + if (value == _online) + { + return; + } await _connectionLock.WaitAsync(); _online = value; try From b82dd4c91c08a734b2dfc40517262e76e2bc8557 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 21 Aug 2019 14:56:36 -0700 Subject: [PATCH 236/254] Move value check inside of lock for Android unit test --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 93122746..0d1fc6ba 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -283,11 +283,11 @@ void SetupConnectionManager() public async Task SetOnlineAsync(bool value) { + await _connectionLock.WaitAsync(); if (value == _online) { return; } - await _connectionLock.WaitAsync(); _online = value; try { From a17153e16a48a70d9c136247d0230f4429763654 Mon Sep 17 00:00:00 2001 From: torchhound <5600929+torchhound@users.noreply.github.com> Date: Wed, 21 Aug 2019 15:28:36 -0700 Subject: [PATCH 237/254] Added second value check inside Online, moved value check into try in SetOnlineAsync --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 0d1fc6ba..90dce51d 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -71,6 +71,10 @@ public bool Online get => _online; set { + if (value == _online) + { + return; + } AsyncUtils.WaitSafely(() => SetOnlineAsync(value)); } } @@ -284,13 +288,13 @@ void SetupConnectionManager() public async Task SetOnlineAsync(bool value) { await _connectionLock.WaitAsync(); - if (value == _online) - { - return; - } - _online = value; try { + if (value == _online) + { + return; + } + _online = value; if (_online) { await RestartUpdateProcessorAsync(Config.PollingInterval); From d9d01dc59909caa756c1abfc480a34c6032a7330 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 23 Aug 2019 16:23:54 -0700 Subject: [PATCH 238/254] disable REPORT mode because it doesn't work in Android yet --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 3 +- .../ConfigurationBuilder.cs | 18 --- .../FeatureFlagRequestorTests.cs | 111 +++++++++--------- .../LDClientEndToEndTests.cs | 26 ++-- .../MobileStreamingProcessorTests.cs | 63 +++++----- 5 files changed, 103 insertions(+), 118 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index d3279706..3f78fe58 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -216,7 +216,8 @@ public sealed class Configuration /// encoded into the request URI. Using REPORT allows the user data to be sent in the request body instead. /// However, some network gateways do not support REPORT. /// - public bool UseReport => _useReport; + internal bool UseReport => _useReport; + // UseReport is currently disabled due to Android HTTP issues (ch47341), but it's still implemented internally /// /// The number of user keys that the event processor can remember at any one time. diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index 84212c7a..e3782890 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -228,18 +228,6 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder StreamUri(Uri streamUri); - /// - /// Sets whether to use the HTTP REPORT method for feature flag requests. - /// - /// - /// By default, polling and streaming connections are made with the GET method, witht the user data - /// encoded into the request URI. Using REPORT allows the user data to be sent in the request body instead. - /// However, some network gateways do not support REPORT. - /// - /// whether to use REPORT mode - /// the same builder - IConfigurationBuilder UseReport(bool useReport); - /// /// Sets the number of user keys that the event processor can remember at any one time. /// @@ -485,12 +473,6 @@ public IConfigurationBuilder StreamUri(Uri streamUri) return this; } - public IConfigurationBuilder UseReport(bool useReport) - { - _useReport = useReport; - return this; - } - public IConfigurationBuilder UserKeysCapacity(int userKeysCapacity) { _userKeysCapacity = userKeysCapacity; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index 0f68f833..76ccb663 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -25,7 +25,7 @@ await WithServerAsync(async server => server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) - .UseReport(false).Build(); + .Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { @@ -51,7 +51,7 @@ await WithServerAsync(async server => server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) - .UseReport(false).EvaluationReasons(true).Build(); + .EvaluationReasons(true).Build(); using (var requestor = new FeatureFlagRequestor(config, _user)) { @@ -69,58 +69,59 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] - public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync() - { - await WithServerAsync(async server => - { - server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - - var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) - .UseReport(true).Build(); - - using (var requestor = new FeatureFlagRequestor(config, _user)) - { - var resp = await requestor.FeatureFlagsAsync(); - Assert.Equal(200, resp.statusCode); - Assert.Equal(_allDataJson, resp.jsonResponse); - - var req = server.GetLastRequest(); - Assert.Equal("REPORT", req.Method); - Assert.Equal($"/msdk/evalx/user", req.Path); - Assert.Equal("", req.RawQuery); - Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); - - //Assert.Equal("{\"key\":\"foo\"}", req.Body); - // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, WireMock.Net - // is not currently able to detect the body for REPORT requests: https://github.com/WireMock-Net/WireMock.Net/issues/290 - } - }); - } - - [Fact(Skip = SkipIfCannotCreateHttpServer)] - public async Task GetFlagsUsesCorrectUriAndMethodInReportModeWithReasonsAsync() - { - await WithServerAsync(async server => - { - server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); - - var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) - .UseReport(true).EvaluationReasons(true).Build(); - - using (var requestor = new FeatureFlagRequestor(config, _user)) - { - var resp = await requestor.FeatureFlagsAsync(); - Assert.Equal(200, resp.statusCode); - Assert.Equal(_allDataJson, resp.jsonResponse); - - var req = server.GetLastRequest(); - Assert.Equal("REPORT", req.Method); - Assert.Equal($"/msdk/evalx/user", req.Path); - Assert.Equal("?withReasons=true", req.RawQuery); - Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); - } - }); - } + // Report mode is currently disabled - ch47341 + //[Fact(Skip = SkipIfCannotCreateHttpServer)] + //public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync() + //{ + // await WithServerAsync(async server => + // { + // server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + // var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + // .UseReport(true).Build(); + + // using (var requestor = new FeatureFlagRequestor(config, _user)) + // { + // var resp = await requestor.FeatureFlagsAsync(); + // Assert.Equal(200, resp.statusCode); + // Assert.Equal(_allDataJson, resp.jsonResponse); + + // var req = server.GetLastRequest(); + // Assert.Equal("REPORT", req.Method); + // Assert.Equal($"/msdk/evalx/user", req.Path); + // Assert.Equal("", req.RawQuery); + // Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + + // //Assert.Equal("{\"key\":\"foo\"}", req.Body); + // // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, WireMock.Net + // // is not currently able to detect the body for REPORT requests: https://github.com/WireMock-Net/WireMock.Net/issues/290 + // } + // }); + //} + + //[Fact(Skip = SkipIfCannotCreateHttpServer)] + //public async Task GetFlagsUsesCorrectUriAndMethodInReportModeWithReasonsAsync() + //{ + // await WithServerAsync(async server => + // { + // server.ForAllRequests(r => r.WithJsonBody(_allDataJson)); + + // var config = Configuration.Builder(_mobileKey).BaseUri(new Uri(server.GetUrl())) + // .UseReport(true).EvaluationReasons(true).Build(); + + // using (var requestor = new FeatureFlagRequestor(config, _user)) + // { + // var resp = await requestor.FeatureFlagsAsync(); + // Assert.Equal(200, resp.statusCode); + // Assert.Equal(_allDataJson, resp.jsonResponse); + + // var req = server.GetLastRequest(); + // Assert.Equal("REPORT", req.Method); + // Assert.Equal($"/msdk/evalx/user", req.Path); + // Assert.Equal("?withReasons=true", req.RawQuery); + // Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + // } + // }); + //} } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index de480274..be54844e 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -48,7 +48,7 @@ public void InitGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -65,7 +65,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -107,7 +107,7 @@ public void InitFailsOn401Sync(UpdateMode mode) { try { - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); using (var client = TestUtil.CreateClient(config, _user)) { } } catch (Exception e) @@ -133,7 +133,7 @@ await WithServerAsync(async server => using (var log = new LogSinkScope()) { - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); // Currently the behavior of LdClient.InitAsync is somewhat inconsistent with LdClient.Init if there is // an unrecoverable error: LdClient.Init throws an exception, but LdClient.InitAsync returns a task that @@ -155,7 +155,7 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(false).Build(); + var config = BaseConfig(server).IsStreamingEnabled(false).Build(); var name = "Sue"; var anonUser = User.Builder((string)null).Name(name).Anonymous(true).Build(); @@ -186,7 +186,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -214,7 +214,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server).UseReport(false).IsStreamingEnabled(mode.IsStreaming).Build(); + var config = BaseConfig(server, mode).Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -245,7 +245,6 @@ public void OfflineClientUsesCachedFlagsSync() try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -285,7 +284,6 @@ await WithServerAsync(async server => try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -324,7 +322,6 @@ public void OfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -364,7 +361,6 @@ await WithServerAsync(async server => try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -409,7 +405,6 @@ public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor( try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -453,7 +448,6 @@ await WithServerAsync(async server => try { var config = BaseConfig(server) - .UseReport(false) .IsStreamingEnabled(false) .PersistFlagValues(true) .Build(); @@ -495,6 +489,12 @@ private IConfigurationBuilder BaseConfig(FluentMockServer server) .PersistFlagValues(false); // unless we're specifically testing flag caching, this helps to prevent test state contamination } + private IConfigurationBuilder BaseConfig(FluentMockServer server, UpdateMode mode) + { + return BaseConfig(server) + .IsStreamingEnabled(mode.IsStreaming); + } + private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) { server.ForAllRequests(r => diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index d1fcd60c..0b5b470b 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -49,7 +49,7 @@ private IMobileUpdateProcessor MobileStreamingProcessorStarted() [Fact] public void StreamUriInGetModeHasUser() { - var config = configBuilder.UseReport(false).Build(); + var config = configBuilder.Build(); MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(HttpMethod.Get, props.Method); @@ -59,41 +59,42 @@ public void StreamUriInGetModeHasUser() [Fact] public void StreamUriInGetModeHasReasonsParameterIfConfigured() { - var config = configBuilder.UseReport(false).EvaluationReasons(true).Build(); + var config = configBuilder.EvaluationReasons(true).Build(); MobileStreamingProcessorStarted(); var props = eventSourceFactory.ReceivedProperties; Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + encodedUser + "?withReasons=true"), props.StreamUri); } - [Fact] - public void StreamUriInReportModeHasNoUser() - { - var config = configBuilder.UseReport(true).Build(); - MobileStreamingProcessorStarted(); - var props = eventSourceFactory.ReceivedProperties; - Assert.Equal(new HttpMethod("REPORT"), props.Method); - Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH), props.StreamUri); - } - - [Fact] - public void StreamUriInReportModeHasReasonsParameterIfConfigured() - { - var config = configBuilder.UseReport(true).EvaluationReasons(true).Build(); - MobileStreamingProcessorStarted(); - var props = eventSourceFactory.ReceivedProperties; - Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + "?withReasons=true"), props.StreamUri); - } - - [Fact] - public async Task StreamRequestBodyInReportModeHasUser() - { - configBuilder.UseReport(true); - MobileStreamingProcessorStarted(); - var props = eventSourceFactory.ReceivedProperties; - var body = Assert.IsType(props.RequestBody); - var s = await body.ReadAsStringAsync(); - Assert.Equal(user.AsJson(), s); - } + // Report mode is currently disabled - ch47341 + //[Fact] + //public void StreamUriInReportModeHasNoUser() + //{ + // var config = configBuilder.UseReport(true).Build(); + // MobileStreamingProcessorStarted(); + // var props = eventSourceFactory.ReceivedProperties; + // Assert.Equal(new HttpMethod("REPORT"), props.Method); + // Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH), props.StreamUri); + //} + + //[Fact] + //public void StreamUriInReportModeHasReasonsParameterIfConfigured() + //{ + // var config = configBuilder.UseReport(true).EvaluationReasons(true).Build(); + // MobileStreamingProcessorStarted(); + // var props = eventSourceFactory.ReceivedProperties; + // Assert.Equal(new Uri(config.StreamUri, Constants.STREAM_REQUEST_PATH + "?withReasons=true"), props.StreamUri); + //} + + //[Fact] + //public async Task StreamRequestBodyInReportModeHasUser() + //{ + // configBuilder.UseReport(true); + // MobileStreamingProcessorStarted(); + // var props = eventSourceFactory.ReceivedProperties; + // var body = Assert.IsType(props.RequestBody); + // var s = await body.ReadAsStringAsync(); + // Assert.Equal(user.AsJson(), s); + //} [Fact] public void PUTstoresFeatureFlags() From 50f963cc74e74f82259682a1853479b78bbe05ef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 26 Aug 2019 14:47:10 -0700 Subject: [PATCH 239/254] refactor connection state management, replace Online property with Offline, etc. (#74) --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 4 +- .../ConfigurationBuilder.cs | 6 +- .../ConnectionManager.cs | 283 ++++++++++++++ ....cs => DefaultConnectivityStateManager.cs} | 8 +- src/LaunchDarkly.XamarinSdk/Factory.cs | 48 ++- ...anager.cs => IConnectivityStateManager.cs} | 3 +- src/LaunchDarkly.XamarinSdk/ILdClient.cs | 142 ++++--- .../LaunchDarkly.XamarinSdk.csproj | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 364 +++++++----------- src/LaunchDarkly.XamarinSdk/LockUtils.cs | 47 +++ .../MobilePollingProcessor.cs | 4 +- .../MobileStreamingProcessor.cs | 10 +- .../Resources/Resource.designer.cs | 101 +++++ .../LDClientEndToEndTests.cs | 121 ++++-- .../LdClientEventTests.cs | 5 +- .../LdClientTests.cs | 42 +- .../MobileStreamingProcessorTests.cs | 2 +- .../MockComponents.cs | 6 +- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 2 +- 19 files changed, 790 insertions(+), 410 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/ConnectionManager.cs rename src/LaunchDarkly.XamarinSdk/{MobileConnectionManager.cs => DefaultConnectivityStateManager.cs} (75%) rename src/LaunchDarkly.XamarinSdk/{IConnectionManager.cs => IConnectivityStateManager.cs} (50%) create mode 100644 src/LaunchDarkly.XamarinSdk/LockUtils.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index 3f78fe58..1d3949a3 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -43,7 +43,7 @@ public sealed class Configuration private readonly TimeSpan _userKeysFlushInterval; // Settable only for testing - internal readonly IConnectionManager _connectionManager; + internal readonly IConnectivityStateManager _connectivityStateManager; internal readonly IDeviceInfo _deviceInfo; internal readonly IEventProcessor _eventProcessor; internal readonly IFlagCacheManager _flagCacheManager; @@ -345,7 +345,7 @@ internal Configuration(ConfigurationBuilder builder) _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; - _connectionManager = builder._connectionManager; + _connectivityStateManager = builder._connectivityStateManager; _deviceInfo = builder._deviceInfo; _eventProcessor = builder._eventProcessor; _flagCacheManager = builder._flagCacheManager; diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index e3782890..72cdd11a 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -280,7 +280,7 @@ internal sealed class ConfigurationBuilder : IConfigurationBuilder internal TimeSpan _userKeysFlushInterval = Configuration.DefaultUserKeysFlushInterval; // Internal properties only settable for testing - internal IConnectionManager _connectionManager; + internal IConnectivityStateManager _connectivityStateManager; internal IDeviceInfo _deviceInfo; internal IEventProcessor _eventProcessor; internal IFlagCacheManager _flagCacheManager; @@ -491,9 +491,9 @@ public IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterva // and then call these methods before you have called any of the public methods (since // only these methods return ConfigurationBuilder rather than IConfigurationBuilder). - internal ConfigurationBuilder ConnectionManager(IConnectionManager connectionManager) + internal ConfigurationBuilder ConnectivityStateManager(IConnectivityStateManager connectivityStateManager) { - _connectionManager = connectionManager; + _connectivityStateManager = connectivityStateManager; return this; } diff --git a/src/LaunchDarkly.XamarinSdk/ConnectionManager.cs b/src/LaunchDarkly.XamarinSdk/ConnectionManager.cs new file mode 100644 index 00000000..a7e4aca8 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/ConnectionManager.cs @@ -0,0 +1,283 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Common.Logging; +using LaunchDarkly.Common; + +namespace LaunchDarkly.Xamarin +{ + /// + /// Manages our connection to LaunchDarkly, if any, and encapsulates all of the state that + /// determines whether we should have a connection or not. + /// + /// + /// Whenever the state of this object is modified by , + /// , , + /// or , it will decide whether to make a new connection, drop an existing + /// connection, both, or neither. If the caller wants to know when a new connection (if any) is + /// ready, it should await the returned task. + /// + /// The object begins in a non-started state, so regardless of what properties are set, it will not + /// make a connection until after has been called. + /// + internal sealed class ConnectionManager : IDisposable + { + private static readonly ILog Log = LogManager.GetLogger(typeof(ConnectionManager)); + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private bool _disposed = false; + private bool _started = false; + private bool _initialized = false; + private bool _forceOffline = false; + private bool _networkEnabled = false; + private IMobileUpdateProcessor _updateProcessor = null; + private Func _updateProcessorFactory = null; + + // Note that these properties do not have simple setter methods, because the setters all + // need to return Tasks. + + /// + /// True if we are in offline mode ( was set to true). + /// + public bool ForceOffline => LockUtils.WithReadLock(_lock, () => _forceOffline); + + /// + /// True if we have been told there is network connectivity ( + /// was set to true). + /// + public bool NetworkEnabled => LockUtils.WithReadLock(_lock, () => _networkEnabled); + + /// + /// True if we made a successful LaunchDarkly connection or do not need to make one (see + /// ). + /// + public bool Initialized => LockUtils.WithReadLock(_lock, () => _initialized); + + /// + /// Sets whether the client should always be offline, and attempts to connect if appropriate. + /// + /// + /// Besides updating the value of the property, we do the + /// following: + /// + /// If forceOffline is true, we drop our current connection (if any), and we will not + /// make any connections no matter what other properties are changed as long as this property is + /// still true. + /// + /// If forceOffline is false and we already have a connection, nothing happens. + /// + /// If forceOffline is false and we have no connection, but other conditions disallow + /// making a connection (or we do not have an update processor factory), nothing happens. + /// + /// If forceOffline is false, and we do not yet have a connection, and no other + /// conditions disallow making a connection, and we have an update processor factory, + /// we create an update processor and tell it to start. + /// + /// The returned task is immediately completed unless we are making a new connection, in which + /// case it is completed when the update processor signals success or failure. The task yields + /// a true result if we successfully made a connection or if we decided not to connect + /// because we are in offline mode. In other words, the result is true if + /// is true. + /// + /// true if the client should always be offline + /// a task as described above + public Task SetForceOffline(bool forceOffline) + { + return LockUtils.WithWriteLock(_lock, () => + { + if (_disposed || _forceOffline == forceOffline) + { + return Task.FromResult(false); + } + _forceOffline = forceOffline; + Log.InfoFormat("Offline mode is now {0}", forceOffline); + return OpenOrCloseConnectionIfNecessary(); // not awaiting + }); + } + + /// + /// Sets whether we should be able to make network connections, and attempts to connect if appropriate. + /// + /// + /// Besides updating the value of the property, we do the + /// following: + /// + /// If networkEnabled is false, we drop our current connection (if any), and we will not + /// make any connections no matter what other properties are changed as long as this property is + /// still true. + /// + /// If networkEnabled is true and we already have a connection, nothing happens. + /// + /// If networkEnabled is true and we have no connection, but other conditions disallow + /// making a connection (or we do not have an update processor factory), nothing happens. + /// + /// If networkEnabled is true, and we do not yet have a connection, and no other + /// conditions disallow making a connection, and we have an update processor factory, + /// we create an update processor and tell it to start. + /// + /// The returned task is immediately completed unless we are making a new connection, in which + /// case it is completed when the update processor signals success or failure. The task yields + /// a true result if we successfully made a connection or if we decided not to connect + /// because we are in offline mode. In other words, the result is true if + /// is true. + /// + /// true if we think we can make network connections + /// a task as described above + public Task SetNetworkEnabled(bool networkEnabled) + { + return LockUtils.WithWriteLock(_lock, () => + { + if (_disposed || _networkEnabled == networkEnabled) + { + return Task.FromResult(false); + } + _networkEnabled = networkEnabled; + Log.InfoFormat("Network availability is now {0}", networkEnabled); + return OpenOrCloseConnectionIfNecessary(); // not awaiting + }); + } + + /// + /// Sets the factory function for creating an update processor, and attempts to connect if + /// appropriate. + /// + /// + /// The factory function encapsulates all the information that takes into + /// account when making a connection, i.e. whether we are in streaming or polling mode, the + /// polling interval, and the curent user. ConnectionManager itself has no knowledge of + /// those things. + /// + /// Besides updating the private factory function field, we do the following: + /// + /// If the function is null, we drop our current connection (if any), and we will not make + /// any connections no matter what other properties are changed as long as it is still null. + /// + /// If it is non-null and we already have the same factory function, nothing happens. + /// + /// If it is non-null and we do not already have the same factory function, but other conditions + /// disallow making a connection, nothing happens. + /// + /// If it is non-null and we do not already have the same factory function, and no other + /// conditions disallow making a connection, we create an update processor and tell it to start. + /// In this case, we also reset to false if resetInitialized is + /// true. + /// + /// The returned task is immediately completed unless we are making a new connection, in which + /// case it is completed when the update processor signals success or failure. The task yields + /// a true result if we successfully made a connection or if we decided not to connect + /// because we are in offline mode. In other words, the result is true if + /// is true. + /// + /// a factory function or null + /// true if we should reset the initialized state (e.g. if we + /// are switching users + /// a task as described above + public Task SetUpdateProcessorFactory(Func updateProcessorFactory, bool resetInitialized) + { + return LockUtils.WithWriteLock(_lock, () => + { + if (_disposed || _updateProcessorFactory == updateProcessorFactory) + { + return Task.FromResult(false); + } + _updateProcessorFactory = updateProcessorFactory; + _updateProcessor?.Dispose(); + _updateProcessor = null; + if (resetInitialized) + { + _initialized = false; + } + return OpenOrCloseConnectionIfNecessary(); // not awaiting + }); + } + + /// + /// Tells the ConnectionManager that it can go ahead and connect if appropriate. + /// + /// a task which will yield true if this method results in a successful connection, or + /// if we are in offline mode and don't need to make a connection + public Task Start() + { + return LockUtils.WithWriteLock(_lock, () => + { + if (_started) + { + return Task.FromResult(_initialized); + } + _started = true; + return OpenOrCloseConnectionIfNecessary(); // not awaiting + }); + } + + public void Dispose() + { + IMobileUpdateProcessor processor = null; + LockUtils.WithWriteLock(_lock, () => + { + if (_disposed) + { + return; + } + processor = _updateProcessor; + _updateProcessor = null; + _updateProcessorFactory = null; + _disposed = true; + }); + processor?.Dispose(); + } + + // This method is called while _lock is being held. If we're starting up a new connection, we do + // *not* wait for it to succeed; we return a Task that will be completed once it succeeds. In all + // other cases we return an immediately-completed Task. + + private Task OpenOrCloseConnectionIfNecessary() + { + if (!_started) + { + return Task.FromResult(false); + } + if (_networkEnabled && !_forceOffline) + { + if (_updateProcessor == null && _updateProcessorFactory != null) + { + _updateProcessor = _updateProcessorFactory(); + return _updateProcessor.Start() + .ContinueWith(SetInitializedIfUpdateProcessorStartedSuccessfully); + } + } + else + { + _updateProcessor?.Dispose(); + _updateProcessor = null; + _initialized = true; + return Task.FromResult(true); + } + return Task.FromResult(false); + } + + // When this method is called, we are no longer holding the lock. + + private bool SetInitializedIfUpdateProcessorStartedSuccessfully(Task task) + { + if (task.IsCompleted) + { + if (task.IsFaulted) + { + // Don't let exceptions from the update processor propagate up into the SDK. Just say we didn't initialize. + Log.ErrorFormat("Failed to initialize LaunchDarkly connection: {0}", Util.ExceptionMessage(task.Exception)); + return false; + } + var success = task.Result; + if (success) + { + LockUtils.WithWriteLock(_lock, () => + { + _initialized = true; + }); + return true; + } + } + return false; + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs b/src/LaunchDarkly.XamarinSdk/DefaultConnectivityStateManager.cs similarity index 75% rename from src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs rename to src/LaunchDarkly.XamarinSdk/DefaultConnectivityStateManager.cs index b576892a..8e39748a 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileConnectionManager.cs +++ b/src/LaunchDarkly.XamarinSdk/DefaultConnectivityStateManager.cs @@ -3,18 +3,18 @@ namespace LaunchDarkly.Xamarin { - internal sealed class MobileConnectionManager : IConnectionManager + internal sealed class DefaultConnectivityStateManager : IConnectivityStateManager { - internal Action ConnectionChanged; + public Action ConnectionChanged { get; set; } - internal MobileConnectionManager() + internal DefaultConnectivityStateManager() { UpdateConnectedStatus(); Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; } bool isConnected; - bool IConnectionManager.IsConnected + bool IConnectivityStateManager.IsConnected { get { return isConnected; } set diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index 043f854e..65ce2cba 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -27,38 +27,34 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura } } - internal static IConnectionManager CreateConnectionManager(Configuration configuration) + internal static IConnectivityStateManager CreateConnectivityStateManager(Configuration configuration) { - return configuration._connectionManager ?? new MobileConnectionManager(); + return configuration._connectivityStateManager ?? new DefaultConnectivityStateManager(); } - internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, User user, - IFlagCacheManager flagCacheManager, TimeSpan? overridePollingInterval, - bool disableStreaming) + internal static Func CreateUpdateProcessorFactory(Configuration configuration, User user, + IFlagCacheManager flagCacheManager, bool inBackground) { - if (configuration.Offline) + return () => { - Log.InfoFormat("Starting LaunchDarkly client in offline mode"); - return new NullUpdateProcessor(); - } - - if (configuration._updateProcessorFactory != null) - { - return configuration._updateProcessorFactory(configuration, flagCacheManager, user); - } + if (configuration._updateProcessorFactory != null) + { + return configuration._updateProcessorFactory(configuration, flagCacheManager, user); + } - if (configuration.IsStreamingEnabled && !disableStreaming) - { - return new MobileStreamingProcessor(configuration, flagCacheManager, user, null); - } - else - { - var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); - return new MobilePollingProcessor(featureFlagRequestor, - flagCacheManager, - user, - overridePollingInterval ?? configuration.PollingInterval); - } + if (configuration.IsStreamingEnabled && !inBackground) + { + return new MobileStreamingProcessor(configuration, flagCacheManager, user, null); + } + else + { + var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); + return new MobilePollingProcessor(featureFlagRequestor, + flagCacheManager, + user, + inBackground ? configuration.BackgroundPollingInterval : configuration.PollingInterval); + } + }; } internal static IEventProcessor CreateEventProcessor(Configuration configuration) diff --git a/src/LaunchDarkly.XamarinSdk/IConnectionManager.cs b/src/LaunchDarkly.XamarinSdk/IConnectivityStateManager.cs similarity index 50% rename from src/LaunchDarkly.XamarinSdk/IConnectionManager.cs rename to src/LaunchDarkly.XamarinSdk/IConnectivityStateManager.cs index fe7dbaf3..ec36679e 100644 --- a/src/LaunchDarkly.XamarinSdk/IConnectionManager.cs +++ b/src/LaunchDarkly.XamarinSdk/IConnectivityStateManager.cs @@ -2,8 +2,9 @@ namespace LaunchDarkly.Xamarin { - public interface IConnectionManager + internal interface IConnectivityStateManager { bool IsConnected { get; set; } + Action ConnectionChanged { get; set; } } } diff --git a/src/LaunchDarkly.XamarinSdk/ILdClient.cs b/src/LaunchDarkly.XamarinSdk/ILdClient.cs index ed8a251f..fb4dc3d2 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdClient.cs @@ -8,35 +8,82 @@ namespace LaunchDarkly.Xamarin { public interface ILdClient : IDisposable { - /// - /// Returns the current version number of the LaunchDarkly client. - /// - Version Version { get; } - /// /// Returns a boolean value indicating LaunchDarkly connection and flag state within the client. /// /// - /// When you first start the client, once Init or InitAsync has returned, - /// Initialized() should be true if and only if either 1. it connected to LaunchDarkly + /// When you first start the client, once Init or InitAsync has returned, + /// Initialized should be true if and only if either 1. it connected to LaunchDarkly /// and successfully retrieved flags, or 2. it started in offline mode so /// there's no need to connect to LaunchDarkly. So if the client timed out trying to - /// connect to LD, then Initialized is false (even if we do have cached flags). + /// connect to LD, then Initialized is false (even if we do have cached flags). /// If the client connected and got a 401 error, Initialized is false. /// This serves the purpose of letting the app know that - /// there was a problem of some kind. Initialized() will be temporarily false during the - /// time in between calling Identify and receiving the new user's flags. It will also be false - /// if you switch users with Identify and the client is unable + /// there was a problem of some kind. Initialized will be temporarily false during the + /// time in between calling Identify and receiving the new user's flags. It will also be false + /// if you switch users with Identify and the client is unable /// to get the new user's flags from LaunchDarkly. /// - /// true if the client has connected to LaunchDarkly and has flags or if the config is set offline, or false if it couldn't connect. - bool Initialized(); + bool Initialized { get; } + + /// + /// Indicates whether the SDK is configured to be always offline. + /// + /// + /// This is initially true if you set it to true in the configuration with . + /// However, you can change it at any time to allow the client to go online, or force it to go offline, + /// using or . + /// + /// When Offline is false, the SDK connects to LaunchDarkly if possible, but this does not guarantee + /// that the connection is successful. There is currently no mechanism to detect whether the SDK is currently + /// connected to LaunchDarkly. + /// + bool Offline { get; } /// - /// Tests whether the client is being used in offline mode. + /// Sets whether the SDK should be always offline. /// - /// true if the client is offline - bool IsOffline(); + /// + /// This is equivalent to , but as a synchronous method. + /// + /// If you set the property to true, any existing connection will be dropped, and the method immediately + /// returns false. + /// + /// If you set it to false when it was previously true, but no connection can be made because the network + /// is not available, the method immediately returns false, but the SDK will attempt to connect later if + /// the network becomes available. + /// + /// If you set it to false when it was previously true, and the network is available, the SDK will attempt + /// to connect to LaunchDarkly. If the connection succeeds within the interval maxWaitTime, the + /// method returns true. If the connection permanently fails (e.g. if the mobile key is invalid), the + /// method returns false. If the connection attempt is still in progress after maxWaitTime elapses, + /// the method returns false, but the connection might succeed later. + /// + /// true if the client should be always offline + /// the maximum length of time to wait for a connection + /// true if a new connection was successfully made + bool SetOffline(bool value, TimeSpan maxWaitTime); + + /// + /// Sets whether the SDK should be always offline. + /// + /// + /// This is equivalent to , but as an asynchronous method. + /// + /// If you set the property to true, any existing connection will be dropped, and the task immediately + /// yields false. + /// + /// If you set it to false when it was previously true, but no connection can be made because the network + /// is not available, the task immediately yields false, but the SDK will attempt to connect later if + /// the network becomes available. + /// + /// If you set it to false when it was previously true, and the network is available, the SDK will attempt + /// to connect to LaunchDarkly. If and when the connection succeeds, the task yields true. If and when the + /// connection permanently fails (e.g. if the mobile key is invalid), the task yields false. + /// + /// true if the client should be always offline + /// a task that yields true if a new connection was successfully made + Task SetOfflineAsync(bool value); /// /// Returns the boolean value of a feature flag for a given flag key. @@ -161,24 +208,6 @@ public interface ILdClient : IDisposable /// the name of the event void Track(string eventName); - /// - /// Gets or sets the online status of the client. - /// - /// - /// The setter is equivalent to calling ; if you are going from offline to - /// online, it does not wait until the connection has been established. If you want to wait for the - /// connection, call and then use await. - /// - /// true if online; otherwise, false. - bool Online { get; set; } - - /// - /// Sets the client to be online or not. - /// - /// a Task - /// true if the client should be online - Task SetOnlineAsync(bool value); - /// /// Returns a map from feature flag keys to feature flag values for the current user. /// @@ -220,37 +249,38 @@ public interface ILdClient : IDisposable event EventHandler FlagChanged; /// - /// Changes the current user. + /// Changes the current user, requests flags for that user from LaunchDarkly if we are online, and generates + /// an analytics event to tell LaunchDarkly about the user. /// /// - /// This both sets the current user for the purpose of flag evaluations and also generates an analytics event to - /// tell LaunchDarkly about the user. - /// - /// Identify waits and blocks the current thread until the SDK has received feature flag values for the - /// new user from LaunchDarkly. If you do not want to wait, consider . + /// This is equivalent to , but as a synchronous method. + /// + /// If the SDK is online, Identify waits to receive feature flag values for the new user from + /// LaunchDarkly. If it receives the new flag values before maxWaitTime has elapsed, it returns true. + /// If the timeout elapses, it returns false (although the SDK might still receive the flag values later). + /// If we do not need to request flags from LaunchDarkly because we are in offline mode, it returns true. + /// + /// If you do not want to wait, you can either set maxWaitTime to zero or call . /// /// the new user - void Identify(User user); + /// the maximum time to wait for the new flag values + /// true if new flag values were obtained + bool Identify(User user, TimeSpan maxWaitTime); /// - /// Changes the current user. + /// Changes the current user, requests flags for that user from LaunchDarkly if we are online, and generates + /// an analytics event to tell LaunchDarkly about the user. /// /// - /// This both sets the current user for the purpose of flag evaluations and also generates an analytics event to - /// tell LaunchDarkly about the user. - /// - /// IdentifyAsync is meant to be used from asynchronous code. It returns a Task that is resolved once the - /// SDK has received feature flag values for the new user from LaunchDarkly. - /// - /// - /// // Within asynchronous code, use await to wait for the task to be resolved - /// await client.IdentifyAsync(user); + /// This is equivalent to , but as an asynchronous method. /// - /// // Or, if you want to let the flag values be retrieved in the background instead of waiting: - /// Task.Run(() => client.IdentifyAsync(user)); - /// - /// the user to register - Task IdentifyAsync(User user); + /// If the SDK is online, the returned task is completed once the SDK has received feature flag values for the + /// new user from LaunchDarkly, or received an unrecoverable error; it yields true for success or false for an + /// error. If the SDK is offline, the returned task is completed immediately and yields true. + /// + /// the new user + /// a task that yields true if new flag values were obtained + Task IdentifyAsync(User user); /// /// Flushes all pending events. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index be708926..9e0a07d4 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -74,6 +74,6 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 90dce51d..0a03cb63 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -19,65 +19,62 @@ public sealed class LdClient : ILdClient { private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); - static volatile LdClient _instance; - static volatile User _user; - - bool initialized; - - static readonly object _createInstanceLock = new object(); static readonly EventFactory _eventFactoryDefault = EventFactory.Default; static readonly EventFactory _eventFactoryWithReasons = EventFactory.DefaultWithReasons; - readonly Configuration _config; - readonly SemaphoreSlim _connectionLock; + static readonly object _createInstanceLock = new object(); + static volatile LdClient _instance; + // Immutable client state + readonly Configuration _config; + readonly ConnectionManager _connectionManager; readonly IDeviceInfo deviceInfo; - readonly IConnectionManager connectionManager; + readonly IConnectivityStateManager _connectivityStateManager; readonly IEventProcessor eventProcessor; readonly IFlagCacheManager flagCacheManager; internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing readonly IPersistentStorage persister; - // These LdClient fields are not readonly because they change according to online status - internal volatile IMobileUpdateProcessor updateProcessor; - volatile bool _disableStreaming; - volatile bool _online; + // Mutable client state (some state is also in the ConnectionManager) + readonly ReaderWriterLockSlim _userLock = new ReaderWriterLockSlim(); + volatile User _user; + volatile bool _inBackground; /// /// The singleton instance used by your application throughout its lifetime. Once this exists, you cannot /// create a new client instance unless you first call on this one. - /// - /// Use the designated static methods or - /// to set this LdClient instance. /// - /// The LdClient instance. + /// + /// Use the static factory methods or + /// to set this LdClient instance. + /// public static LdClient Instance => _instance; + /// + /// Returns the current version number of the LaunchDarkly client. + /// + public static Version Version => MobileClientEnvironment.Instance.Version; + /// /// The Configuration instance used to setup the LdClient. /// - /// The Configuration instance. public Configuration Config => _config; /// - /// The User for the LdClient operations. + /// The current user for all SDK operations. /// - /// The User. - public User User => _user; + /// + /// This is initially the user specified for or + /// , but can be changed later with + /// or . + /// + public User User => LockUtils.WithReadLock(_userLock, () => _user); - /// - public bool Online - { - get => _online; - set - { - if (value == _online) - { - return; - } - AsyncUtils.WaitSafely(() => SetOnlineAsync(value)); - } - } + /// + public bool Offline => _connectionManager.ForceOffline; + + /// + public bool Initialized => _connectionManager.Initialized; /// /// Indicates which platform the SDK is built for. @@ -97,11 +94,18 @@ public bool Online /// that these platform-specific behaviors are not working correctly, you may want to check this property to /// make sure you are not for some reason running the .NET Standard SDK on a phone. /// - public static PlatformType PlatformType + public static PlatformType PlatformType => UserMetadata.PlatformType; + + /// + public event EventHandler FlagChanged { - get + add + { + flagChangedEventManager.FlagChanged += value; + } + remove { - return UserMetadata.PlatformType; + flagChangedEventManager.FlagChanged -= value; } } @@ -118,13 +122,6 @@ public static PlatformType PlatformType _config = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _connectionLock = new SemaphoreSlim(1, 1); - - if (configuration.Offline) - { - initialized = true; - } - persister = Factory.CreatePersistentStorage(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagChangedEventManager = Factory.CreateFlagChangedEventManager(configuration); @@ -132,16 +129,47 @@ public static PlatformType PlatformType _user = DecorateUser(user); flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagChangedEventManager, User); - connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager, null, false); eventProcessor = Factory.CreateEventProcessor(configuration); + _connectionManager = new ConnectionManager(); + _connectionManager.SetForceOffline(configuration.Offline); + if (configuration.Offline) + { + Log.InfoFormat("Starting LaunchDarkly client in offline mode"); + } + _connectionManager.SetUpdateProcessorFactory( + Factory.CreateUpdateProcessorFactory(configuration, User, flagCacheManager, _inBackground), + true + ); + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); - SetupConnectionManager(); + _connectivityStateManager = Factory.CreateConnectivityStateManager(configuration); + _connectivityStateManager.ConnectionChanged += networkAvailable => + { + Log.DebugFormat("Setting online to {0} due to a connectivity change event", networkAvailable); + _ = _connectionManager.SetNetworkEnabled(networkAvailable); // do not await the result + }; + _connectionManager.SetNetworkEnabled(_connectivityStateManager.IsConnected); + BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; } + void Start(TimeSpan maxWaitTime) + { + var success = AsyncUtils.WaitSafely(() => _connectionManager.Start(), maxWaitTime); + if (!success) + { + Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", + maxWaitTime.TotalMilliseconds); + } + } + + async Task StartAsync() + { + await _connectionManager.Start(); + } + /// /// Creates and returns a new LdClient singleton instance, then starts the workflow for /// fetching feature flags. @@ -216,16 +244,7 @@ public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTim } var c = CreateInstance(config, user); - - if (c.Online) - { - if (!c.StartUpdateProcessor(maxWaitTime)) - { - Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", - maxWaitTime.TotalMilliseconds); - } - } - + c.Start(maxWaitTime); return c; } @@ -243,19 +262,11 @@ public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTim /// The client configuration object /// The user needed for client operations. Must not be null. /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static Task InitAsync(Configuration config, User user) + public static async Task InitAsync(Configuration config, User user) { var c = CreateInstance(config, user); - - if (c.Online) - { - Task t = c.StartUpdateProcessorAsync(); - return t.ContinueWith((result) => c); - } - else - { - return Task.FromResult(c); - } + await c.StartAsync(); + return c; } static LdClient CreateInstance(Configuration configuration, User user) @@ -269,110 +280,78 @@ static LdClient CreateInstance(Configuration configuration, User user) var c = new LdClient(configuration, user); _instance = c; - Log.InfoFormat("Initialized LaunchDarkly Client {0}", c.Version); + Log.InfoFormat("Initialized LaunchDarkly Client {0}", Version); return c; } } - void SetupConnectionManager() + /// + public bool SetOffline(bool value, TimeSpan maxWaitTime) { - if (connectionManager is MobileConnectionManager mobileConnectionManager) - { - mobileConnectionManager.ConnectionChanged += MobileConnectionManager_ConnectionChanged; - Log.InfoFormat("The mobile client connection changed online to {0}", - connectionManager.IsConnected); - } - _online = connectionManager.IsConnected; + return AsyncUtils.WaitSafely(() => SetOfflineAsync(value), maxWaitTime); } - public async Task SetOnlineAsync(bool value) + /// + public async Task SetOfflineAsync(bool value) { - await _connectionLock.WaitAsync(); - try - { - if (value == _online) - { - return; - } - _online = value; - if (_online) - { - await RestartUpdateProcessorAsync(Config.PollingInterval); - } - else - { - ClearUpdateProcessor(); - } - } - finally - { - _connectionLock.Release(); - } - - return; - } - - void MobileConnectionManager_ConnectionChanged(bool isOnline) - { - Log.DebugFormat("Setting online to {0} due to a connectivity change event", isOnline); - Online = isOnline; + await _connectionManager.SetForceOffline(value); } - /// + /// public bool BoolVariation(string key, bool defaultValue = false) { return VariationInternal(key, defaultValue, ValueTypes.Bool, _eventFactoryDefault).Value; } - /// + /// public EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false) { return VariationInternal(key, defaultValue, ValueTypes.Bool, _eventFactoryWithReasons); } - /// + /// public string StringVariation(string key, string defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.String, _eventFactoryDefault).Value; } - /// + /// public EvaluationDetail StringVariationDetail(string key, string defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.String, _eventFactoryWithReasons); } - /// + /// public float FloatVariation(string key, float defaultValue = 0) { return VariationInternal(key, defaultValue, ValueTypes.Float, _eventFactoryDefault).Value; } - /// + /// public EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0) { return VariationInternal(key, defaultValue, ValueTypes.Float, _eventFactoryWithReasons); } - /// + /// public int IntVariation(string key, int defaultValue = 0) { return VariationInternal(key, defaultValue, ValueTypes.Int, _eventFactoryDefault).Value; } - /// + /// public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0) { return VariationInternal(key, defaultValue, ValueTypes.Int, _eventFactoryWithReasons); } - /// + /// public ImmutableJsonValue JsonVariation(string key, ImmutableJsonValue defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.Json, _eventFactoryDefault).Value; } - /// + /// public EvaluationDetail JsonVariationDetail(string key, ImmutableJsonValue defaultValue) { return VariationInternal(key, defaultValue, ValueTypes.Json, _eventFactoryWithReasons); @@ -389,7 +368,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => var flag = flagCacheManager.FlagForUser(featureKey, User); if (flag == null) { - if (!Initialized()) + if (!Initialized) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default value"); eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, @@ -406,7 +385,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => } else { - if (!Initialized()) + if (!Initialized) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning cached value"); } @@ -440,51 +419,44 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => return result; } - /// + /// public IDictionary AllFlags() { return flagCacheManager.FlagsForUser(User) .ToDictionary(p => p.Key, p => ImmutableJsonValue.FromSafeValue(p.Value.value)); } - /// + /// public void Track(string eventName, ImmutableJsonValue data) { eventProcessor.SendEvent(_eventFactoryDefault.NewCustomEvent(eventName, User, data.AsJToken())); } - /// + /// public void Track(string eventName) { Track(eventName, ImmutableJsonValue.Null); } - /// - public bool Initialized() - { - return initialized; - } - - /// - public bool IsOffline() - { - return !_online; - } - - /// + /// public void Flush() { eventProcessor.Flush(); } - /// - public void Identify(User user) + /// + public bool Identify(User user, TimeSpan maxWaitTime) { - AsyncUtils.WaitSafely(() => IdentifyAsync(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return AsyncUtils.WaitSafely(() => IdentifyAsync(user), maxWaitTime); } - /// - public async Task IdentifyAsync(User user) + /// + public async Task IdentifyAsync(User user) { if (user == null) { @@ -493,71 +465,17 @@ public async Task IdentifyAsync(User user) User newUser = DecorateUser(user); - await _connectionLock.WaitAsync(); - try + LockUtils.WithWriteLock(_userLock, () => { _user = newUser; - initialized = false; - await RestartUpdateProcessorAsync(Config.PollingInterval); - } - finally - { - _connectionLock.Release(); - } + }); eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(newUser)); - } - - bool StartUpdateProcessor(TimeSpan maxWaitTime) - { - if (Online) - { - var successfulConnection = AsyncUtils.WaitSafely(() => updateProcessor.Start(), maxWaitTime); - initialized = successfulConnection; - return successfulConnection; - } - else - { - return true; - } - } - - async Task StartUpdateProcessorAsync() - { - if (Online) - { - var successfulConnection = await updateProcessor.Start(); - if (successfulConnection) - { - initialized = true; - } - return successfulConnection; - } - else - { - return true; - } - } - - async Task RestartUpdateProcessorAsync(TimeSpan pollingInterval) - { - ClearAndSetUpdateProcessor(pollingInterval); - await StartUpdateProcessorAsync(); - } - void ClearAndSetUpdateProcessor(TimeSpan pollingInterval) - { - ClearUpdateProcessor(); - updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager, pollingInterval, _disableStreaming); - } - - void ClearUpdateProcessor() - { - if (updateProcessor != null) - { - updateProcessor.Dispose(); - updateProcessor = new NullUpdateProcessor(); - } + return await _connectionManager.SetUpdateProcessorFactory( + Factory.CreateUpdateProcessorFactory(_config, user, flagCacheManager, _inBackground), + true + ); } User DecorateUser(User user) @@ -604,7 +522,7 @@ void Dispose(bool disposing) Log.InfoFormat("Shutting down the LaunchDarkly client"); BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; - updateProcessor.Dispose(); + _connectionManager.Dispose(); eventProcessor.Dispose(); // Reset the static Instance to null *if* it was referring to this instance @@ -617,31 +535,9 @@ void Dispose(bool disposing) Interlocked.CompareExchange(ref _instance, null, this); } - /// - public Version Version - { - get - { - return MobileClientEnvironment.Instance.Version; - } - } - - /// - public event EventHandler FlagChanged - { - add - { - flagChangedEventManager.FlagChanged += value; - } - remove - { - flagChangedEventManager.FlagChanged -= value; - } - } - internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventArgs args) { - AsyncUtils.WaitSafely(() => OnBackgroundModeChangedAsync(sender, args)); + _ = OnBackgroundModeChangedAsync(sender, args); // do not wait for the result } internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) @@ -649,23 +545,23 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh Log.DebugFormat("Background mode is changing to {0}", args.IsInBackground); if (args.IsInBackground) { - ClearUpdateProcessor(); - _disableStreaming = true; - if (Config.EnableBackgroundUpdating) - { - Log.Debug("Background updating is enabled, starting polling processor"); - await RestartUpdateProcessorAsync(Config.BackgroundPollingInterval); - } - else + _inBackground = true; + if (!Config.EnableBackgroundUpdating) { Log.Debug("Background updating is disabled"); + await _connectionManager.SetUpdateProcessorFactory(null, false); + return; } + Log.Debug("Background updating is enabled, starting polling processor"); } else { - _disableStreaming = false; - await RestartUpdateProcessorAsync(Config.PollingInterval); + _inBackground = false; } + await _connectionManager.SetUpdateProcessorFactory( + Factory.CreateUpdateProcessorFactory(_config, User, flagCacheManager, _inBackground), + false // don't reset initialized state because the user is still the same + ); } } } diff --git a/src/LaunchDarkly.XamarinSdk/LockUtils.cs b/src/LaunchDarkly.XamarinSdk/LockUtils.cs new file mode 100644 index 00000000..338c35ea --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/LockUtils.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; + +namespace LaunchDarkly.Xamarin +{ + internal static class LockUtils + { + public static T WithReadLock(ReaderWriterLockSlim rwLock, Func fn) + { + rwLock.EnterReadLock(); + try + { + return fn(); + } + finally + { + rwLock.ExitReadLock(); + } + } + + public static T WithWriteLock(ReaderWriterLockSlim rwLock, Func fn) + { + rwLock.EnterWriteLock(); + try + { + return fn(); + } + finally + { + rwLock.ExitWriteLock(); + } + } + + public static void WithWriteLock(ReaderWriterLockSlim rwLock, Action a) + { + rwLock.EnterWriteLock(); + try + { + a(); + } + finally + { + rwLock.ExitWriteLock(); + } + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs index a76552f3..4c9864f7 100644 --- a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs @@ -93,9 +93,7 @@ private async Task UpdateTaskAsync() } catch (Exception ex) { - Log.ErrorFormat("Error Updating features: '{0}'", - ex, - Util.ExceptionMessage(ex)); + Log.ErrorFormat("Error Updating features: '{0}'", Util.ExceptionMessage(ex)); } } diff --git a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs index 89490a80..77a30a44 100644 --- a/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobileStreamingProcessor.cs @@ -46,9 +46,9 @@ bool IMobileUpdateProcessor.Initialized() return _streamManager.Initialized; } - Task IMobileUpdateProcessor.Start() + async Task IMobileUpdateProcessor.Start() { - return _streamManager.Start(); + return await _streamManager.Start(); } #endregion @@ -100,8 +100,7 @@ Task IStreamProcessor.HandleMessage(StreamManager streamManager, string messageT } catch (Exception ex) { - Log.ErrorFormat("Error parsing PATCH message {0}: '{1}'", - ex, messageData, Util.ExceptionMessage(ex)); + Log.ErrorFormat("Error parsing PATCH message {0}: {1}", messageData, Util.ExceptionMessage(ex)); } break; } @@ -116,8 +115,7 @@ Task IStreamProcessor.HandleMessage(StreamManager streamManager, string messageT } catch (Exception ex) { - Log.ErrorFormat("Error parsing DELETE message {0}: '{1}'", - ex, messageData, Util.ExceptionMessage(ex)); + Log.ErrorFormat("Error parsing DELETE message {0}: {1}", messageData, Util.ExceptionMessage(ex)); } break; } diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs index f352a1ff..bdd99699 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Resources/Resource.designer.cs @@ -26,6 +26,107 @@ static Resource() public static void UpdateIdValues() { + global::LaunchDarkly.XamarinSdk.Resource.Attribute.font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.font; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderAuthority; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderCerts; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchStrategy; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderFetchTimeout; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderPackage; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontProviderQuery; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Attribute.fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Attribute.fontWeight; + global::LaunchDarkly.XamarinSdk.Resource.Boolean.abc_action_bar_embed_tabs = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Boolean.abc_action_bar_embed_tabs; + global::LaunchDarkly.XamarinSdk.Resource.Color.notification_action_color_filter = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_action_color_filter; + global::LaunchDarkly.XamarinSdk.Resource.Color.notification_icon_bg_color = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.notification_icon_bg_color; + global::LaunchDarkly.XamarinSdk.Resource.Color.ripple_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.ripple_material_light; + global::LaunchDarkly.XamarinSdk.Resource.Color.secondary_text_default_material_light = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Color.secondary_text_default_material_light; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_horizontal_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_inset_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_inset_vertical_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_horizontal_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_horizontal_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_button_padding_vertical_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_button_padding_vertical_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.compat_control_corner_material = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.compat_control_corner_material; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_icon_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_action_text_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_action_text_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_big_circle_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_big_circle_margin; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_content_margin_start = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_content_margin_start; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_height = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_height; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_large_icon_width = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_large_icon_width; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_main_column_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_main_column_padding_top; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_media_narrow_margin = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_media_narrow_margin; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_icon_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_icon_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_right_side_padding_top = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_right_side_padding_top; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_background_padding = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_background_padding; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_small_icon_size_as_large = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_small_icon_size_as_large; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_subtext_size = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_subtext_size; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad; + global::LaunchDarkly.XamarinSdk.Resource.Dimension.notification_top_pad_large_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Dimension.notification_top_pad_large_text; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_action_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_action_background; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_normal; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_low_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_low_pressed; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_bg_normal_pressed = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_bg_normal_pressed; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_icon_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_icon_background; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_template_icon_low_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_template_icon_low_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notification_tile_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notification_tile_bg; + global::LaunchDarkly.XamarinSdk.Resource.Drawable.notify_panel_notification_icon_bg = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Drawable.notify_panel_notification_icon_bg; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_container; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_divider = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_divider; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_image = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_image; + global::LaunchDarkly.XamarinSdk.Resource.Id.action_text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.action_text; + global::LaunchDarkly.XamarinSdk.Resource.Id.actions = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.actions; + global::LaunchDarkly.XamarinSdk.Resource.Id.async = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.async; + global::LaunchDarkly.XamarinSdk.Resource.Id.blocking = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.blocking; + global::LaunchDarkly.XamarinSdk.Resource.Id.chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.chronometer; + global::LaunchDarkly.XamarinSdk.Resource.Id.forever = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.forever; + global::LaunchDarkly.XamarinSdk.Resource.Id.icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon; + global::LaunchDarkly.XamarinSdk.Resource.Id.icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.icon_group; + global::LaunchDarkly.XamarinSdk.Resource.Id.info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.info; + global::LaunchDarkly.XamarinSdk.Resource.Id.italic = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.italic; + global::LaunchDarkly.XamarinSdk.Resource.Id.line1 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line1; + global::LaunchDarkly.XamarinSdk.Resource.Id.line3 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.line3; + global::LaunchDarkly.XamarinSdk.Resource.Id.normal = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.normal; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_background = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_background; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column; + global::LaunchDarkly.XamarinSdk.Resource.Id.notification_main_column_container = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.notification_main_column_container; + global::LaunchDarkly.XamarinSdk.Resource.Id.right_icon = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_icon; + global::LaunchDarkly.XamarinSdk.Resource.Id.right_side = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.right_side; + global::LaunchDarkly.XamarinSdk.Resource.Id.tag_transition_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.tag_transition_group; + global::LaunchDarkly.XamarinSdk.Resource.Id.text = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text; + global::LaunchDarkly.XamarinSdk.Resource.Id.text2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.text2; + global::LaunchDarkly.XamarinSdk.Resource.Id.time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.time; + global::LaunchDarkly.XamarinSdk.Resource.Id.title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Id.title; + global::LaunchDarkly.XamarinSdk.Resource.Integer.status_bar_notification_info_maxnum = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Integer.status_bar_notification_info_maxnum; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_action_tombstone = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_action_tombstone; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_custom_big = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_custom_big; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_icon_group = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_icon_group; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_chronometer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_chronometer; + global::LaunchDarkly.XamarinSdk.Resource.Layout.notification_template_part_time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Layout.notification_template_part_time; + global::LaunchDarkly.XamarinSdk.Resource.String.status_bar_notification_info_overflow = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.String.status_bar_notification_info_overflow; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Info = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Info; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Line2 = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Line2; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Time = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Time; + global::LaunchDarkly.XamarinSdk.Resource.Style.TextAppearance_Compat_Notification_Title = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.TextAppearance_Compat_Notification_Title; + global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionContainer = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionContainer; + global::LaunchDarkly.XamarinSdk.Resource.Style.Widget_Compat_NotificationActionText = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Style.Widget_Compat_NotificationActionText; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderAuthority = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderAuthority; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderCerts = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderCerts; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchStrategy = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchStrategy; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderFetchTimeout = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderFetchTimeout; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderPackage = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderPackage; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamily_fontProviderQuery = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamily_fontProviderQuery; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_font; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_android_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_android_fontWeight; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_font = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_font; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontStyle = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontStyle; + global::LaunchDarkly.XamarinSdk.Resource.Styleable.FontFamilyFont_fontWeight = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Styleable.FontFamilyFont_fontWeight; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_in = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_in; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_fade_out = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_fade_out; global::Xamarin.Forms.Platform.Android.Resource.Animation.abc_grow_fade_in_from_bottom = global::LaunchDarkly.XamarinSdk.Android.Tests.Resource.Animation.abc_grow_fade_in_from_bottom; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index be54844e..dc7caa71 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -85,8 +85,7 @@ public void InitCanTimeOutSync() var config = BaseConfig(server).IsStreamingEnabled(false).Build(); using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) { - Assert.False(Initialized(client)); - Assert.False(client.Initialized()); + Assert.False(client.Initialized); Assert.Null(client.StringVariation(_flagData1.First().Key, null)); Assert.Contains(log.Messages, m => m.Level == LogLevel.Warn && m.Text == "Client did not successfully initialize within 200 milliseconds."); @@ -105,20 +104,11 @@ public void InitFailsOn401Sync(UpdateMode mode) using (var log = new LogSinkScope()) { - try - { - var config = BaseConfig(server, mode).Build(); - using (var client = TestUtil.CreateClient(config, _user)) { } - } - catch (Exception e) + var config = BaseConfig(server, mode).Build(); + using (var client = TestUtil.CreateClient(config, _user)) { - // Currently the exact class of this exception is undefined: the polling processor throws - // LaunchDarkly.Client.UnsuccessfulResponseException, while the streaming processor throws - // a lower-level exception that is defined by LaunchDarkly.EventSource. - Assert.Contains("401", e.Message); - return; + Assert.False(client.Initialized); } - throw new Exception("Expected exception from LdClient.Init"); } }); } @@ -140,8 +130,7 @@ await WithServerAsync(async server => // will complete successfully with an uninitialized client. using (var client = await TestUtil.CreateClientAsync(config, _user)) { - Assert.False(Initialized(client)); - Assert.False(client.Initialized()); + Assert.False(client.Initialized); } } }); @@ -155,7 +144,7 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); - var config = BaseConfig(server).IsStreamingEnabled(false).Build(); + var config = BaseConfig(server, UpdateMode.Polling).Build(); var name = "Sue"; var anonUser = User.Builder((string)null).Name(name).Anonymous(true).Build(); @@ -196,7 +185,9 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) server.Reset(); SetupResponse(server, _flagData2, mode); - client.Identify(_otherUser); + var success = client.Identify(_otherUser, TimeSpan.FromSeconds(5)); + Assert.True(success); + Assert.True(client.Initialized); Assert.Equal(_otherUser.Key, client.User.Key); // don't compare entire user, because SDK may have added device/os attributes VerifyRequest(server, mode); @@ -224,7 +215,9 @@ await WithServerAsync(async server => server.Reset(); SetupResponse(server, _flagData2, mode); - await client.IdentifyAsync(_otherUser); + var success = await client.IdentifyAsync(_otherUser); + Assert.True(success); + Assert.True(client.Initialized); Assert.Equal(_otherUser.Key, client.User.Key); // don't compare entire user, because SDK may have added device/os attributes VerifyRequest(server, mode); @@ -234,6 +227,32 @@ await WithServerAsync(async server => }); } + [Theory(Skip = SkipIfCannotCreateHttpServer)] + [MemberData(nameof(PollingAndStreaming))] + public void IdentifyCanTimeOutSync(UpdateMode mode) + { + WithServer(server => + { + SetupResponse(server, _flagData1, mode); + + var config = BaseConfig(server, mode).Build(); + using (var client = TestUtil.CreateClient(config, _user)) + { + VerifyRequest(server, mode); + VerifyFlagValues(client, _flagData1); + var user1RequestPath = server.GetLastRequest().Path; + + server.Reset(); + server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); + + var success = client.Identify(_otherUser, TimeSpan.FromMilliseconds(100)); + Assert.False(success); + Assert.False(client.Initialized); + Assert.Null(client.StringVariation(_flagData1.First().Key, null)); + } + }); + } + [Fact(Skip = SkipIfCannotCreateHttpServer)] public void OfflineClientUsesCachedFlagsSync() { @@ -244,14 +263,13 @@ public void OfflineClientUsesCachedFlagsSync() ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = TestUtil.CreateClient(config, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); + Assert.True(client.Initialized); } // At this point the SDK should have written the flags to persistent storage for this user key. @@ -263,7 +281,7 @@ public void OfflineClientUsesCachedFlagsSync() using (var client = TestUtil.CreateClient(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); + Assert.True(client.Initialized); } } finally @@ -283,8 +301,7 @@ await WithServerAsync(async server => ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) @@ -301,7 +318,7 @@ await WithServerAsync(async server => using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized()); + Assert.True(client.Initialized); } } finally @@ -321,8 +338,7 @@ public void OfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = TestUtil.CreateClient(config, _user)) @@ -360,8 +376,7 @@ await WithServerAsync(async server => ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) @@ -389,11 +404,6 @@ await WithServerAsync(async server => }); } - private bool Initialized(LdClient client) - { - return client.Online && client.updateProcessor.Initialized(); - } - [Fact(Skip = SkipIfCannotCreateHttpServer)] public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() { @@ -404,8 +414,7 @@ public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor( ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = TestUtil.CreateClient(config, _user)) @@ -447,8 +456,7 @@ await WithServerAsync(async server => ClearCachedFlags(_user); try { - var config = BaseConfig(server) - .IsStreamingEnabled(false) + var config = BaseConfig(server, UpdateMode.Polling) .PersistFlagValues(true) .Build(); using (var client = await TestUtil.CreateClientAsync(config, _user)) @@ -480,6 +488,30 @@ await WithServerAsync(async server => }); } + [Theory(Skip = SkipIfCannotCreateHttpServer)] + [MemberData(nameof(PollingAndStreaming))] + public async Task OfflineClientGoesOnlineAndGetsFlagsAsync(UpdateMode mode) + { + await WithServerAsync(async server => + { + ClearCachedFlags(_user); + var config = BaseConfig(server, mode) + .Offline(true) + .PersistFlagValues(false) + .Build(); + using (var client = await TestUtil.CreateClientAsync(config, _user)) + { + VerifyNoFlagValues(client, _flagData1); + + SetupResponse(server, _flagData1, mode); + + await client.SetOfflineAsync(false); + + VerifyFlagValues(client, _flagData1); + } + }); + } + private IConfigurationBuilder BaseConfig(FluentMockServer server) { return Configuration.BuilderInternal(_mobileKey) @@ -523,13 +555,22 @@ private void VerifyRequest(FluentMockServer server, UpdateMode mode) private void VerifyFlagValues(ILdClient client, IDictionary flags) { - Assert.True(Initialized((LdClient) client)); + Assert.True(client.Initialized); foreach (var e in flags) { Assert.Equal(e.Value, client.StringVariation(e.Key, null)); } } + private void VerifyNoFlagValues(ILdClient client, IDictionary flags) + { + Assert.True(client.Initialized); + foreach (var e in flags) + { + Assert.Null(client.StringVariation(e.Key, null)); + } + } + private JToken FlagJson(string key, string value) { var o = new JObject(); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs index 039d1821..0679b318 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientEventTests.cs @@ -1,4 +1,5 @@ -using LaunchDarkly.Client; +using System; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; using Xunit; @@ -22,7 +23,7 @@ public void IdentifySendsIdentifyEvent() using (LdClient client = MakeClient(user, "{}")) { User user1 = User.WithKey("userkey1"); - client.Identify(user1); + client.Identify(user1, TimeSpan.FromSeconds(1)); Assert.Collection(eventProcessor.Events, e => CheckIdentifyEvent(e, user), // there's always an initial identify event e => CheckIdentifyEvent(e, user1)); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 877e273e..37e4a759 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -54,7 +54,8 @@ public void IdentifyUpdatesTheUser() using (var client = Client()) { var updatedUser = User.WithKey("some new key"); - client.Identify(updatedUser); + var success = client.Identify(updatedUser, TimeSpan.FromSeconds(1)); + Assert.True(success); Assert.Equal(client.User.Key, updatedUser.Key); // don't compare entire user, because SDK may have added device/os attributes } } @@ -65,7 +66,7 @@ public Task IdentifyAsyncCompletesOnlyWhenNewFlagsAreAvailable() [Fact] public Task IdentifySyncCompletesOnlyWhenNewFlagsAreAvailable() - => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => Task.Run(() => client.Identify(user))); + => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => Task.Run(() => client.Identify(user, TimeSpan.FromSeconds(1)))); private async Task IdentifyCompletesOnlyWhenNewFlagsAreAvailable(Func identifyTask) { @@ -106,7 +107,7 @@ private async Task IdentifyCompletesOnlyWhenNewFlagsAreAvailable(Func @@ -117,13 +118,13 @@ private async Task IdentifyCompletesOnlyWhenNewFlagsAreAvailable(Func(() => client.Identify(null)); + Assert.Throws(() => client.Identify(null, TimeSpan.Zero)); } } @@ -175,31 +176,18 @@ public void CanCreateNewClientAfterDisposingOfSharedInstance() }); } - [Fact] - public void ConnectionManagerShouldKnowIfOnlineOrNot() - { - using (var client = Client()) - { - var connMgr = client.Config._connectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(true); - Assert.False(client.IsOffline()); - connMgr.Connect(false); - Assert.False(client.Online); - } - } - [Fact] public void ConnectionChangeShouldStopUpdateProcessor() { var mockUpdateProc = new MockPollingProcessor(null); + var mockConnectivityStateManager = new MockConnectivityStateManager(true); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .UpdateProcessorFactory(mockUpdateProc.AsFactory()).Build(); + .UpdateProcessorFactory(mockUpdateProc.AsFactory()) + .ConnectivityStateManager(mockConnectivityStateManager) + .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { - var connMgr = client.Config._connectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(false); + mockConnectivityStateManager.Connect(false); Assert.False(mockUpdateProc.IsRunning); } } @@ -241,7 +229,7 @@ public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { - client.Identify(userWithNullKey); + client.Identify(userWithNullKey, TimeSpan.FromSeconds(1)); Assert.Equal(uniqueId, client.User.Key); Assert.True(client.User.Anonymous); } @@ -256,7 +244,7 @@ public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { - client.Identify(userWithEmptyKey); + client.Identify(userWithEmptyKey, TimeSpan.FromSeconds(1)); Assert.Equal(uniqueId, client.User.Key); Assert.True(client.User.Anonymous); } @@ -281,7 +269,7 @@ public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() .DeviceInfo(new MockDeviceInfo(uniqueId)).Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) { - client.Identify(user); + client.Identify(user, TimeSpan.FromSeconds(1)); User newUser = client.User; Assert.NotEqual(user.Key, newUser.Key); Assert.Equal(user.Avatar, newUser.Avatar); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs index 0b5b470b..9f887898 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobileStreamingProcessorTests.cs @@ -31,7 +31,7 @@ public MobileStreamingProcessorTests() eventSourceFactory = new TestEventSourceFactory(mockEventSource); mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); configBuilder = Configuration.BuilderInternal("someKey") - .ConnectionManager(new MockConnectionManager(true)) + .ConnectivityStateManager(new MockConnectivityStateManager(true)) .FlagCacheManager(mockFlagCacheMgr) .IsStreamingEnabled(true); diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index 25e94762..a72bae29 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -6,11 +6,11 @@ namespace LaunchDarkly.Xamarin.Tests { - internal class MockConnectionManager : IConnectionManager + internal class MockConnectivityStateManager : IConnectivityStateManager { - public Action ConnectionChanged; + public Action ConnectionChanged { get; set; } - public MockConnectionManager(bool isOnline) + public MockConnectivityStateManager(bool isOnline) { isConnected = isOnline; } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 4b5e3ce3..75efc5f6 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -140,7 +140,7 @@ internal static ConfigurationBuilder ConfigWithFlagsJson(User user, string appKe return Configuration.BuilderInternal(appKey) .FlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) - .ConnectionManager(new MockConnectionManager(true)) + .ConnectivityStateManager(new MockConnectivityStateManager(true)) .EventProcessor(new MockEventProcessor()) .UpdateProcessorFactory(MockPollingProcessor.Factory(null)) .PersistentStorage(new MockPersistentStorage()) From e2ce9b0e7e78b60a7ba64e4256b0ae414d27ffd0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 26 Aug 2019 18:58:11 -0700 Subject: [PATCH 240/254] remove HttpClientTimeout in configuration, use ConnectionTimeout (#77) --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 14 +++------- .../ConfigurationBuilder.cs | 27 ++++++++----------- .../FeatureFlagRequestor.cs | 4 +-- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index 1d3949a3..d898c71d 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -27,7 +27,6 @@ public sealed class Configuration private readonly int _eventCapacity; private readonly Uri _eventsUri; private readonly HttpMessageHandler _httpMessageHandler; - private readonly TimeSpan _httpClientTimeout; private readonly bool _inlineUsersInEvents; private readonly bool _isStreamingEnabled; private readonly string _mobileKey; @@ -80,7 +79,7 @@ public sealed class Configuration /// /// The connection timeout to the LaunchDarkly server. /// - public TimeSpan ConnectionTimeout { get; internal set; } + public TimeSpan ConnectionTimeout => _connectionTimeout; /// /// Whether to enable feature flag updates when the application is running in the background. @@ -130,11 +129,6 @@ public sealed class Configuration /// public HttpMessageHandler HttpMessageHandler => _httpMessageHandler; - /// - /// The connection timeout. The default value is 10 seconds. - /// - public TimeSpan HttpClientTimeout => _httpClientTimeout; - /// /// Sets whether to include full user details in every analytics event. /// @@ -250,7 +244,6 @@ public sealed class Configuration internal static readonly TimeSpan DefaultEventFlushInterval = TimeSpan.FromSeconds(5); internal static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); internal static readonly TimeSpan DefaultReconnectTime = TimeSpan.FromSeconds(1); - internal static readonly TimeSpan DefaultHttpClientTimeout = TimeSpan.FromSeconds(10); internal static readonly int DefaultUserKeysCapacity = 1000; internal static readonly TimeSpan DefaultUserKeysFlushInterval = TimeSpan.FromMinutes(5); internal static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(60); @@ -329,7 +322,6 @@ internal Configuration(ConfigurationBuilder builder) _eventCapacity = builder._eventCapacity; _eventsUri = builder._eventsUri; _httpMessageHandler = builder._httpMessageHandler; - _httpClientTimeout = builder._httpClientTimeout; _inlineUsersInEvents = builder._inlineUsersInEvents; _isStreamingEnabled = builder._isStreamingEnabled; _mobileKey = builder._mobileKey; @@ -365,7 +357,7 @@ private class EventProcessorAdapter : IEventProcessorConfiguration public int EventCapacity => Config.EventCapacity; public TimeSpan EventFlushInterval => Config.EventFlushInterval; public Uri EventsUri => Config.EventsUri; - public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; + public TimeSpan HttpClientTimeout => Config.ConnectionTimeout; public bool InlineUsersInEvents => Config.InlineUsersInEvents; public IImmutableSet PrivateAttributeNames => Config.PrivateAttributeNames; public TimeSpan ReadTimeout => Config.ReadTimeout; @@ -386,7 +378,7 @@ private class StreamManagerAdapter : IStreamManagerConfiguration internal Configuration Config { get; set; } public string HttpAuthorizationKey => Config.MobileKey; public HttpMessageHandler HttpMessageHandler => Config.HttpMessageHandler; - public TimeSpan HttpClientTimeout => Config.HttpClientTimeout; + public TimeSpan HttpClientTimeout => Config.ConnectionTimeout; public TimeSpan ReadTimeout => Config.ReadTimeout; public TimeSpan ReconnectTime => Config.ReconnectTime; } diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index 72cdd11a..be9cc152 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -61,6 +61,16 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder BaseUri(Uri baseUri); + /// + /// Sets the connection timeout for all HTTP requests. + /// + /// + /// The default value is 10 seconds. + /// + /// the connection timeout + /// the same builder + IConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout); + /// /// Set to true if LaunchDarkly should provide additional information about how flag values were /// calculated. @@ -119,13 +129,6 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder HttpMessageHandler(HttpMessageHandler httpMessageHandler); - /// - /// Sets the connection timeout. The default value is 10 seconds. - /// - /// the connection timeout - /// the same builder - IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout); - /// /// Sets whether to include full user details in every analytics event. /// @@ -257,14 +260,13 @@ internal sealed class ConfigurationBuilder : IConfigurationBuilder internal bool _allAttributesPrivate = false; internal TimeSpan _backgroundPollingInterval; internal Uri _baseUri = Configuration.DefaultUri; - internal TimeSpan _connectionTimeout; + internal TimeSpan _connectionTimeout = Configuration.DefaultConnectionTimeout; internal bool _enableBackgroundUpdating; internal bool _evaluationReasons = false; internal int _eventCapacity = Configuration.DefaultEventCapacity; internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; internal Uri _eventsUri = Configuration.DefaultEventsUri; internal HttpMessageHandler _httpMessageHandler = PlatformSpecific.Http.GetHttpMessageHandler(); // see Http.shared.cs - internal TimeSpan _httpClientTimeout = Configuration.DefaultHttpClientTimeout; internal bool _inlineUsersInEvents = false; internal bool _isStreamingEnabled = true; internal string _mobileKey; @@ -305,7 +307,6 @@ internal ConfigurationBuilder(Configuration copyFrom) _eventFlushInterval = copyFrom.EventFlushInterval; _eventsUri = copyFrom.EventsUri; _httpMessageHandler = copyFrom.HttpMessageHandler; - _httpClientTimeout = copyFrom.HttpClientTimeout; _inlineUsersInEvents = copyFrom.InlineUsersInEvents; _isStreamingEnabled = copyFrom.IsStreamingEnabled; _mobileKey = copyFrom.MobileKey; @@ -395,12 +396,6 @@ public IConfigurationBuilder HttpMessageHandler(HttpMessageHandler httpMessageHa return this; } - public IConfigurationBuilder HttpClientTimeout(TimeSpan httpClientTimeout) - { - _httpClientTimeout = httpClientTimeout; - return this; - } - public IConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents) { _inlineUsersInEvents = inlineUsersInEvents; diff --git a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs index dd840484..db5ae4ae 100644 --- a/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs +++ b/src/LaunchDarkly.XamarinSdk/FeatureFlagRequestor.cs @@ -79,7 +79,7 @@ private Uri MakeRequestUriWithPath(string path) private async Task MakeRequest(HttpRequestMessage request) { - using (var cts = new CancellationTokenSource(_configuration.HttpClientTimeout)) + using (var cts = new CancellationTokenSource(_configuration.ConnectionTimeout)) { if (_etag != null) { @@ -116,7 +116,7 @@ private async Task MakeRequest(HttpRequestMessage request) } //Otherwise this was a request timeout. throw new TimeoutException("Get item with URL: " + request.RequestUri + - " timed out after : " + _configuration.HttpClientTimeout); + " timed out after : " + _configuration.ConnectionTimeout); } } } From be3e3173ceef6077f01490c56a9cd32e58855656 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 27 Aug 2019 11:21:27 -0700 Subject: [PATCH 241/254] misc fixes to background mode logic, add tests for it (#78) --- src/LaunchDarkly.XamarinSdk/Configuration.cs | 2 + .../ConfigurationBuilder.cs | 28 +- .../DefaultBackgroundModeManager.cs | 20 ++ src/LaunchDarkly.XamarinSdk/Factory.cs | 3 +- .../IBackgroundModeManager.cs | 10 + src/LaunchDarkly.XamarinSdk/LdClient.cs | 34 ++- .../MobilePollingProcessor.cs | 36 ++- .../BackgroundDetection.shared.cs | 2 +- .../LDClientEndToEndTests.cs | 270 +++++++----------- .../MobilePollingProcessorTests.cs | 3 +- .../MockComponents.cs | 11 + 11 files changed, 226 insertions(+), 193 deletions(-) create mode 100644 src/LaunchDarkly.XamarinSdk/DefaultBackgroundModeManager.cs create mode 100644 src/LaunchDarkly.XamarinSdk/IBackgroundModeManager.cs diff --git a/src/LaunchDarkly.XamarinSdk/Configuration.cs b/src/LaunchDarkly.XamarinSdk/Configuration.cs index d898c71d..a3cfa2c3 100644 --- a/src/LaunchDarkly.XamarinSdk/Configuration.cs +++ b/src/LaunchDarkly.XamarinSdk/Configuration.cs @@ -42,6 +42,7 @@ public sealed class Configuration private readonly TimeSpan _userKeysFlushInterval; // Settable only for testing + internal readonly IBackgroundModeManager _backgroundModeManager; internal readonly IConnectivityStateManager _connectivityStateManager; internal readonly IDeviceInfo _deviceInfo; internal readonly IEventProcessor _eventProcessor; @@ -337,6 +338,7 @@ internal Configuration(ConfigurationBuilder builder) _userKeysCapacity = builder._userKeysCapacity; _userKeysFlushInterval = builder._userKeysFlushInterval; + _backgroundModeManager = builder._backgroundModeManager; _connectivityStateManager = builder._connectivityStateManager; _deviceInfo = builder._deviceInfo; _eventProcessor = builder._eventProcessor; diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index be9cc152..24d751f6 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -71,6 +71,19 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout); + /// Sets whether to enable feature flag polling when the application is in the background. + /// + /// + /// By default, on Android and iOS the SDK can still receive feature flag updates when an application + /// is in the background, but it will use polling rather than maintaining a streaming connection (and + /// will use rather than ). + /// If you set EnableBackgroundUpdating to false, it will not check for feature flag updates + /// until the application returns to the foreground. + /// + /// true if background updating should be allowed + /// the same builder + IConfigurationBuilder EnableBackgroundUpdating(bool enableBackgroundUpdating); + /// /// Set to true if LaunchDarkly should provide additional information about how flag values were /// calculated. @@ -261,7 +274,7 @@ internal sealed class ConfigurationBuilder : IConfigurationBuilder internal TimeSpan _backgroundPollingInterval; internal Uri _baseUri = Configuration.DefaultUri; internal TimeSpan _connectionTimeout = Configuration.DefaultConnectionTimeout; - internal bool _enableBackgroundUpdating; + internal bool _enableBackgroundUpdating = true; internal bool _evaluationReasons = false; internal int _eventCapacity = Configuration.DefaultEventCapacity; internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; @@ -282,6 +295,7 @@ internal sealed class ConfigurationBuilder : IConfigurationBuilder internal TimeSpan _userKeysFlushInterval = Configuration.DefaultUserKeysFlushInterval; // Internal properties only settable for testing + internal IBackgroundModeManager _backgroundModeManager; internal IConnectivityStateManager _connectivityStateManager; internal IDeviceInfo _deviceInfo; internal IEventProcessor _eventProcessor; @@ -486,6 +500,18 @@ public IConfigurationBuilder UserKeysFlushInterval(TimeSpan userKeysFlushInterva // and then call these methods before you have called any of the public methods (since // only these methods return ConfigurationBuilder rather than IConfigurationBuilder). + internal ConfigurationBuilder BackgroundModeManager(IBackgroundModeManager backgroundModeManager) + { + _backgroundModeManager = backgroundModeManager; + return this; + } + + internal IConfigurationBuilder BackgroundPollingIntervalWithoutMinimum(TimeSpan backgroundPollingInterval) + { + _backgroundPollingInterval = backgroundPollingInterval; + return this; + } + internal ConfigurationBuilder ConnectivityStateManager(IConnectivityStateManager connectivityStateManager) { _connectivityStateManager = connectivityStateManager; diff --git a/src/LaunchDarkly.XamarinSdk/DefaultBackgroundModeManager.cs b/src/LaunchDarkly.XamarinSdk/DefaultBackgroundModeManager.cs new file mode 100644 index 00000000..0f35b823 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/DefaultBackgroundModeManager.cs @@ -0,0 +1,20 @@ +using System; +using LaunchDarkly.Xamarin.PlatformSpecific; + +namespace LaunchDarkly.Xamarin +{ + internal class DefaultBackgroundModeManager : IBackgroundModeManager + { + public event EventHandler BackgroundModeChanged + { + add + { + BackgroundDetection.BackgroundModeChanged += value; + } + remove + { + BackgroundDetection.BackgroundModeChanged -= value; + } + } + } +} diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index 65ce2cba..c77846f4 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -52,7 +52,8 @@ internal static Func CreateUpdateProcessorFactory(Config return new MobilePollingProcessor(featureFlagRequestor, flagCacheManager, user, - inBackground ? configuration.BackgroundPollingInterval : configuration.PollingInterval); + inBackground ? configuration.BackgroundPollingInterval : configuration.PollingInterval, + inBackground ? configuration.BackgroundPollingInterval : TimeSpan.Zero); } }; } diff --git a/src/LaunchDarkly.XamarinSdk/IBackgroundModeManager.cs b/src/LaunchDarkly.XamarinSdk/IBackgroundModeManager.cs new file mode 100644 index 00000000..205d4061 --- /dev/null +++ b/src/LaunchDarkly.XamarinSdk/IBackgroundModeManager.cs @@ -0,0 +1,10 @@ +using System; +using LaunchDarkly.Xamarin.PlatformSpecific; + +namespace LaunchDarkly.Xamarin +{ + internal interface IBackgroundModeManager + { + event EventHandler BackgroundModeChanged; + } +} diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 0a03cb63..a336dbbf 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -28,6 +28,7 @@ public sealed class LdClient : ILdClient // Immutable client state readonly Configuration _config; readonly ConnectionManager _connectionManager; + readonly IBackgroundModeManager _backgroundModeManager; readonly IDeviceInfo deviceInfo; readonly IConnectivityStateManager _connectivityStateManager; readonly IEventProcessor eventProcessor; @@ -36,7 +37,7 @@ public sealed class LdClient : ILdClient readonly IPersistentStorage persister; // Mutable client state (some state is also in the ConnectionManager) - readonly ReaderWriterLockSlim _userLock = new ReaderWriterLockSlim(); + readonly ReaderWriterLockSlim _stateLock = new ReaderWriterLockSlim(); volatile User _user; volatile bool _inBackground; @@ -68,7 +69,7 @@ public sealed class LdClient : ILdClient /// , but can be changed later with /// or . /// - public User User => LockUtils.WithReadLock(_userLock, () => _user); + public User User => LockUtils.WithReadLock(_stateLock, () => _user); /// public bool Offline => _connectionManager.ForceOffline; @@ -152,7 +153,8 @@ public event EventHandler FlagChanged }; _connectionManager.SetNetworkEnabled(_connectivityStateManager.IsConnected); - BackgroundDetection.BackgroundModeChanged += OnBackgroundModeChanged; + _backgroundModeManager = _config._backgroundModeManager ?? new DefaultBackgroundModeManager(); + _backgroundModeManager.BackgroundModeChanged += OnBackgroundModeChanged; } void Start(TimeSpan maxWaitTime) @@ -465,7 +467,7 @@ public async Task IdentifyAsync(User user) User newUser = DecorateUser(user); - LockUtils.WithWriteLock(_userLock, () => + LockUtils.WithWriteLock(_stateLock, () => { _user = newUser; }); @@ -521,7 +523,7 @@ void Dispose(bool disposing) { Log.InfoFormat("Shutting down the LaunchDarkly client"); - BackgroundDetection.BackgroundModeChanged -= OnBackgroundModeChanged; + _backgroundModeManager.BackgroundModeChanged -= OnBackgroundModeChanged; _connectionManager.Dispose(); eventProcessor.Dispose(); @@ -542,10 +544,20 @@ internal void OnBackgroundModeChanged(object sender, BackgroundModeChangedEventA internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeChangedEventArgs args) { - Log.DebugFormat("Background mode is changing to {0}", args.IsInBackground); - if (args.IsInBackground) + var goingIntoBackground = args.IsInBackground; + var wasInBackground = LockUtils.WithWriteLock(_stateLock, () => + { + var oldValue = _inBackground; + _inBackground = goingIntoBackground; + return oldValue; + }); + if (goingIntoBackground == wasInBackground) + { + return; + } + Log.DebugFormat("Background mode is changing to {0}", goingIntoBackground); + if (goingIntoBackground) { - _inBackground = true; if (!Config.EnableBackgroundUpdating) { Log.Debug("Background updating is disabled"); @@ -554,12 +566,8 @@ internal async Task OnBackgroundModeChangedAsync(object sender, BackgroundModeCh } Log.Debug("Background updating is enabled, starting polling processor"); } - else - { - _inBackground = false; - } await _connectionManager.SetUpdateProcessorFactory( - Factory.CreateUpdateProcessorFactory(_config, User, flagCacheManager, _inBackground), + Factory.CreateUpdateProcessorFactory(_config, User, flagCacheManager, goingIntoBackground), false // don't reset initialized state because the user is still the same ); } diff --git a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs index 4c9864f7..0ed96ee3 100644 --- a/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/MobilePollingProcessor.cs @@ -15,8 +15,9 @@ internal sealed class MobilePollingProcessor : IMobileUpdateProcessor private readonly IFeatureFlagRequestor _featureFlagRequestor; private readonly IFlagCacheManager _flagCacheManager; - private readonly User user; - private readonly TimeSpan pollingInterval; + private readonly User _user; + private readonly TimeSpan _pollingInterval; + private readonly TimeSpan _initialDelay; private readonly TaskCompletionSource _startTask; private readonly TaskCompletionSource _stopTask; private const int UNINITIALIZED = 0; @@ -27,23 +28,32 @@ internal sealed class MobilePollingProcessor : IMobileUpdateProcessor internal MobilePollingProcessor(IFeatureFlagRequestor featureFlagRequestor, IFlagCacheManager cacheManager, User user, - TimeSpan pollingInterval) + TimeSpan pollingInterval, + TimeSpan initialDelay) { this._featureFlagRequestor = featureFlagRequestor; this._flagCacheManager = cacheManager; - this.user = user; - this.pollingInterval = pollingInterval; + this._user = user; + this._pollingInterval = pollingInterval; + this._initialDelay = initialDelay; _startTask = new TaskCompletionSource(); _stopTask = new TaskCompletionSource(); } Task IMobileUpdateProcessor.Start() { - if (pollingInterval.Equals(TimeSpan.Zero)) + if (_pollingInterval.Equals(TimeSpan.Zero)) throw new Exception("Timespan for polling can't be zero"); - Log.InfoFormat("Starting LaunchDarkly PollingProcessor with interval: {0}", pollingInterval); - + if (_initialDelay > TimeSpan.Zero) + { + Log.InfoFormat("Starting LaunchDarkly PollingProcessor with interval: {0} (waiting {1} first)", _pollingInterval, _initialDelay); + } + else + { + Log.InfoFormat("Starting LaunchDarkly PollingProcessor with interval: {0}", _pollingInterval); + } + Task.Run(() => UpdateTaskLoopAsync()); return _startTask.Task; } @@ -55,10 +65,14 @@ bool IMobileUpdateProcessor.Initialized() private async Task UpdateTaskLoopAsync() { + if (_initialDelay > TimeSpan.Zero) + { + await Task.Delay(_initialDelay); + } while (!_disposed) { await UpdateTaskAsync(); - await Task.Delay(pollingInterval); + await Task.Delay(_pollingInterval); } } @@ -71,9 +85,9 @@ private async Task UpdateTaskAsync() { var flagsAsJsonString = response.jsonResponse; var flagsDictionary = JsonConvert.DeserializeObject>(flagsAsJsonString); - _flagCacheManager.CacheFlagsFromService(flagsDictionary, user); + _flagCacheManager.CacheFlagsFromService(flagsDictionary, _user); - //We can't use bool in CompareExchange because it is not a reference type. + // We can't use bool in CompareExchange because it is not a reference type. if (Interlocked.CompareExchange(ref _initialized, INITIALIZED, UNINITIALIZED) == UNINITIALIZED) { _startTask.SetResult(true); diff --git a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs index 996ec6bf..146f2e46 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformSpecific/BackgroundDetection.shared.cs @@ -38,7 +38,7 @@ public static event EventHandler BackgroundModeC } } - internal static void UpdateBackgroundMode(bool isInBackground) + private static void UpdateBackgroundMode(bool isInBackground) { var args = new BackgroundModeChangedEventArgs(isInBackground); var handlers = _backgroundModeChanged; diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index dc7caa71..220b2950 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Common.Logging; using LaunchDarkly.Client; using LaunchDarkly.Xamarin.PlatformSpecific; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; using WireMock.Server; using Xunit; @@ -48,7 +52,7 @@ public void InitGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -65,7 +69,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -82,7 +86,7 @@ public void InitCanTimeOutSync() using (var log = new LogSinkScope()) { - var config = BaseConfig(server).IsStreamingEnabled(false).Build(); + var config = BaseConfig(server, builder => builder.IsStreamingEnabled(false)); using (var client = TestUtil.CreateClient(config, _user, TimeSpan.FromMilliseconds(200))) { Assert.False(client.Initialized); @@ -104,7 +108,7 @@ public void InitFailsOn401Sync(UpdateMode mode) using (var log = new LogSinkScope()) { - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = TestUtil.CreateClient(config, _user)) { Assert.False(client.Initialized); @@ -123,7 +127,7 @@ await WithServerAsync(async server => using (var log = new LogSinkScope()) { - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); // Currently the behavior of LdClient.InitAsync is somewhat inconsistent with LdClient.Init if there is // an unrecoverable error: LdClient.Init throws an exception, but LdClient.InitAsync returns a task that @@ -144,7 +148,7 @@ await WithServerAsync(async server => { server.ForAllRequests(r => r.WithDelay(TimeSpan.FromSeconds(2)).WithJsonBody(PollingData(_flagData1))); - var config = BaseConfig(server, UpdateMode.Polling).Build(); + var config = BaseConfig(server, UpdateMode.Polling); var name = "Sue"; var anonUser = User.Builder((string)null).Name(name).Anonymous(true).Build(); @@ -175,7 +179,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -205,7 +209,7 @@ await WithServerAsync(async server => { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyRequest(server, mode); @@ -235,7 +239,7 @@ public void IdentifyCanTimeOutSync(UpdateMode mode) { SetupResponse(server, _flagData1, mode); - var config = BaseConfig(server, mode).Build(); + var config = BaseConfig(server, mode); using (var client = TestUtil.CreateClient(config, _user)) { VerifyRequest(server, mode); @@ -263,13 +267,10 @@ public void OfflineClientUsesCachedFlagsSync() ClearCachedFlags(_user); try { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); + var config = BaseConfig(server, UpdateMode.Polling, builder => builder.PersistFlagValues(true)); using (var client = TestUtil.CreateClient(config, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized); } // At this point the SDK should have written the flags to persistent storage for this user key. @@ -281,7 +282,6 @@ public void OfflineClientUsesCachedFlagsSync() using (var client = TestUtil.CreateClient(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized); } } finally @@ -301,9 +301,7 @@ await WithServerAsync(async server => ClearCachedFlags(_user); try { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); + var config = BaseConfig(server, UpdateMode.Polling, builder => builder.PersistFlagValues(true)); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyFlagValues(client, _flagData1); @@ -318,7 +316,6 @@ await WithServerAsync(async server => using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) { VerifyFlagValues(client, _flagData1); - Assert.True(client.Initialized); } } finally @@ -329,161 +326,87 @@ await WithServerAsync(async server => } [Fact(Skip = SkipIfCannotCreateHttpServer)] - public void OfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() + public async Task BackgroundModeForcesPollingAsync() { - WithServer(server => - { - SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this - - ClearCachedFlags(_user); - try - { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); - using (var client = TestUtil.CreateClient(config, _user)) - { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } - - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + var mockBackgroundModeManager = new MockBackgroundModeManager(); + var backgroundInterval = TimeSpan.FromMilliseconds(50); + var hackyUpdateDelay = TimeSpan.FromMilliseconds(200); - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = TestUtil.CreateClient(offlineConfig, _user)) - { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } - } - finally - { - ClearCachedFlags(_user); - } - }); - } - - [Fact(Skip = SkipIfCannotCreateHttpServer)] - public async Task OfflineClientUsesCachedFlagsAsyncAfterStartUpdateProcessorAsync() - { + ClearCachedFlags(_user); await WithServerAsync(async server => { - SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + var config = BaseConfig(server, UpdateMode.Streaming, builder => builder + .BackgroundModeManager(mockBackgroundModeManager) + .BackgroundPollingIntervalWithoutMinimum(backgroundInterval) + .PersistFlagValues(false)); - ClearCachedFlags(_user); - try - { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); - using (var client = await TestUtil.CreateClientAsync(config, _user)) - { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + SetupResponse(server, _flagData1, UpdateMode.Streaming); - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. - - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) - { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } - } - finally + using (var client = await TestUtil.CreateClientAsync(config, _user)) { - ClearCachedFlags(_user); - } - }); - } + VerifyFlagValues(client, _flagData1); - [Fact(Skip = SkipIfCannotCreateHttpServer)] - public void BackgroundOfflineClientUsesCachedFlagsSyncAfterStartUpdateProcessor() - { - WithServer(server => - { - SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + // SetupResponse makes the server *only* respond to the right endpoint for the update mode that we + // specified, so here we only want it to succeed if it gets a polling request, not streaming. + var requestReceivedSignal = SetupResponse(server, _flagData2, UpdateMode.Polling); + mockBackgroundModeManager.UpdateBackgroundMode(true); - ClearCachedFlags(_user); - try - { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); - using (var client = TestUtil.CreateClient(config, _user)) - { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + // There's no way for us to directly observe when the new update processor has finished both doing the + // request and updating the flag store; we can only detect, via requestReceivedSignal, when the server + // has received the request. So we need to add a hacky delay here. + await requestReceivedSignal.WaitAsync(); + await Task.Delay(hackyUpdateDelay); + VerifyFlagValues(client, _flagData2); - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // Now switch back to streaming + requestReceivedSignal = SetupResponse(server, _flagData1, UpdateMode.Streaming); + mockBackgroundModeManager.UpdateBackgroundMode(false); - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = TestUtil.CreateClient(offlineConfig, _user)) - { - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - } - } - finally - { - ClearCachedFlags(_user); + // Again, we have no way to really guarantee how long this state change will take + await requestReceivedSignal.WaitAsync(); + await Task.Delay(hackyUpdateDelay); + VerifyFlagValues(client, _flagData1); } }); } [Fact(Skip = SkipIfCannotCreateHttpServer)] - public async Task BackgroundOfflineClientUsesCachedFlagsAsyncAfterStartUpdateProcessorAsync() + public async Task BackgroundModePollingCanBeDisabledAsync() { + var mockBackgroundModeManager = new MockBackgroundModeManager(); + var backgroundInterval = TimeSpan.FromMilliseconds(50); + var hackyUpdateDelay = TimeSpan.FromMilliseconds(200); + + ClearCachedFlags(_user); await WithServerAsync(async server => { - SetupResponse(server, _flagData1, UpdateMode.Polling); // streaming vs. polling should make no difference for this + var config = BaseConfig(server, UpdateMode.Streaming, builder => builder + .BackgroundModeManager(mockBackgroundModeManager) + .EnableBackgroundUpdating(false) + .BackgroundPollingInterval(backgroundInterval) + .PersistFlagValues(false)); - ClearCachedFlags(_user); - try + SetupResponse(server, _flagData1, UpdateMode.Streaming); + + using (var client = await TestUtil.CreateClientAsync(config, _user)) { - var config = BaseConfig(server, UpdateMode.Polling) - .PersistFlagValues(true) - .Build(); - using (var client = await TestUtil.CreateClientAsync(config, _user)) - { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } + VerifyFlagValues(client, _flagData1); - // At this point the SDK should have written the flags to persistent storage for this user key. - // We'll now start over, but with a server that doesn't respond immediately. When the client times - // out, we should still see the earlier flag values. + // The SDK should *not* hit this polling endpoint, but we're providing some data there so we can + // detect whether it does. + SetupResponse(server, _flagData2, UpdateMode.Polling); + mockBackgroundModeManager.UpdateBackgroundMode(true); - server.Reset(); // the offline client shouldn't be making any requests, but just in case - var offlineConfig = Configuration.Builder(_mobileKey).Offline(true).Build(); - using (var client = await TestUtil.CreateClientAsync(offlineConfig, _user)) - { - BackgroundDetection.UpdateBackgroundMode(true); - VerifyFlagValues(client, _flagData1); - BackgroundDetection.UpdateBackgroundMode(false); - VerifyFlagValues(client, _flagData1); - } - } - finally - { - ClearCachedFlags(_user); + await Task.Delay(hackyUpdateDelay); + VerifyFlagValues(client, _flagData1); // we should *not* have done a poll + + // Now switch back to streaming + var requestReceivedSignal = SetupResponse(server, _flagData1, UpdateMode.Streaming); + mockBackgroundModeManager.UpdateBackgroundMode(false); + + await requestReceivedSignal.WaitAsync(); + await Task.Delay(hackyUpdateDelay); + VerifyFlagValues(client, _flagData1); } }); } @@ -495,10 +418,7 @@ public async Task OfflineClientGoesOnlineAndGetsFlagsAsync(UpdateMode mode) await WithServerAsync(async server => { ClearCachedFlags(_user); - var config = BaseConfig(server, mode) - .Offline(true) - .PersistFlagValues(false) - .Build(); + var config = BaseConfig(server, mode, builder => builder.Offline(true).PersistFlagValues(false)); using (var client = await TestUtil.CreateClientAsync(config, _user)) { VerifyNoFlagValues(client, _flagData1); @@ -512,29 +432,49 @@ await WithServerAsync(async server => }); } - private IConfigurationBuilder BaseConfig(FluentMockServer server) + private Configuration BaseConfig(FluentMockServer server, Func extraConfig = null) { - return Configuration.BuilderInternal(_mobileKey) - .EventProcessor(new MockEventProcessor()) + var builderInternal = Configuration.BuilderInternal(_mobileKey) + .EventProcessor(new MockEventProcessor()); + builderInternal .BaseUri(new Uri(server.GetUrl())) .StreamUri(new Uri(server.GetUrl())) - .PersistFlagValues(false); // unless we're specifically testing flag caching, this helps to prevent test state contamination + .PersistFlagValues(false); // unless we're specifically testing flag caching, this helps to prevent test state contamination + var builder = extraConfig == null ? builderInternal : extraConfig(builderInternal); + return builder.Build(); } - private IConfigurationBuilder BaseConfig(FluentMockServer server, UpdateMode mode) + private Configuration BaseConfig(FluentMockServer server, UpdateMode mode, Func extraConfig = null) { - return BaseConfig(server) - .IsStreamingEnabled(mode.IsStreaming); + return BaseConfig(server, builder => + { + builder.IsStreamingEnabled(mode.IsStreaming); + return extraConfig == null ? builder : extraConfig(builder); + }); } - private void SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) + // + private SemaphoreSlim SetupResponse(FluentMockServer server, IDictionary data, UpdateMode mode) { - server.ForAllRequests(r => - mode.IsStreaming ? r.WithEventsBody(StreamingData(data)) : r.WithJsonBody(PollingData(data))); + var signal = new SemaphoreSlim(0, 1); + server.ResetMappings(); + var resp = Response.Create().WithCallback(req => + { + signal.Release(); + var respBuilder = mode.IsStreaming ? + Response.Create().WithEventsBody(StreamingData(data)) : + Response.Create().WithJsonBody(PollingData(data)); + return ((Response)respBuilder).ResponseMessage; + }); + // Note: in streaming mode, since WireMock.Net doesn't seem to support streaming responses, the fake response will close // after the end of the data-- so the SDK will enter retry mode and we may get another identical streaming request. For // the purposes of these tests, that doesn't matter. The correct processing of a chunked stream is tested in the // LaunchDarkly.EventSource tests, and the retry logic is tested in LaunchDarkly.CommonSdk. + + server.Given(Request.Create().WithPath(path => Regex.IsMatch(path, mode.FlagsPathRegex))) + .RespondWith(resp); + return signal; } private void VerifyRequest(FluentMockServer server, UpdateMode mode) @@ -611,5 +551,7 @@ public class UpdateMode IsStreaming = false, FlagsPathRegex = "^/msdk/evalx/users/[^/?]+" }; + + public override string ToString() => IsStreaming ? "Streaming" : "Polling"; } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs index 1aeca942..10127382 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MobilePollingProcessorTests.cs @@ -21,8 +21,7 @@ IMobileUpdateProcessor Processor() var stubbedFlagCache = new UserFlagInMemoryCache(); mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); user = User.WithKey("user1Key"); - var timeSpan = TimeSpan.FromSeconds(1); - return new MobilePollingProcessor(mockFeatureFlagRequestor, mockFlagCacheManager, user, timeSpan); + return new MobilePollingProcessor(mockFeatureFlagRequestor, mockFlagCacheManager, user, TimeSpan.FromSeconds(30), TimeSpan.Zero); } [Fact] diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index a72bae29..d8984938 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -2,10 +2,21 @@ using System.Collections.Generic; using System.Threading.Tasks; using LaunchDarkly.Client; +using LaunchDarkly.Xamarin.PlatformSpecific; using Newtonsoft.Json; namespace LaunchDarkly.Xamarin.Tests { + internal class MockBackgroundModeManager : IBackgroundModeManager + { + public event EventHandler BackgroundModeChanged; + + public void UpdateBackgroundMode(bool isInBackground) + { + BackgroundModeChanged?.Invoke(this, new BackgroundModeChangedEventArgs(isInBackground)); + } + } + internal class MockConnectivityStateManager : IConnectivityStateManager { public Action ConnectionChanged { get; set; } From 240a2ef4bbf08b47d410ef719bdf800e644401a1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 27 Aug 2019 20:19:07 -0700 Subject: [PATCH 242/254] fix comments --- src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs index 24d751f6..008e5230 100644 --- a/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.XamarinSdk/ConfigurationBuilder.cs @@ -48,7 +48,8 @@ public interface IConfigurationBuilder /// Sets the interval between feature flag updates when the application is running in the background. /// /// - /// This is only relevant on mobile platforms. + /// This is only relevant on mobile platforms. The default is ; + /// the minimum is . /// /// the background polling interval /// the same builder @@ -71,6 +72,7 @@ public interface IConfigurationBuilder /// the same builder IConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout); + /// /// Sets whether to enable feature flag polling when the application is in the background. /// /// @@ -196,7 +198,8 @@ public interface IConfigurationBuilder /// Sets the polling interval (when streaming is disabled). /// /// - /// Values less than the default of 30 seconds will be changed to the default. + /// The default is ; the minimum is + /// . /// /// the rule update polling interval /// the same builder From d78754c6e201165c4ac7e3ddc2de1dae9f29fd86 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 28 Aug 2019 10:25:00 -0700 Subject: [PATCH 243/254] misc fixes for flaky tests --- .../AndroidSpecificTests.cs | 2 +- tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs | 2 +- tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs index 337a3dc3..1d6fa17f 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/AndroidSpecificTests.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class AndroidSpecificTests + public class AndroidSpecificTests : BaseTest { [Fact] public void SdkReturnsAndroidPlatformType() diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index 37e4a759..d7803018 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -66,7 +66,7 @@ public Task IdentifyAsyncCompletesOnlyWhenNewFlagsAreAvailable() [Fact] public Task IdentifySyncCompletesOnlyWhenNewFlagsAreAvailable() - => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => Task.Run(() => client.Identify(user, TimeSpan.FromSeconds(1)))); + => IdentifyCompletesOnlyWhenNewFlagsAreAvailable((client, user) => Task.Run(() => client.Identify(user, TimeSpan.FromSeconds(4)))); private async Task IdentifyCompletesOnlyWhenNewFlagsAreAvailable(Func identifyTask) { diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs index 63bbb60d..5f2d776f 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/IOsSpecificTests.cs @@ -3,7 +3,7 @@ namespace LaunchDarkly.Xamarin.Tests { - public class IOsSpecificTests + public class IOsSpecificTests : BaseTest { [Fact] public void SdkReturnsIOsPlatformType() From 6866f85b4dd9e0c12f94b43a71a2fe80dce87fc2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 28 Aug 2019 18:17:56 -0700 Subject: [PATCH 244/254] update WireMock, re-enable HTTP tests for Android --- ...unchDarkly.XamarinSdk.Android.Tests.csproj | 2 +- .../Properties/AssemblyInfo.cs | 5 +- .../LaunchDarkly.XamarinSdk.Tests/BaseTest.cs | 35 ++-- .../FeatureFlagRequestorTests.cs | 19 +- .../LDClientEndToEndTests.cs | 28 +-- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LaunchDarkly.XamarinSdk.Tests/TestUtil.cs | 184 ++++++++++-------- .../AssemblyInfo.cs | 4 + .../LaunchDarkly.XamarinSdk.iOS.Tests.csproj | 3 +- 9 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 tests/LaunchDarkly.XamarinSdk.iOS.Tests/AssemblyInfo.cs diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj index da88579f..5d6e2429 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/LaunchDarkly.XamarinSdk.Android.Tests.csproj @@ -147,7 +147,7 @@ 3.4.1 - 1.0.20 + 1.0.22 2.4.1 diff --git a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs index bdf7f78e..20e589a2 100644 --- a/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs +++ b/tests/LaunchDarkly.XamarinSdk.Android.Tests/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Android.App; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -28,3 +28,6 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + +// We must disable all parallel test running by XUnit in order for LogSink to work. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs index 007dab21..b101b5a2 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/BaseTest.cs @@ -1,8 +1,11 @@ using System; +using System.Net.Http; using System.Threading.Tasks; using Common.Logging; using LaunchDarkly.Client; +using WireMock.Logging; using WireMock.Server; +using WireMock.Settings; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -10,16 +13,6 @@ namespace LaunchDarkly.Xamarin.Tests [Collection("serialize all tests")] public class BaseTest : IDisposable { -#if __ANDROID__ - // WireMock.Net currently doesn't work on Android, so we can't run tests that need an embedded HTTP server. - // Mark such tests with [Fact(Skip = SkipIfCannotCreateHttpServer)] or [Theory(Skip = SkipIfCannotCreateHttpServer)] - // and they'll be skipped on Android (but not on other platforms, because "Skip = null" is a no-op). - // https://github.com/WireMock-Net/WireMock.Net/issues/292 - public const string SkipIfCannotCreateHttpServer = "can't run this test because we can't create an embedded HTTP server on this platform; see BaseTest.cs"; -#else - public const string SkipIfCannotCreateHttpServer = null; -#endif - public BaseTest() { LogManager.Adapter = new LogSinkFactoryAdapter(); @@ -64,20 +57,18 @@ protected async Task WithServerAsync(Func a) protected FluentMockServer MakeServer() { -#pragma warning disable RECS0110 // Condition is always 'true' or always 'false' - if (SkipIfCannotCreateHttpServer != null) + // currently we don't need to customize any server settings + var server = FluentMockServer.Start(); + + // Perform an initial request to make sure the server has warmed up. On Android in particular, startup + // of the very first server instance in the test run seems to be very slow, which may cause the first + // request made by unit tests to time out. + using (var client = new HttpClient()) { - // Until WireMock.Net supports all of our platforms, we'll need to mark any tests that use an embedded server - // with [ConditionalFact(Condition = TestCondition.CanRunHttpServer)] or [ConditionalFact(Condition = TestCondition.CanRunHttpServer)] - // instead of [Fact] or [Theory]; otherwise, you'll see this error. - throw new Exception("tried to create an embedded HTTP server on a platform that doesn't support it; see BaseTest.cs"); + AsyncUtils.WaitSafely(() => client.GetAsync(server.Urls[0])); } -#pragma warning restore RECS0110 // Condition is always 'true' or always 'false' - - // currently we don't need to customize any server settings -#pragma warning disable CS0162 // Unreachable code detected - return FluentMockServer.Start(); -#pragma warning restore CS0162 // Unreachable code detected + server.ResetLogEntries(); // so the initial request doesn't interfere with test postconditions + return server; } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs index 76ccb663..462c1c97 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/FeatureFlagRequestorTests.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -11,13 +13,14 @@ public class FeatureFlagRequestorTests : BaseTest private const string _mobileKey = "FAKE_KEY"; private static readonly User _user = User.WithKey("foo"); + private const string _userJson = "{\"key\":\"foo\",\"anonymous\":false}"; private const string _encodedUser = "eyJrZXkiOiJmb28iLCJhbm9ueW1vdXMiOmZhbHNlLCJjdXN0b20iOnt9fQ=="; // Note that in a real use case, the user encoding may vary depending on the target platform, because the SDK adds custom // user attributes like "os". But the lower-level FeatureFlagRequestor component does not do that. private const string _allDataJson = "{}"; // Note that in this implementation, unlike the .NET SDK, FeatureFlagRequestor does not unmarshal the response - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task GetFlagsUsesCorrectUriAndMethodInGetModeAsync() { await WithServerAsync(async server => @@ -43,7 +46,7 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task GetFlagsUsesCorrectUriAndMethodInGetModeWithReasonsAsync() { await WithServerAsync(async server => @@ -69,8 +72,8 @@ await WithServerAsync(async server => }); } - // Report mode is currently disabled - ch47341 - //[Fact(Skip = SkipIfCannotCreateHttpServer)] + // Report mode is currently disabled - ch47341 + //[Fact] //public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync() //{ // await WithServerAsync(async server => @@ -91,15 +94,12 @@ await WithServerAsync(async server => // Assert.Equal($"/msdk/evalx/user", req.Path); // Assert.Equal("", req.RawQuery); // Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); - - // //Assert.Equal("{\"key\":\"foo\"}", req.Body); - // // Here, ideally, we would verify that the request body contained the expected user data. Unfortunately, WireMock.Net - // // is not currently able to detect the body for REPORT requests: https://github.com/WireMock-Net/WireMock.Net/issues/290 + // TestUtil.AssertJsonEquals(JToken.Parse(_userJson), TestUtil.NormalizeJsonUser(JToken.Parse(req.Body))); // } // }); //} - //[Fact(Skip = SkipIfCannotCreateHttpServer)] + //[Fact] //public async Task GetFlagsUsesCorrectUriAndMethodInReportModeWithReasonsAsync() //{ // await WithServerAsync(async server => @@ -120,6 +120,7 @@ await WithServerAsync(async server => // Assert.Equal($"/msdk/evalx/user", req.Path); // Assert.Equal("?withReasons=true", req.RawQuery); // Assert.Equal(_mobileKey, req.Headers["Authorization"][0]); + // TestUtil.AssertJsonEquals(JToken.Parse(_userJson), TestUtil.NormalizeJsonUser(JToken.Parse(req.Body))); // } // }); //} diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index 220b2950..eab4f949 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -44,7 +44,7 @@ public class LdClientEndToEndTests : BaseTest { new object[] { UpdateMode.Streaming } } }; - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public void InitGetsFlagsSync(UpdateMode mode) { @@ -61,7 +61,7 @@ public void InitGetsFlagsSync(UpdateMode mode) }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public async Task InitGetsFlagsAsync(UpdateMode mode) { @@ -77,7 +77,7 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public void InitCanTimeOutSync() { WithServer(server => @@ -98,7 +98,7 @@ public void InitCanTimeOutSync() }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public void InitFailsOn401Sync(UpdateMode mode) { @@ -117,7 +117,7 @@ public void InitFailsOn401Sync(UpdateMode mode) }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public async Task InitFailsOn401Async(UpdateMode mode) { @@ -140,7 +140,7 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task InitWithKeylessAnonUserAddsKeyAndReusesIt() { // Note, we don't care about polling mode vs. streaming mode for this functionality. @@ -171,7 +171,7 @@ await WithServerAsync(async server => }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) { @@ -201,7 +201,7 @@ public void IdentifySwitchesUserAndGetsFlagsSync(UpdateMode mode) }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public async Task IdentifySwitchesUserAndGetsFlagsAsync(UpdateMode mode) { @@ -231,7 +231,7 @@ await WithServerAsync(async server => }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public void IdentifyCanTimeOutSync(UpdateMode mode) { @@ -257,7 +257,7 @@ public void IdentifyCanTimeOutSync(UpdateMode mode) }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public void OfflineClientUsesCachedFlagsSync() { WithServer(server => @@ -291,7 +291,7 @@ public void OfflineClientUsesCachedFlagsSync() }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task OfflineClientUsesCachedFlagsAsync() { await WithServerAsync(async server => @@ -325,7 +325,7 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task BackgroundModeForcesPollingAsync() { var mockBackgroundModeManager = new MockBackgroundModeManager(); @@ -370,7 +370,7 @@ await WithServerAsync(async server => }); } - [Fact(Skip = SkipIfCannotCreateHttpServer)] + [Fact] public async Task BackgroundModePollingCanBeDisabledAsync() { var mockBackgroundModeManager = new MockBackgroundModeManager(); @@ -411,7 +411,7 @@ await WithServerAsync(async server => }); } - [Theory(Skip = SkipIfCannotCreateHttpServer)] + [Theory] [MemberData(nameof(PollingAndStreaming))] public async Task OfflineClientGoesOnlineAndGetsFlagsAsync(UpdateMode mode) { diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 646d691d..e98a23f2 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs index 75efc5f6..e362d879 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/TestUtil.cs @@ -1,21 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using LaunchDarkly.Client; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin.Tests -{ - public static class TestUtil - { - // Any tests that are going to access the static LdClient.Instance must hold this lock, - // to avoid interfering with tests that use CreateClient. - private static readonly SemaphoreSlim ClientInstanceLock = new SemaphoreSlim(1); - - private static ThreadLocal InClientLock = new ThreadLocal(); - +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public static class TestUtil + { + // Any tests that are going to access the static LdClient.Instance must hold this lock, + // to avoid interfering with tests that use CreateClient. + private static readonly SemaphoreSlim ClientInstanceLock = new SemaphoreSlim(1); + + private static ThreadLocal InClientLock = new ThreadLocal(); + public static T WithClientLock(Func f) { // This cumbersome combination of a ThreadLocal and a SemaphoreSlim is simply because 1. we have to use @@ -75,44 +76,44 @@ public static async Task WithClientLockAsync(Func> f) InClientLock.Value = false; ClientInstanceLock.Release(); } - } - - // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can - // instantiate their own independent clients. Application code cannot do this because - // the LdClient.Instance setter has internal scope. - public static LdClient CreateClient(Configuration config, User user, TimeSpan? timeout = null) - { - return WithClientLock(() => + } + + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can + // instantiate their own independent clients. Application code cannot do this because + // the LdClient.Instance setter has internal scope. + public static LdClient CreateClient(Configuration config, User user, TimeSpan? timeout = null) + { + return WithClientLock(() => { - ClearClient(); - LdClient client = LdClient.Init(config, user, timeout ?? TimeSpan.FromSeconds(1)); - client.DetachInstance(); - return client; - }); - } - - // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can - // instantiate their own independent clients. Application code cannot do this because - // the LdClient.Instance setter has internal scope. - public static async Task CreateClientAsync(Configuration config, User user) - { - return await WithClientLockAsync(async () => - { - ClearClient(); - LdClient client = await LdClient.InitAsync(config, user); - client.DetachInstance(); + ClearClient(); + LdClient client = LdClient.Init(config, user, timeout ?? TimeSpan.FromSeconds(1)); + client.DetachInstance(); return client; - }); - } - - public static void ClearClient() - { - WithClientLock(() => - { + }); + } + + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can + // instantiate their own independent clients. Application code cannot do this because + // the LdClient.Instance setter has internal scope. + public static async Task CreateClientAsync(Configuration config, User user) + { + return await WithClientLockAsync(async () => + { + ClearClient(); + LdClient client = await LdClient.InitAsync(config, user); + client.DetachInstance(); + return client; + }); + } + + public static void ClearClient() + { + WithClientLock(() => + { LdClient.Instance?.Dispose(); - }); - } - + }); + } + internal static Dictionary MakeSingleFlagData(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) { var flag = new FeatureFlag { value = value, variation = variation, reason = reason }; @@ -121,30 +122,53 @@ internal static Dictionary MakeSingleFlagData(string flagKe internal static string JsonFlagsWithSingleFlag(string flagKey, JToken value, int? variation = null, EvaluationReason reason = null) { - return JsonConvert.SerializeObject(MakeSingleFlagData(flagKey, value, variation, reason)); - } - - internal static IDictionary DecodeFlagsJson(string flagsJson) - { - return JsonConvert.DeserializeObject>(flagsJson); - } - - internal static ConfigurationBuilder ConfigWithFlagsJson(User user, string appKey, string flagsJson) - { - var flags = DecodeFlagsJson(flagsJson); - IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); - if (user != null && user.Key != null) - { - stubbedFlagCache.CacheFlagsForUser(flags, user); - } - - return Configuration.BuilderInternal(appKey) - .FlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) - .ConnectivityStateManager(new MockConnectivityStateManager(true)) - .EventProcessor(new MockEventProcessor()) - .UpdateProcessorFactory(MockPollingProcessor.Factory(null)) - .PersistentStorage(new MockPersistentStorage()) - .DeviceInfo(new MockDeviceInfo("")); - } - } -} + return JsonConvert.SerializeObject(MakeSingleFlagData(flagKey, value, variation, reason)); + } + + internal static IDictionary DecodeFlagsJson(string flagsJson) + { + return JsonConvert.DeserializeObject>(flagsJson); + } + + internal static ConfigurationBuilder ConfigWithFlagsJson(User user, string appKey, string flagsJson) + { + var flags = DecodeFlagsJson(flagsJson); + IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); + if (user != null && user.Key != null) + { + stubbedFlagCache.CacheFlagsForUser(flags, user); + } + + return Configuration.BuilderInternal(appKey) + .FlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) + .ConnectivityStateManager(new MockConnectivityStateManager(true)) + .EventProcessor(new MockEventProcessor()) + .UpdateProcessorFactory(MockPollingProcessor.Factory(null)) + .PersistentStorage(new MockPersistentStorage()) + .DeviceInfo(new MockDeviceInfo("")); + } + + public static void AssertJsonEquals(JToken expected, JToken actual) + { + if (!JToken.DeepEquals(expected, actual)) + { + Assert.Equal(expected.ToString(), actual.ToString()); // will print the values with the failure + } + } + + public static JToken NormalizeJsonUser(JToken json) + { + // It's undefined whether a user with no custom attributes will have "custom":{} or not + if (json is JObject o && o.ContainsKey("custom") && o["custom"] is JObject co) + { + if (co.Count == 0) + { + JObject o1 = new JObject(o); + o1.Remove("custom"); + return o1; + } + } + return json; + } + } +} diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AssemblyInfo.cs b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..c4e33f39 --- /dev/null +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// We must disable all parallel test running by XUnit in order for LogSink to work. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj index db128f57..3f8703c3 100644 --- a/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.iOS.Tests/LaunchDarkly.XamarinSdk.iOS.Tests.csproj @@ -83,7 +83,7 @@ 4.0.0.497661 - 1.0.20 + 1.0.22 @@ -194,6 +194,7 @@ SharedTestCode\WireMockExtensions.cs + From e48a0b0988149171518b2a0560c7851b00335957 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 28 Aug 2019 23:19:44 -0700 Subject: [PATCH 245/254] more stable way of synchronizing the background mode tests --- .../LDClientEndToEndTests.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs index eab4f949..f7775556 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LDClientEndToEndTests.cs @@ -330,7 +330,6 @@ public async Task BackgroundModeForcesPollingAsync() { var mockBackgroundModeManager = new MockBackgroundModeManager(); var backgroundInterval = TimeSpan.FromMilliseconds(50); - var hackyUpdateDelay = TimeSpan.FromMilliseconds(200); ClearCachedFlags(_user); await WithServerAsync(async server => @@ -346,25 +345,26 @@ await WithServerAsync(async server => { VerifyFlagValues(client, _flagData1); - // SetupResponse makes the server *only* respond to the right endpoint for the update mode that we - // specified, so here we only want it to succeed if it gets a polling request, not streaming. - var requestReceivedSignal = SetupResponse(server, _flagData2, UpdateMode.Polling); + // Set it up so that when the client switches to background mode and does a polling request, it will + // receive _flagData2, and we will be notified of that via a change event. SetupResponse will only + // configure the polling endpoint, so if the client makes a streaming request here it'll fail. + SetupResponse(server, _flagData2, UpdateMode.Polling); + var receivedChangeSignal = new SemaphoreSlim(0, 1); + client.FlagChanged += (sender, args) => + { + receivedChangeSignal.Release(); + }; + mockBackgroundModeManager.UpdateBackgroundMode(true); - // There's no way for us to directly observe when the new update processor has finished both doing the - // request and updating the flag store; we can only detect, via requestReceivedSignal, when the server - // has received the request. So we need to add a hacky delay here. - await requestReceivedSignal.WaitAsync(); - await Task.Delay(hackyUpdateDelay); + await receivedChangeSignal.WaitAsync(); VerifyFlagValues(client, _flagData2); // Now switch back to streaming - requestReceivedSignal = SetupResponse(server, _flagData1, UpdateMode.Streaming); + SetupResponse(server, _flagData1, UpdateMode.Streaming); mockBackgroundModeManager.UpdateBackgroundMode(false); - // Again, we have no way to really guarantee how long this state change will take - await requestReceivedSignal.WaitAsync(); - await Task.Delay(hackyUpdateDelay); + await receivedChangeSignal.WaitAsync(); VerifyFlagValues(client, _flagData1); } }); @@ -400,13 +400,18 @@ await WithServerAsync(async server => await Task.Delay(hackyUpdateDelay); VerifyFlagValues(client, _flagData1); // we should *not* have done a poll + var receivedChangeSignal = new SemaphoreSlim(0, 1); + client.FlagChanged += (sender, args) => + { + receivedChangeSignal.Release(); + }; + // Now switch back to streaming - var requestReceivedSignal = SetupResponse(server, _flagData1, UpdateMode.Streaming); + SetupResponse(server, _flagData2, UpdateMode.Streaming); mockBackgroundModeManager.UpdateBackgroundMode(false); - await requestReceivedSignal.WaitAsync(); - await Task.Delay(hackyUpdateDelay); - VerifyFlagValues(client, _flagData1); + await receivedChangeSignal.WaitAsync(); + VerifyFlagValues(client, _flagData2); } }); } From 20d8ec81339df43761562c88ba6d24e75e73eb00 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Aug 2019 18:55:56 -0700 Subject: [PATCH 246/254] put event processor off/online based on Offline property + network status --- src/LaunchDarkly.XamarinSdk/Factory.cs | 5 -- .../LaunchDarkly.XamarinSdk.csproj | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 12 ++-- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- .../LdClientTests.cs | 70 +++++++++++++++++++ .../MockComponents.cs | 6 ++ 6 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/Factory.cs b/src/LaunchDarkly.XamarinSdk/Factory.cs index c77846f4..c8114f03 100644 --- a/src/LaunchDarkly.XamarinSdk/Factory.cs +++ b/src/LaunchDarkly.XamarinSdk/Factory.cs @@ -64,11 +64,6 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration { return configuration._eventProcessor; } - if (configuration.Offline) - { - return new NullEventProcessor(); - } - HttpClient httpClient = Util.MakeHttpClient(configuration.HttpRequestConfiguration, MobileClientEnvironment.Instance); return new DefaultEventProcessor(configuration.EventProcessorConfiguration, null, httpClient, Constants.EVENTS_PATH); } diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 9e0a07d4..168d54d0 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index a336dbbf..e557d807 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -130,7 +130,6 @@ public event EventHandler FlagChanged _user = DecorateUser(user); flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagChangedEventManager, User); - eventProcessor = Factory.CreateEventProcessor(configuration); _connectionManager = new ConnectionManager(); _connectionManager.SetForceOffline(configuration.Offline); @@ -143,15 +142,19 @@ public event EventHandler FlagChanged true ); - eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); - _connectivityStateManager = Factory.CreateConnectivityStateManager(configuration); _connectivityStateManager.ConnectionChanged += networkAvailable => { Log.DebugFormat("Setting online to {0} due to a connectivity change event", networkAvailable); _ = _connectionManager.SetNetworkEnabled(networkAvailable); // do not await the result + eventProcessor.SetOffline(!networkAvailable || _connectionManager.ForceOffline); }; - _connectionManager.SetNetworkEnabled(_connectivityStateManager.IsConnected); + var isConnected = _connectivityStateManager.IsConnected; + _connectionManager.SetNetworkEnabled(isConnected); + + eventProcessor = Factory.CreateEventProcessor(configuration); + eventProcessor.SetOffline(configuration.Offline || !isConnected); + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); _backgroundModeManager = _config._backgroundModeManager ?? new DefaultBackgroundModeManager(); _backgroundModeManager.BackgroundModeChanged += OnBackgroundModeChanged; @@ -296,6 +299,7 @@ public bool SetOffline(bool value, TimeSpan maxWaitTime) /// public async Task SetOfflineAsync(bool value) { + eventProcessor.SetOffline(value || !_connectionManager.NetworkEnabled); await _connectionManager.SetForceOffline(value); } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index e98a23f2..80a225af 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs index d7803018..f83fcbd6 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LdClientTests.cs @@ -353,5 +353,75 @@ public void FlagsAreSavedToPersistentStorageByDefault() Assert.Equal(new JValue(100), flags["flag"].value); } } + + [Fact] + public void EventProcessorIsOnlineByDefault() + { + var eventProcessor = new MockEventProcessor(); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .EventProcessor(eventProcessor) + .Build(); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.False(eventProcessor.Offline); + } + } + + [Fact] + public void EventProcessorIsOfflineWhenClientIsConfiguredOffline() + { + var connectivityStateManager = new MockConnectivityStateManager(true); + var eventProcessor = new MockEventProcessor(); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .ConnectivityStateManager(connectivityStateManager) + .EventProcessor(eventProcessor) + .Offline(true) + .Build(); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.True(eventProcessor.Offline); + + client.SetOffline(false, TimeSpan.FromSeconds(1)); + Assert.False(eventProcessor.Offline); + + client.SetOffline(true, TimeSpan.FromSeconds(1)); + Assert.True(eventProcessor.Offline); + + // If the network is unavailable... + connectivityStateManager.Connect(false); + + // ...then even if Offline is set to false, events stay off + client.SetOffline(false, TimeSpan.FromSeconds(1)); + Assert.True(eventProcessor.Offline); + } + } + + [Fact] + public void EventProcessorIsOfflineWhenNetworkIsUnavailable() + { + var connectivityStateManager = new MockConnectivityStateManager(false); + var eventProcessor = new MockEventProcessor(); + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .ConnectivityStateManager(connectivityStateManager) + .EventProcessor(eventProcessor) + .Build(); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + Assert.True(eventProcessor.Offline); + + connectivityStateManager.Connect(true); + Assert.False(eventProcessor.Offline); + + connectivityStateManager.Connect(false); + Assert.True(eventProcessor.Offline); + + // If client is configured offline... + client.SetOffline(true, TimeSpan.FromSeconds(1)); + + // ...then even if the network comes back on, events stay off + connectivityStateManager.Connect(true); + Assert.True(eventProcessor.Offline); + } + } } } diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs index d8984938..4e730528 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.XamarinSdk.Tests/MockComponents.cs @@ -65,6 +65,12 @@ public string UniqueDeviceId() internal class MockEventProcessor : IEventProcessor { public List Events = new List(); + public bool Offline = false; + + public void SetOffline(bool offline) + { + Offline = offline; + } public void SendEvent(Event e) { From 23f2c850525ed31397f097b54c0f491e63b0d3f7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Aug 2019 19:02:41 -0700 Subject: [PATCH 247/254] don't post more events if we're offline --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index e557d807..849a37b3 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -154,8 +154,11 @@ public event EventHandler FlagChanged eventProcessor = Factory.CreateEventProcessor(configuration); eventProcessor.SetOffline(configuration.Offline || !isConnected); - eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); + // Send an initial identify event - we will do this even if we're in offline mode, because if you later + // put the client online again, we do want the user to be identified. + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); + _backgroundModeManager = _config._backgroundModeManager ?? new DefaultBackgroundModeManager(); _backgroundModeManager.BackgroundModeChanged += OnBackgroundModeChanged; } @@ -377,14 +380,14 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => if (!Initialized) { Log.Warn("LaunchDarkly client has not yet been initialized. Returning default value"); - eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, + SendEventIfOnline(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, EvaluationErrorKind.CLIENT_NOT_READY)); return errorResult(EvaluationErrorKind.CLIENT_NOT_READY); } else { Log.InfoFormat("Unknown feature flag {0}; returning default value", featureKey); - eventProcessor.SendEvent(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, + SendEventIfOnline(eventFactory.NewUnknownFeatureRequestEvent(featureKey, User, defaultJson, EvaluationErrorKind.FLAG_NOT_FOUND)); return errorResult(EvaluationErrorKind.FLAG_NOT_FOUND); } @@ -421,10 +424,18 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => } var featureEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, User, new EvaluationDetail(valueJson, flag.variation, flag.reason), defaultJson); - eventProcessor.SendEvent(featureEvent); + SendEventIfOnline(featureEvent); return result; } + private void SendEventIfOnline(Event e) + { + if (!_connectionManager.ForceOffline) + { + eventProcessor.SendEvent(e); + } + } + /// public IDictionary AllFlags() { @@ -435,7 +446,7 @@ public IDictionary AllFlags() /// public void Track(string eventName, ImmutableJsonValue data) { - eventProcessor.SendEvent(_eventFactoryDefault.NewCustomEvent(eventName, User, data.AsJToken())); + SendEventIfOnline(_eventFactoryDefault.NewCustomEvent(eventName, User, data.AsJToken())); } /// @@ -447,7 +458,7 @@ public void Track(string eventName) /// public void Flush() { - eventProcessor.Flush(); + eventProcessor.Flush(); // eventProcessor will ignore this if it is offline } /// @@ -476,7 +487,7 @@ public async Task IdentifyAsync(User user) _user = newUser; }); - eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(newUser)); + SendEventIfOnline(_eventFactoryDefault.NewIdentifyEvent(newUser)); return await _connectionManager.SetUpdateProcessorFactory( Factory.CreateUpdateProcessorFactory(_config, user, flagCacheManager, _inBackground), From dc2d86fd18cc423a3edfdd581974e3de9f48beea Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Aug 2019 19:04:57 -0700 Subject: [PATCH 248/254] don't send identify event if we're offline --- src/LaunchDarkly.XamarinSdk/LdClient.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index 849a37b3..c5c1f14f 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -155,9 +155,11 @@ public event EventHandler FlagChanged eventProcessor = Factory.CreateEventProcessor(configuration); eventProcessor.SetOffline(configuration.Offline || !isConnected); - // Send an initial identify event - we will do this even if we're in offline mode, because if you later - // put the client online again, we do want the user to be identified. - eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); + // Send an initial identify event, but only if we weren't explicitly set to be offline + if (!configuration.Offline) + { + eventProcessor.SendEvent(_eventFactoryDefault.NewIdentifyEvent(User)); + } _backgroundModeManager = _config._backgroundModeManager ?? new DefaultBackgroundModeManager(); _backgroundModeManager.BackgroundModeChanged += OnBackgroundModeChanged; From dfdc9aab5c0b607ccd83aa9010467c07f4837344 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 10:42:36 -0700 Subject: [PATCH 249/254] generate XML comment files --- src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 168d54d0..80b7dcf5 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -16,6 +16,13 @@ False + + bin\Debug\$(TargetFramework)\LaunchDarkly.XamarinSdk.xml + + + bin\Release\$(TargetFramework)\LaunchDarkly.XamarinSdk.xml + + From ad45b10c0d10e1db7d6bc637a24ebbde7b944e39 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 11:05:37 -0700 Subject: [PATCH 250/254] try skipping XML step in Android CI job because msbuild will fail --- .circleci/config.yml | 3 +++ src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d4cd36d1..1a23e709 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,9 @@ jobs: docker: - image: ldcircleci/ld-xamarin-android-linux:api27 + environment: + LD_SKIP_XML_DOCS: 1 # there seems to be a bug in Xamarin Android builds on Linux that makes the XML build step fail + steps: - checkout diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 80b7dcf5..4ca2cb18 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -16,10 +16,10 @@ False - + bin\Debug\$(TargetFramework)\LaunchDarkly.XamarinSdk.xml - + bin\Release\$(TargetFramework)\LaunchDarkly.XamarinSdk.xml From dffedd40fe993bd4f33b9ec2503075ae11439ec5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 12:25:09 -0700 Subject: [PATCH 251/254] rm unused type --- src/LaunchDarkly.XamarinSdk/ClientRunMode.cs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/LaunchDarkly.XamarinSdk/ClientRunMode.cs diff --git a/src/LaunchDarkly.XamarinSdk/ClientRunMode.cs b/src/LaunchDarkly.XamarinSdk/ClientRunMode.cs deleted file mode 100644 index 320af5ee..00000000 --- a/src/LaunchDarkly.XamarinSdk/ClientRunMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LaunchDarkly.Xamarin -{ - public enum ClientRunMode - { - Foreground, - Background - } -} From 44dbb6a356985417b06540f908781e0c05f2daf6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 12:25:35 -0700 Subject: [PATCH 252/254] XML comment fixes --- src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs | 2 +- src/LaunchDarkly.XamarinSdk/ILdClient.cs | 3 +++ src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs | 2 +- src/LaunchDarkly.XamarinSdk/LdClient.cs | 10 ++++++++++ src/LaunchDarkly.XamarinSdk/PlatformType.cs | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs index 1dec8f2d..d7d87522 100644 --- a/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs +++ b/src/LaunchDarkly.XamarinSdk/FlagChangedEvent.cs @@ -7,7 +7,7 @@ namespace LaunchDarkly.Xamarin { /// - /// An event object that is sent to handlers for the event. + /// An event object that is sent to handlers for the event. /// public sealed class FlagChangedEventArgs { diff --git a/src/LaunchDarkly.XamarinSdk/ILdClient.cs b/src/LaunchDarkly.XamarinSdk/ILdClient.cs index fb4dc3d2..41077a5e 100644 --- a/src/LaunchDarkly.XamarinSdk/ILdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/ILdClient.cs @@ -6,6 +6,9 @@ namespace LaunchDarkly.Xamarin { + /// + /// Interface for the standard SDK client methods and properties. The only implementation of this is . + /// public interface ILdClient : IDisposable { /// diff --git a/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs index cbe0c73b..ce5f4daa 100644 --- a/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs +++ b/src/LaunchDarkly.XamarinSdk/IMobileUpdateProcessor.cs @@ -11,7 +11,7 @@ namespace LaunchDarkly.Xamarin internal interface IMobileUpdateProcessor : IDisposable { /// - /// Initializes the processor. This is called once from the constructor. + /// Initializes the processor. This is called once from the constructor. /// /// a Task which is completed once the processor has finished starting up Task Start(); diff --git a/src/LaunchDarkly.XamarinSdk/LdClient.cs b/src/LaunchDarkly.XamarinSdk/LdClient.cs index c5c1f14f..789bbad1 100644 --- a/src/LaunchDarkly.XamarinSdk/LdClient.cs +++ b/src/LaunchDarkly.XamarinSdk/LdClient.cs @@ -528,6 +528,16 @@ User DecorateUser(User user) return buildUser is null ? user : buildUser.Build(); } + /// + /// Permanently shuts down the SDK client. + /// + /// + /// This method closes all network collections, shuts down all background tasks, and releases any other + /// resources being held by the SDK. + /// + /// If there are any pending analytics events, and if the SDK is online, it attempts to deliver the events + /// to LaunchDarkly before closing. + /// public void Dispose() { Dispose(true); diff --git a/src/LaunchDarkly.XamarinSdk/PlatformType.cs b/src/LaunchDarkly.XamarinSdk/PlatformType.cs index 3c5ab789..9c34ff70 100644 --- a/src/LaunchDarkly.XamarinSdk/PlatformType.cs +++ b/src/LaunchDarkly.XamarinSdk/PlatformType.cs @@ -2,7 +2,7 @@ namespace LaunchDarkly.Xamarin { /// - /// Values returned by . + /// Values returned by . /// public enum PlatformType { From e4347b78fbc98ad26673fe8fe6c18597c5f25127 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 12:29:49 -0700 Subject: [PATCH 253/254] version 1.0.0-beta23 --- CHANGELOG.md | 21 +++++++++++++++++++ .../LaunchDarkly.XamarinSdk.csproj | 4 ++-- .../LaunchDarkly.XamarinSdk.Tests.csproj | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d89cf6e..d2c7fd77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to the LaunchDarkly Client-side SDK for Xamarin will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [1.0.0-beta23] - 2019-08-30 +### Added: +- XML documentation comments are now included in the package, so they should be visible in Visual Studio for all LaunchDarkly types and methods. + +### Changed: +- The `Online` property of `LdClient` was not useful because it could reflect either a deliberate change to whether the client is allowed to go online (that is, it would be false if you had set `Offline` to true in the configuration), _or_ a change in network availability. It has been removed and replaced with `Offline`, which is only used for explicitly forcing the client to be offline. This is a read-only property; to set it, use `SetOffline` or `SetOfflineAsync`. +- The synchronous `Identify` method now requires a timeout parameter, and returns false if it times out. +- In `Configuration` and `IConfigurationBuilder`, `HttpClientTimeout` is now `ConnectionTimeout`. +- There is now more debug-level logging for stream connection state changes. + +### Fixed: +- Network availability changes are now detected in both Android and iOS. The SDK should not attempt to connect to LaunchDarkly if the OS has told it that the network is unavailable. +- Background polling was never enabled, even if `Configuration.EnableBackgroundUpdating` was true. +- When changing from offline to online by setting `client.Online = true`, or calling `await client.SetOnlineAsync(true)` (the equivalent now would be `client.Offline = false`, etc.), the SDK was returning too soon before it had acquired flags from LaunchDarkly. The known issues in 1.0.0-beta22 have been fixed. +- If the SDK was online and then was explicitly set to be offline, or if network connectivity was lost, the SDK was still attempting to send analytics events. It will no longer do so. +- If the SDK was originally set to be offline and then was put online, the SDK was _not_ sending analytics events. Now it will. + +### Removed: +- `ConfigurationBuilder.UseReport`. Due to [an issue](https://github.com/xamarin/xamarin-android/issues/3544) with the Android implementation of HTTP, the HTTP REPORT method is not currently usable in the Xamarin SDK. +- `IConnectionManager` interface. The SDK now always uses a platform-appropriate implementation of this logic. + ## [1.0.0-beta22] - 2019-08-12 ### Changed: - By default, on Android and iOS the SDK now uses Xamarin's platform-specific implementations of `HttpMessageHandler` that are based on native APIs, rather than the basic `System.Net.Http.HttpClientHandler`. This improves performance and stability on mobile platforms. diff --git a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj index 4ca2cb18..656350c0 100644 --- a/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj +++ b/src/LaunchDarkly.XamarinSdk/LaunchDarkly.XamarinSdk.csproj @@ -4,7 +4,7 @@ netstandard1.6;netstandard2.0;net45;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; netstandard1.6;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81; - 1.0.0-beta22 + 1.0.0-beta23 Library LaunchDarkly.XamarinSdk LaunchDarkly.XamarinSdk @@ -31,7 +31,7 @@ - + diff --git a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj index 80a225af..0676b1a1 100644 --- a/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj +++ b/tests/LaunchDarkly.XamarinSdk.Tests/LaunchDarkly.XamarinSdk.Tests.csproj @@ -10,7 +10,7 @@ - + From eb4830b7ce47271ea1c83b5b66f1cde20f754de5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 30 Aug 2019 12:40:07 -0700 Subject: [PATCH 254/254] changelog additions --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c7fd77..7e54ed8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org). ### Changed: - The `Online` property of `LdClient` was not useful because it could reflect either a deliberate change to whether the client is allowed to go online (that is, it would be false if you had set `Offline` to true in the configuration), _or_ a change in network availability. It has been removed and replaced with `Offline`, which is only used for explicitly forcing the client to be offline. This is a read-only property; to set it, use `SetOffline` or `SetOfflineAsync`. - The synchronous `Identify` method now requires a timeout parameter, and returns false if it times out. +- `LdClient.Initialized` is now a property, not a method. +- `LdClient.Version` is now static, since it describes the entire package rather than a client instance. - In `Configuration` and `IConfigurationBuilder`, `HttpClientTimeout` is now `ConnectionTimeout`. - There is now more debug-level logging for stream connection state changes.