diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index e741c1570..d8dc9b817 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -6,22 +6,4 @@ labels: feature-request assignees: '' --- - -## Summary -Please provide a brief summary of your proposal. Two to three sentences is best here. - -## API Changes - -Include a list of all API changes, additions, subtractions as would be required by your proposal. These APIs should be considered placeholders, so the naming is not as important as getting the concepts correct. If possible you should include some "example" code of usage of your new API. You should also provide details of the level of availability for the feature on each of the supported platforms. - -e.g. - -In order to facilitate the new Shiny Button api, a bool is added to the Button class. This is done as a bool because it is simpler to data bind and other reasons... - - var button = new Button (); - button.MakeShiny = true; // new API - -The MakeShiny API works even if the button is already visible. - -## Intended Use Case -Provide a detailed example of where your proposal would be used and for what purpose. \ No newline at end of file +We are no longer accepting new feature request for Xamarin.Essentials. Please make your new feature requests in the .NET MAUI repo which contains .NET MAUI Essentials. diff --git a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj index bb5e8ff69..ec88c04ec 100644 --- a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj +++ b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj @@ -9,7 +9,6 @@ DeviceTests.Droid XamarinEssentialsDeviceTestsAndroid v10.0 - false True true Resource @@ -19,6 +18,7 @@ true true Xamarin.Android.Net.AndroidClientHandler + armeabi-v7a;x86;x86_64;arm64-v8a true @@ -29,7 +29,6 @@ prompt 4 None - armeabi-v7a;x86;x86_64;arm64-v8a 1G false false @@ -47,7 +46,6 @@ 4 true false - armeabi-v7a;x86;x86_64;arm64-v8a true 1G @@ -63,7 +61,7 @@ - + diff --git a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml index 4ab740cad..7d1546089 100644 --- a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml +++ b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml @@ -12,5 +12,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj b/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj index 8085b44b7..cedc68a32 100644 --- a/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj +++ b/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj @@ -80,7 +80,7 @@ - + diff --git a/DeviceTests/DeviceTests.iOS/Main.cs b/DeviceTests/DeviceTests.iOS/Main.cs index c79e23b40..82ad09057 100644 --- a/DeviceTests/DeviceTests.iOS/Main.cs +++ b/DeviceTests/DeviceTests.iOS/Main.cs @@ -1,4 +1,5 @@ -using UIKit; +using System; +using UIKit; namespace DeviceTests.iOS { @@ -6,10 +7,10 @@ public class Application { static void Main(string[] args) { - if (args?.Length > 0) // usually means this is from xharness - UIApplication.Main(args, null, nameof(TestApplicationDelegate)); + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ci-run"))) + UIApplication.Main(args, null, typeof(TestApplicationDelegate)); else - UIApplication.Main(args, null, nameof(AppDelegate)); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/DeviceTests/build.cake b/DeviceTests/build.cake index 5f851f1f9..69aa1586b 100644 --- a/DeviceTests/build.cake +++ b/DeviceTests/build.cake @@ -118,11 +118,12 @@ Task("test-ios-emu") CleanDirectories(IOS_TEST_RESULTS_PATH.FullPath); // Run the tests - var resultCode = StartProcess("xharness", "ios test " + + var resultCode = StartProcess("xharness", "apple test " + $"--app=\"{IOS_IPA_PATH}\" " + $"--targets=\"ios-simulator-64\" " + $"--output-directory=\"{IOS_TEST_RESULTS_PATH}\" " + - $"--verbosity=\"Debug\" "); + $"--verbosity=\"Debug\" " + + $"--set-env=ci-run=true "); // Rename test result files var resultFiles = GetFiles($"{IOS_TEST_RESULTS_PATH}/*.xml"); diff --git a/README.md b/README.md index c5a3e24f6..c0dc98d5a 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,5 @@ Here are some frequently asked questions about Xamarin.Essentials, but be sure t Please see the [License](LICENSE). +## Stats + diff --git a/Samples/Sample.Server.WebAuthenticator/Sample.Server.WebAuthenticator.csproj b/Samples/Sample.Server.WebAuthenticator/Sample.Server.WebAuthenticator.csproj index 511006200..1615b2910 100644 --- a/Samples/Sample.Server.WebAuthenticator/Sample.Server.WebAuthenticator.csproj +++ b/Samples/Sample.Server.WebAuthenticator/Sample.Server.WebAuthenticator.csproj @@ -1,16 +1,16 @@ - net5.0 + netcoreapp3.1 - - - - - - + + + + + + diff --git a/Samples/Samples.iOS/Main.cs b/Samples/Samples.iOS/Main.cs index 0bb5eadad..5ece82925 100644 --- a/Samples/Samples.iOS/Main.cs +++ b/Samples/Samples.iOS/Main.cs @@ -6,7 +6,7 @@ public class Application { static void Main(string[] args) { - UIApplication.Main(args, null, nameof(AppDelegate)); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/Xamarin.Essentials/Contacts/Contacts.ios.macos.cs b/Xamarin.Essentials/Contacts/Contacts.ios.macos.cs index d1b54132e..17022bb04 100644 --- a/Xamarin.Essentials/Contacts/Contacts.ios.macos.cs +++ b/Xamarin.Essentials/Contacts/Contacts.ios.macos.cs @@ -39,6 +39,14 @@ static Task PlatformPickContactAsync() }) }; + if (picker.PresentationController != null) + { + picker.PresentationController.Delegate = new Platform.UIPresentationControllerDelegate + { + DismissHandler = () => source?.TrySetResult(null) + }; + } + uiView.PresentViewController(picker, true, null); return source.Task; diff --git a/Xamarin.Essentials/Email/Email.ios.cs b/Xamarin.Essentials/Email/Email.ios.cs index 0415350cb..5b0cf6613 100644 --- a/Xamarin.Essentials/Email/Email.ios.cs +++ b/Xamarin.Essentials/Email/Email.ios.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; using Foundation; using MessageUI; @@ -50,13 +51,21 @@ static Task ComposeWithMailCompose(EmailMessage message) } } - // show the controller var tcs = new TaskCompletionSource(); controller.Finished += (sender, e) => { controller.DismissViewController(true, null); tcs.TrySetResult(e.Result == MFMailComposeResult.Sent); }; + + if (controller.PresentationController != null) + { + controller.PresentationController.Delegate = new Platform.UIPresentationControllerDelegate + { + DismissHandler = () => tcs.TrySetResult(false) + }; + } + parentController.PresentViewController(controller, true, null); return tcs.Task; diff --git a/Xamarin.Essentials/FilePicker/FilePicker.ios.cs b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs index 284829ddc..76516c429 100644 --- a/Xamarin.Essentials/FilePicker/FilePicker.ios.cs +++ b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs @@ -34,9 +34,9 @@ static async Task> PlatformPickAsync(PickOptions options if (documentPicker.PresentationController != null) { - documentPicker.PresentationController.Delegate = new PickerPresentationControllerDelegate + documentPicker.PresentationController.Delegate = new Platform.UIPresentationControllerDelegate { - PickHandler = urls => GetFileResults(urls, tcs) + DismissHandler = () => GetFileResults(null, tcs) }; } @@ -74,14 +74,6 @@ public override void DidPickDocument(UIDocumentPickerViewController controller, public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url) => PickHandler?.Invoke(new NSUrl[] { url }); } - - class PickerPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate - { - public Action PickHandler { get; set; } - - public override void DidDismiss(UIPresentationController presentationController) => - PickHandler?.Invoke(null); - } } public partial class FilePickerFileType diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs index 6c38e5682..44a06155a 100644 --- a/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs +++ b/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs @@ -75,9 +75,9 @@ static async Task PhotoAsync(MediaPickerOptions options, bool photo, if (picker.PresentationController != null) { - picker.PresentationController.Delegate = new PhotoPickerPresentationControllerDelegate + picker.PresentationController.Delegate = new Platform.UIPresentationControllerDelegate { - CompletedHandler = info => GetFileResult(info, tcs) + DismissHandler = () => GetFileResult(null, tcs) }; } @@ -167,13 +167,5 @@ public override void FinishedPickingMedia(UIImagePickerController picker, NSDict public override void Canceled(UIImagePickerController picker) => CompletedHandler?.Invoke(null); } - - class PhotoPickerPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate - { - public Action CompletedHandler { get; set; } - - public override void DidDismiss(UIPresentationController presentationController) => - CompletedHandler?.Invoke(null); - } } } diff --git a/Xamarin.Essentials/Permissions/Permissions.android.cs b/Xamarin.Essentials/Permissions/Permissions.android.cs index f8afa8f69..e80105ae7 100644 --- a/Xamarin.Essentials/Permissions/Permissions.android.cs +++ b/Xamarin.Essentials/Permissions/Permissions.android.cs @@ -29,10 +29,23 @@ public static bool IsDeclaredInManifest(string permission) internal static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) => BasePlatformPermission.OnRequestPermissionsResult(requestCode, permissions, grantResults); + public partial class PermissionResult + { + public PermissionResult(string[] permissions, Permission[] grantResults) + { + Permissions = permissions; + GrantResults = grantResults; + } + + public string[] Permissions { get; } + + public Permission[] GrantResults { get; } + } + public abstract partial class BasePlatformPermission : BasePermission { - static readonly Dictionary tcs)> requests = - new Dictionary)>(); + static readonly Dictionary> requests = + new Dictionary>(); static readonly object locker = new object(); static int requestCode; @@ -79,9 +92,6 @@ public override async Task RequestAsync() if (await CheckStatusAsync() == PermissionStatus.Granted) return PermissionStatus.Granted; - TaskCompletionSource tcs; - var doRequest = true; - var runtimePermissions = RequiredPermissions.Where(p => p.isRuntime) ?.Select(p => p.androidPermission)?.ToArray(); @@ -90,38 +100,32 @@ public override async Task RequestAsync() if (runtimePermissions == null || !runtimePermissions.Any()) return PermissionStatus.Granted; - var permissionId = string.Join(';', runtimePermissions); + var permissionResult = await DoRequest(runtimePermissions); + if (permissionResult.GrantResults.Any(g => g == Permission.Denied)) + return PermissionStatus.Denied; + + return PermissionStatus.Granted; + } + + protected virtual async Task DoRequest(string[] permissions) + { + TaskCompletionSource tcs; lock (locker) { - if (requests.ContainsKey(permissionId)) - { - tcs = requests[permissionId].tcs; - doRequest = false; - } - else - { - tcs = new TaskCompletionSource(); + tcs = new TaskCompletionSource(); - requestCode = Platform.NextRequestCode(); + requestCode = Platform.NextRequestCode(); - requests.Add(permissionId, (requestCode, tcs)); - } + requests.Add(requestCode, tcs); } - if (!doRequest) - return await tcs.Task; - if (!MainThread.IsMainThread) throw new PermissionException("Permission request must be invoked on main thread."); - ActivityCompat.RequestPermissions(Platform.GetCurrentActivity(true), runtimePermissions.ToArray(), requestCode); + ActivityCompat.RequestPermissions(Platform.GetCurrentActivity(true), permissions.ToArray(), requestCode); var result = await tcs.Task; - - if (requests.ContainsKey(permissionId)) - requests.Remove(permissionId); - return result; } @@ -157,22 +161,11 @@ internal static void OnRequestPermissionsResult(int requestCode, string[] permis { lock (locker) { - // Check our pending requests for one with a matching request code - foreach (var kvp in requests) + if (requests.ContainsKey(requestCode)) { - 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; - } + var result = new PermissionResult(permissions, grantResults); + requests[requestCode].TrySetResult(result); + requests.Remove(requestCode); } } } @@ -239,6 +232,24 @@ public override (string androidPermission, bool isRuntime)[] RequiredPermissions (Manifest.Permission.AccessCoarseLocation, true), (Manifest.Permission.AccessFineLocation, true) }; + + public override async Task RequestAsync() + { + // Check status before requesting first + if (await CheckStatusAsync() == PermissionStatus.Granted) + return PermissionStatus.Granted; + + var permissionResult = await DoRequest(new string[] { Manifest.Permission.AccessCoarseLocation, Manifest.Permission.AccessFineLocation }); + + // when requesting fine location, user can decline and set coarse instead + var count = permissionResult.GrantResults.Count(x => x == Permission.Granted); + return count switch + { + 2 => PermissionStatus.Granted, + 1 => PermissionStatus.Restricted, + _ => PermissionStatus.Denied + }; + } } public partial class LocationAlways : BasePlatformPermission @@ -260,6 +271,30 @@ public override (string androidPermission, bool isRuntime)[] RequiredPermissions return permissions.ToArray(); } } + +#if __ANDROID_29__ + public override async Task RequestAsync() + { + // Check status before requesting first + if (await CheckStatusAsync() == PermissionStatus.Granted) + return PermissionStatus.Granted; + + if (Platform.HasApiLevel(30)) + { + var permissionResult = await new LocationWhenInUse().RequestAsync(); + if (permissionResult == PermissionStatus.Denied) + return PermissionStatus.Denied; + + var result = await DoRequest(new string[] { Manifest.Permission.AccessBackgroundLocation }); + if (!result.GrantResults.All(x => x == Permission.Granted)) + permissionResult = PermissionStatus.Restricted; + + return permissionResult; + } + + return await base.RequestAsync(); + } +#endif } public partial class Maps : BasePlatformPermission diff --git a/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs b/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs index 635e4d3f7..3461d6e6d 100644 --- a/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs +++ b/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs @@ -73,12 +73,20 @@ internal static bool HasOSVersion(int major, int minor) => #if __IOS__ || __TVOS__ + static Func getCurrentController; + + public static void Init(Func getCurrentUIViewController) + => getCurrentController = getCurrentUIViewController; + public static UIViewController GetCurrentUIViewController() => GetCurrentViewController(false); internal static UIViewController GetCurrentViewController(bool throwIfNull = true) { - UIViewController viewController = null; + var viewController = getCurrentController?.Invoke(); + + if (viewController != null) + return viewController; var window = UIApplication.SharedApplication.KeyWindow; @@ -138,5 +146,24 @@ internal static UIWindow GetCurrentWindow(bool throwIfNull = true) internal static NSOperationQueue GetCurrentQueue() => NSOperationQueue.CurrentQueue ?? new NSOperationQueue(); + +#if __IOS__ + internal class UIPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate + { + public Action DismissHandler { get; set; } + + public override void DidDismiss(UIPresentationController presentationController) + { + DismissHandler?.Invoke(); + DismissHandler = null; + } + + protected override void Dispose(bool disposing) + { + DismissHandler?.Invoke(); + base.Dispose(disposing); + } + } +#endif } } diff --git a/Xamarin.Essentials/Sms/Sms.ios.cs b/Xamarin.Essentials/Sms/Sms.ios.cs index 19da4bbb1..acd775f99 100644 --- a/Xamarin.Essentials/Sms/Sms.ios.cs +++ b/Xamarin.Essentials/Sms/Sms.ios.cs @@ -21,13 +21,21 @@ static Task PlatformComposeAsync(SmsMessage message) messageController.Recipients = message?.Recipients?.ToArray() ?? new string[] { }; - // show the controller var tcs = new TaskCompletionSource(); messageController.Finished += (sender, e) => { messageController.DismissViewController(true, null); tcs?.TrySetResult(e.Result == MessageComposeResult.Sent); }; + + if (controller.PresentationController != null) + { + controller.PresentationController.Delegate = new Platform.UIPresentationControllerDelegate + { + DismissHandler = () => tcs.TrySetResult(false) + }; + } + controller.PresentViewController(messageController, true, null); return tcs.Task; diff --git a/Xamarin.Essentials/Types/LocationExtensions.ios.tvos.watchos.macos.cs b/Xamarin.Essentials/Types/LocationExtensions.ios.tvos.watchos.macos.cs index 01d52c2db..cfdf74fa3 100644 --- a/Xamarin.Essentials/Types/LocationExtensions.ios.tvos.watchos.macos.cs +++ b/Xamarin.Essentials/Types/LocationExtensions.ios.tvos.watchos.macos.cs @@ -33,7 +33,7 @@ internal static Location ToLocation(this CLLocation location) => Accuracy = location.HorizontalAccuracy, VerticalAccuracy = location.VerticalAccuracy, Timestamp = location.Timestamp.ToDateTime(), -#if __iOS__ || __WATCHOS__ +#if __IOS__ || __WATCHOS__ Course = location.Course < 0 ? default(double?) : location.Course, Speed = location.Speed < 0 ? default(double?) : location.Speed, #endif diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ee4b63af1..d8b126a75 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,7 +8,7 @@ pr: - develop variables: - BASE_VERSION: 1.6.2 + BASE_VERSION: 1.7.1 PREVIEW_LABEL: 'ci' BUILD_NUMBER: $[counter(format('{0}_{1}_{2}', variables['BASE_VERSION'], variables['PREVIEW_LABEL'], variables['Build.SourceBranch']), 1)] NUGET_VERSION: $[format('{0}-{1}.{2}', variables['BASE_VERSION'], variables['PREVIEW_LABEL'], variables['BUILD_NUMBER'])] @@ -99,7 +99,7 @@ stages: verbosity: diagnostic cakeFile: DeviceTests/build.cake cakeTarget: test-ios-emu - cakeExtraArgs: --ios-device="`"iPhone 11`"" --ios-runtime="`"com.apple.CoreSimulator.SimRuntime.iOS-14-2`"" + xharness: '1.0.0-prerelease.21620.1' - template: .ci/build.yml@components parameters: @@ -198,18 +198,18 @@ stages: - bash: sh -c "echo \"y\" | $ANDROID_HOME/tools/bin/sdkmanager \"system-images;android-29;google_apis;x86\"" displayName: Install the Android emulators - - template: .ci/build.yml@components - parameters: - name: devicetests_android_api_30 - runChecks: false - displayName: Android API 30 - publishOutputSuffix: '-android30' - windowsImage: '' - areaPath: $(AREA_PATH) - verbosity: diagnostic - cakeFile: DeviceTests/build.cake - cakeTarget: test-android-emu - cakeExtraArgs: --avd-target="`"system-images;android-30;google_apis;x86`"" - preBuildSteps: - - bash: sh -c "echo \"y\" | $ANDROID_HOME/tools/bin/sdkmanager \"system-images;android-30;google_apis;x86\"" - displayName: Install the Android emulators + # - template: .ci/build.yml@components + # parameters: + # name: devicetests_android_api_30 + # runChecks: false + # displayName: Android API 30 + # publishOutputSuffix: '-android30' + # windowsImage: '' + # areaPath: $(AREA_PATH) + # verbosity: diagnostic + # cakeFile: DeviceTests/build.cake + # cakeTarget: test-android-emu + # cakeExtraArgs: --avd-target="`"system-images;android-30;google_apis;x86`"" + # preBuildSteps: + # - bash: sh -c "echo \"y\" | $ANDROID_HOME/tools/bin/sdkmanager \"system-images;android-30;google_apis;x86\"" + # displayName: Install the Android emulators diff --git a/devopsnuget.config b/devopsnuget.config index db87568b5..8f831e620 100644 --- a/devopsnuget.config +++ b/devopsnuget.config @@ -4,6 +4,7 @@ +