diff --git a/.openpublishing.publish.config.json b/.openpublishing.publish.config.json
index b0ddbf4b4..e196e49f2 100644
--- a/.openpublishing.publish.config.json
+++ b/.openpublishing.publish.config.json
@@ -80,5 +80,8 @@
"target_framework": "net45",
"version": "latest"
}
- ]
+ ],
+ "docs_build_engine": {
+ "name": "docfx_v3"
+ }
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7260abb45..826476b3c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -87,7 +87,7 @@ Every pull request which affects public types or members should include correspo
If you're looking for something to fix, please browse [open issues](https://github.com/xamarin/Essentials/issues).
-Follow the style used by the [.NET Foundation](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md), with two primary exceptions:
+Follow the style used by the [.NET Foundation](https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md), with two primary exceptions:
- We do not use the `private` keyword as it is the default accessibility level in C#.
- We will **not** use `_` or `s_` as a prefix for internal or private field names
diff --git a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
index 3634e033b..fba0d928f 100644
--- a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
+++ b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
@@ -49,22 +49,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/DeviceTests/DeviceTests.Android/MainActivity.cs b/DeviceTests/DeviceTests.Android/MainActivity.cs
index 4a0908899..5e176fe81 100644
--- a/DeviceTests/DeviceTests.Android/MainActivity.cs
+++ b/DeviceTests/DeviceTests.Android/MainActivity.cs
@@ -18,7 +18,7 @@ protected override void OnCreate(Bundle bundle)
Xamarin.Essentials.Platform.Init(this, bundle);
var hostIp = Intent.Extras?.GetString("HOST_IP", null);
- var hostPort = Intent.Extras?.GetInt("HOST_PORT", 10578) ?? 10578;
+ var hostPort = Intent.Extras?.GetInt("HOST_PORT", 63559) ?? 63559;
if (!string.IsNullOrEmpty(hostIp))
{
diff --git a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
index ffaf3624a..491919aab 100644
--- a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
+++ b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/DeviceTests/DeviceTests.Android/Tests/FileProvider_Tests.cs b/DeviceTests/DeviceTests.Android/Tests/FileProvider_Tests.cs
index ca18121e3..0db2872a9 100644
--- a/DeviceTests/DeviceTests.Android/Tests/FileProvider_Tests.cs
+++ b/DeviceTests/DeviceTests.Android/Tests/FileProvider_Tests.cs
@@ -20,7 +20,7 @@ public void Share_Simple_Text_File_Test()
Assert.False(FileProvider.IsFileInPublicLocation(file));
// Actually get a safe shareable file uri
- var shareableUri = Platform.GetShareableFileUri(file);
+ var shareableUri = Platform.GetShareableFileUri(new ReadOnlyFile(file));
// Launch an intent to let tye user pick where to open this content
var intent = new Android.Content.Intent(Android.Content.Intent.ActionSend);
@@ -232,7 +232,7 @@ static Android.Net.Uri GetShareableUri(string file, FileProviderLocation locatio
FileProvider.TemporaryLocation = location;
// get the uri
- return Platform.GetShareableFileUri(file);
+ return Platform.GetShareableFileUri(new ReadOnlyFile(file));
}
finally
{
diff --git a/DeviceTests/DeviceTests.Shared/AppActions_Tests.cs b/DeviceTests/DeviceTests.Shared/AppActions_Tests.cs
new file mode 100644
index 000000000..04189303e
--- /dev/null
+++ b/DeviceTests/DeviceTests.Shared/AppActions_Tests.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+using Xunit;
+
+namespace DeviceTests
+{
+ public class AppActions_Tests
+ {
+ [Fact]
+ public void IsSupported()
+ {
+ var expectSupported = false;
+
+#if __ANDROID_25__
+ expectSupported = true;
+#endif
+
+#if __IOS__
+ if (Platform.HasOSVersion(9, 0))
+ expectSupported = true;
+#endif
+ Assert.Equal(expectSupported, AppActions.IsSupported);
+ }
+
+#if __ANDROID_25__ || __IOS__
+ [Fact]
+ public async Task GetSetItems()
+ {
+ if (!AppActions.IsSupported)
+ return;
+
+ var actions = new List
+ {
+ new AppAction("TEST1", "Test 1", "This is a test", "myapp://test1"),
+ new AppAction("TEST2", "Test 2", "This is a test 2", "myapp://test2"),
+ };
+
+ await AppActions.SetAsync(actions);
+
+ var get = await AppActions.GetAsync();
+
+ Assert.Contains(get, a => a.Id == "TEST1");
+ }
+#endif
+ }
+}
diff --git a/DeviceTests/DeviceTests.Shared/DeviceTests.Shared.csproj b/DeviceTests/DeviceTests.Shared/DeviceTests.Shared.csproj
index fd15dda89..f84c87329 100644
--- a/DeviceTests/DeviceTests.Shared/DeviceTests.Shared.csproj
+++ b/DeviceTests/DeviceTests.Shared/DeviceTests.Shared.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/DeviceTests/DeviceTests.Shared/HapticFeedback_Tests.cs b/DeviceTests/DeviceTests.Shared/HapticFeedback_Tests.cs
new file mode 100644
index 000000000..23a996c30
--- /dev/null
+++ b/DeviceTests/DeviceTests.Shared/HapticFeedback_Tests.cs
@@ -0,0 +1,15 @@
+using System;
+using Xamarin.Essentials;
+using Xunit;
+
+namespace DeviceTests
+{
+ public class HapticFeedback_Tests
+ {
+ [Fact]
+ public void Click() => HapticFeedback.Perform(HapticFeedbackType.Click);
+
+ [Fact]
+ public void LongPress() => HapticFeedback.Perform(HapticFeedbackType.LongPress);
+ }
+}
diff --git a/DeviceTests/DeviceTests.UWP/DeviceTests.UWP.csproj b/DeviceTests/DeviceTests.UWP/DeviceTests.UWP.csproj
index 5fd16a1d1..e69730a56 100644
--- a/DeviceTests/DeviceTests.UWP/DeviceTests.UWP.csproj
+++ b/DeviceTests/DeviceTests.UWP/DeviceTests.UWP.csproj
@@ -114,7 +114,7 @@
-
+
diff --git a/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj b/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj
index 8a14bb7c3..d285e57f0 100644
--- a/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj
+++ b/DeviceTests/DeviceTests.iOS/DeviceTests.iOS.csproj
@@ -77,7 +77,7 @@
-
+
diff --git a/DeviceTests/build.cake b/DeviceTests/build.cake
index f0d304342..40a4c0cc5 100644
--- a/DeviceTests/build.cake
+++ b/DeviceTests/build.cake
@@ -1,38 +1,55 @@
#addin nuget:?package=Cake.AppleSimulator&version=0.2.0
#addin nuget:?package=Cake.Android.Adb&version=3.2.0
#addin nuget:?package=Cake.Android.AvdManager&version=2.2.0
-#addin nuget:?package=Cake.FileHelpers
+#addin nuget:?package=Cake.FileHelpers&version=3.3.0
var TARGET = Argument("target", "Default");
-var IOS_SIM_NAME = EnvironmentVariable("IOS_SIM_NAME") ?? "iPhone 11";
-var IOS_SIM_RUNTIME = EnvironmentVariable("IOS_SIM_RUNTIME") ?? "com.apple.CoreSimulator.SimRuntime.iOS-13-1";
+var IOS_SIM_NAME = Argument("ios-device", EnvironmentVariable("IOS_SIM_NAME") ?? "iPhone 11");
+var IOS_SIM_RUNTIME = Argument("ios-runtime", EnvironmentVariable("IOS_SIM_RUNTIME") ?? "com.apple.CoreSimulator.SimRuntime.iOS-13-7");
var IOS_PROJ = "./DeviceTests.iOS/DeviceTests.iOS.csproj";
var IOS_BUNDLE_ID = "com.xamarin.essentials.devicetests";
var IOS_IPA_PATH = "./DeviceTests.iOS/bin/iPhoneSimulator/Release/XamarinEssentialsDeviceTestsiOS.app";
-var IOS_TEST_RESULTS_PATH = "./xunit-ios.xml";
+var IOS_TEST_RESULTS_PATH = MakeAbsolute((FilePath)"../output/test-results/ios/TestResults.xml");
var ANDROID_PROJ = "./DeviceTests.Android/DeviceTests.Android.csproj";
var ANDROID_APK_PATH = "./DeviceTests.Android/bin/Release/com.xamarin.essentials.devicetests-Signed.apk";
-var ANDROID_TEST_RESULTS_PATH = "./xunit-android.xml";
+var ANDROID_TEST_RESULTS_PATH = MakeAbsolute((FilePath)"../output/test-results/android/TestResults.xml");
+var ANDROID_SCREENSHOT_PATH = MakeAbsolute((DirectoryPath)"../output/test-results/android");
var ANDROID_AVD = EnvironmentVariable("ANDROID_AVD") ?? "CABOODLE";
var ANDROID_PKG_NAME = "com.xamarin.essentials.devicetests";
-var ANDROID_EMU_TARGET = EnvironmentVariable("ANDROID_EMU_TARGET") ?? "system-images;android-29;google_apis_playstore;x86_64";
-var ANDROID_EMU_DEVICE = EnvironmentVariable("ANDROID_EMU_DEVICE") ?? "pixel";
+var ANDROID_EMU_TARGET = Argument("avd-target", EnvironmentVariable("ANDROID_EMU_TARGET") ?? "system-images;android-29;google_apis_playstore;x86");
+var ANDROID_EMU_DEVICE = Argument("avd-device", EnvironmentVariable("ANDROID_EMU_DEVICE") ?? "Nexus 5X");
var UWP_PROJ = "./DeviceTests.UWP/DeviceTests.UWP.csproj";
-var UWP_TEST_RESULTS_PATH = "./xunit-uwp.xml";
+var UWP_TEST_RESULTS_PATH = MakeAbsolute((FilePath)"../output/test-results/uwp/TestResults.xml");
var UWP_PACKAGE_ID = "ec0cc741-fd3e-485c-81be-68815c480690";
var TCP_LISTEN_TIMEOUT = 240;
-var TCP_LISTEN_PORT = 10578;
+var TCP_LISTEN_PORT = 63559;
var TCP_LISTEN_HOST = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName())
- .AddressList.First(f => f.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).ToString();
+ .AddressList.First(f => f.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+ .ToString();
+
+var OUTPUT_PATH = MakeAbsolute((DirectoryPath)"../output/");
var ANDROID_HOME = EnvironmentVariable("ANDROID_HOME");
-Func DownloadTcpTextAsync = (int port, FilePath filename) =>
- System.Threading.Tasks.Task.Run (() => {
+System.Environment.SetEnvironmentVariable("PATH",
+ $"{ANDROID_HOME}/tools/bin" + System.IO.Path.PathSeparator +
+ $"{ANDROID_HOME}/platform-tools" + System.IO.Path.PathSeparator +
+ $"{ANDROID_HOME}/emulator" + System.IO.Path.PathSeparator +
+ EnvironmentVariable("PATH"));
+
+
+// utils
+
+Task DownloadTcpTextAsync(int port, FilePath filename, Action waitAction = null)
+{
+ filename = MakeAbsolute(filename);
+ EnsureDirectoryExists(filename.GetDirectory());
+
+ return System.Threading.Tasks.Task.Run(() => {
var tcpListener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, port);
tcpListener.Start();
var listening = true;
@@ -40,9 +57,11 @@ Func DownloadTcpTextAsync = (int port, FilePath filename) =
System.Threading.Tasks.Task.Run(() => {
// Sleep until timeout elapses or tcp listener stopped after a successful connection
var elapsed = 0;
- while (elapsed <= TCP_LISTEN_TIMEOUT && listening) {
+ while(elapsed <= TCP_LISTEN_TIMEOUT && listening) {
System.Threading.Thread.Sleep(1000);
elapsed++;
+ Information($"Still waiting for tests... {elapsed}/{TCP_LISTEN_TIMEOUT}");
+ waitAction?.Invoke();
}
// If still listening, timeout elapsed, stop the listener
@@ -54,76 +73,76 @@ Func DownloadTcpTextAsync = (int port, FilePath filename) =
try {
var tcpClient = tcpListener.AcceptTcpClient();
- var fileName = MakeAbsolute (filename).FullPath;
- using (var file = System.IO.File.Open(fileName, System.IO.FileMode.Create))
+ using (var file = System.IO.File.Open(filename.FullPath, System.IO.FileMode.Create))
using (var stream = tcpClient.GetStream())
stream.CopyTo(file);
tcpClient.Close();
tcpListener.Stop();
- listening = false;
+ listening = false;
} catch {
throw new Exception("Test results listener failed or timed out.");
}
});
+}
-Action AddPlatformToTestResults = (FilePath testResultsFile, string platformName) => {
+void AddPlatformToTestResults(FilePath testResultsFile, string platformName)
+{
if (FileExists(testResultsFile)) {
var txt = FileReadText(testResultsFile);
txt = txt.Replace("
+
+// iOS tasks
+
+Task("build-ios")
+ .Does(() =>
{
// Setup the test listener config to be built into the app
FileWriteText((new FilePath(IOS_PROJ)).GetDirectory().CombineWithFilePath("tests.cfg"), $"{TCP_LISTEN_HOST}:{TCP_LISTEN_PORT}");
- // Nuget restore
- MSBuild (IOS_PROJ, c => {
- c.Configuration = "Release";
- c.Targets.Clear();
- c.Targets.Add("Restore");
- });
-
- // Build the project (with ipa)
- MSBuild (IOS_PROJ, c => {
+ MSBuild(IOS_PROJ, c => {
c.Configuration = "Release";
+ c.Restore = true;
c.Properties["Platform"] = new List { "iPhoneSimulator" };
c.Properties["BuildIpa"] = new List { "true" };
c.Properties["ContinuousIntegrationBuild"] = new List { "false" };
c.Targets.Clear();
c.Targets.Add("Rebuild");
+ c.BinaryLogger = new MSBuildBinaryLogSettings {
+ Enabled = true,
+ FileName = OUTPUT_PATH.CombineWithFilePath("binlogs/device-tests-ios-build.binlog").FullPath,
+ };
});
});
-Task ("test-ios-emu")
- .IsDependentOn ("build-ios")
- .Does (() =>
+Task("test-ios-emu")
+ .IsDependentOn("build-ios")
+ .Does(() =>
{
- var sims = ListAppleSimulators ();
- foreach (var s in sims)
- {
- Information("Info: {0} ({1} - {2} - {3})", s.Name, s.Runtime, s.UDID, s.Availability);
+ var sims = ListAppleSimulators();
+ foreach (var s in sims) {
+ Information("Info: {0}({1} - {2} - {3})", s.Name, s.Runtime, s.UDID, s.Availability);
}
// Look for a matching simulator on the system
- var sim = sims.First (s => s.Name == IOS_SIM_NAME && s.Runtime == IOS_SIM_RUNTIME);
+ var sim = sims.First(s => s.Name == IOS_SIM_NAME && s.Runtime == IOS_SIM_RUNTIME);
// Boot the simulator
- Information("Booting: {0} ({1} - {2})", sim.Name, sim.Runtime, sim.UDID);
- if (!sim.State.ToLower().Contains ("booted"))
- BootAppleSimulator (sim.UDID);
+ Information("Booting: {0}({1} - {2})", sim.Name, sim.Runtime, sim.UDID);
+ if (!sim.State.ToLower().Contains("booted"))
+ BootAppleSimulator(sim.UDID);
// Wait for it to be booted
var booted = false;
for (int i = 0; i < 100; i++) {
- if (ListAppleSimulators().Any (s => s.UDID == sim.UDID && s.State.ToLower().Contains("booted"))) {
+ if (ListAppleSimulators().Any(s => s.UDID == sim.UDID && s.State.ToLower().Contains("booted"))) {
booted = true;
break;
}
@@ -132,12 +151,12 @@ Task ("test-ios-emu")
// Install the IPA that was previously built
var ipaPath = new FilePath(IOS_IPA_PATH);
- Information ("Installing: {0}", ipaPath);
+ Information("Installing: {0}", ipaPath);
InstalliOSApplication(sim.UDID, MakeAbsolute(ipaPath).FullPath);
// Start our Test Results TCP listener
Information("Started TCP Test Results Listener on port: {0}", TCP_LISTEN_PORT);
- var tcpListenerTask = DownloadTcpTextAsync (TCP_LISTEN_PORT, IOS_TEST_RESULTS_PATH);
+ var tcpListenerTask = DownloadTcpTextAsync(TCP_LISTEN_PORT, IOS_TEST_RESULTS_PATH);
// Launch the IPA
Information("Launching: {0}", IOS_BUNDLE_ID);
@@ -145,53 +164,53 @@ Task ("test-ios-emu")
// Wait for the TCP listener to get results
Information("Waiting for tests...");
- tcpListenerTask.Wait ();
+ tcpListenerTask.Wait();
AddPlatformToTestResults(IOS_TEST_RESULTS_PATH, "iOS");
// Close up simulators
Information("Closing Simulator");
- ShutdownAllAppleSimulators ();
+ ShutdownAllAppleSimulators();
});
-Task ("build-android")
- .Does (() =>
-{
- // Nuget restore
- MSBuild (ANDROID_PROJ, c => {
- c.Configuration = "Debug";
- c.Targets.Clear();
- c.Targets.Add("Restore");
- });
+// Android tasks
+Task("build-android")
+ .Does(() =>
+{
// Build the app in debug mode
// needs to be debug so unit tests get discovered
- MSBuild (ANDROID_PROJ, c => {
+ MSBuild(ANDROID_PROJ, c => {
c.Configuration = "Debug";
+ c.Restore = true;
c.Properties["ContinuousIntegrationBuild"] = new List { "false" };
c.Targets.Clear();
c.Targets.Add("Rebuild");
+ c.BinaryLogger = new MSBuildBinaryLogSettings {
+ Enabled = true,
+ FileName = OUTPUT_PATH.CombineWithFilePath("binlogs/device-tests-android-build.binlog").FullPath,
+ };
});
});
-Task ("test-android-emu")
- .IsDependentOn ("build-android")
- .Does (() =>
+Task("test-android-emu")
+ .IsDependentOn("build-android")
+ .Does(() =>
{
- var avdSettings = new AndroidAvdManagerToolSettings { SdkRoot = ANDROID_HOME };
- var emuSettings = new AndroidEmulatorToolSettings { SdkRoot = ANDROID_HOME };
+ var avdSettings = new AndroidAvdManagerToolSettings { SdkRoot = ANDROID_HOME };
// Delete AVD first, if it exists
- Information ("Deleting AVD if exists: {0}...", ANDROID_AVD);
+ Information("Deleting AVD if exists: {0}...", ANDROID_AVD);
try { AndroidAvdDelete(ANDROID_AVD, avdSettings); }
catch { }
// Create the AVD
- Information ("Creating AVD: {0}...", ANDROID_AVD);
- AndroidAvdCreate (ANDROID_AVD, ANDROID_EMU_TARGET, ANDROID_EMU_DEVICE, force: true, settings: avdSettings);
-
- Information ("Starting Emulator: {0}...", ANDROID_AVD);
+ Information("Creating AVD: {0}...", ANDROID_AVD);
+ AndroidAvdCreate(ANDROID_AVD, ANDROID_EMU_TARGET, ANDROID_EMU_DEVICE, force: true, settings: avdSettings);
+
+ Information("Starting Emulator: {0}...", ANDROID_AVD);
+ var emuSettings = new AndroidEmulatorToolSettings { SdkRoot = ANDROID_HOME, ArgumentCustomization = args => args.Append("-no-window") };
var emulatorProcess = AndroidEmulatorStart(ANDROID_AVD, emuSettings);
var adbSettings = new AdbToolSettings { SdkRoot = ANDROID_HOME };
@@ -212,38 +231,54 @@ Task ("test-android-emu")
System.Threading.Thread.Sleep(1000);
}
- Information ("Matched ADB Serial: {0}", emuSerial);
+ Information("Matched ADB Serial: {0}", emuSerial);
adbSettings = new AdbToolSettings { SdkRoot = ANDROID_HOME, Serial = emuSerial };
// Wait for the emulator to enter a 'booted' state
AdbWaitForEmulatorToBoot(TimeSpan.FromSeconds(100), adbSettings);
- Information ("Emulator finished booting.");
+ Information("Emulator finished booting.");
+
+ // Read the logcat
+ AdbLogcat(new AdbLogcatOptions { Clear = true }, settings: adbSettings);
+ AdbLogcat(settings: adbSettings);
- // Try uninstalling the existing package (if installed)
- try {
- AdbUninstall (ANDROID_PKG_NAME, false, adbSettings);
- Information ("Uninstalled old: {0}", ANDROID_PKG_NAME);
+ // Try uninstalling the existing package(if installed)
+ try {
+ AdbUninstall(ANDROID_PKG_NAME, false, adbSettings);
+ Information("Uninstalled old: {0}", ANDROID_PKG_NAME);
} catch { }
// Use the Install target to push the app onto emulator
- MSBuild (ANDROID_PROJ, c => {
+ MSBuild(ANDROID_PROJ, c => {
c.Configuration = "Debug";
c.Properties["ContinuousIntegrationBuild"] = new List { "false" };
c.Properties["AdbTarget"] = new List { "-s " + emuSerial };
c.Targets.Clear();
c.Targets.Add("Install");
+ c.BinaryLogger = new MSBuildBinaryLogSettings {
+ Enabled = true,
+ FileName = OUTPUT_PATH.CombineWithFilePath("binlogs/device-tests-android-install.binlog").FullPath,
+ };
});
// Start the TCP Test results listener
Information("Started TCP Test Results Listener on port: {0}:{1}", TCP_LISTEN_HOST, TCP_LISTEN_PORT);
- var tcpListenerTask = DownloadTcpTextAsync (TCP_LISTEN_PORT, ANDROID_TEST_RESULTS_PATH);
+ // var printed = false;
+ var tcpListenerTask = DownloadTcpTextAsync(TCP_LISTEN_PORT, ANDROID_TEST_RESULTS_PATH, () => {
+ // if (!printed) {
+ // AdbScreenCapture(ANDROID_SCREENSHOT_PATH.CombineWithFilePath($"screenshot.png"), adbSettings);
+ // AdbLogcat(settings: adbSettings);
+ // printed = true;
+ // }
+ });
// Launch the app on the emulator
- AdbShell ($"am start -n {ANDROID_PKG_NAME}/{ANDROID_PKG_NAME}.MainActivity --es HOST_IP {TCP_LISTEN_HOST} --ei HOST_PORT {TCP_LISTEN_PORT}", adbSettings);
+ AdbShell($"am start -n {ANDROID_PKG_NAME}/{ANDROID_PKG_NAME}.MainActivity --es HOST_IP {TCP_LISTEN_HOST} --ei HOST_PORT {TCP_LISTEN_PORT}", adbSettings);
+ AdbLogcat(settings: adbSettings);
// Wait for the test results to come back
Information("Waiting for tests...");
- tcpListenerTask.Wait ();
+ tcpListenerTask.Wait();
AddPlatformToTestResults(ANDROID_TEST_RESULTS_PATH, "Android");
@@ -260,55 +295,54 @@ Task ("test-android-emu")
});
-Task ("build-uwp")
- .Does (() =>
-{
- // Nuget restore
- MSBuild (UWP_PROJ, c => {
- c.Targets.Clear();
- c.Targets.Add("Restore");
- });
+// UWP tasks
- // Build the project (with ipa)
- MSBuild (UWP_PROJ, c => {
+Task("build-uwp")
+ .Does(() =>
+{
+ MSBuild(UWP_PROJ, c => {
c.Configuration = "Debug";
+ c.Restore = true;
c.Properties["ContinuousIntegrationBuild"] = new List { "false" };
c.Properties["AppxBundlePlatforms"] = new List { "x86" };
c.Properties["AppxBundle"] = new List { "Always" };
c.Targets.Clear();
c.Targets.Add("Rebuild");
+ c.BinaryLogger = new MSBuildBinaryLogSettings {
+ Enabled = true,
+ FileName = OUTPUT_PATH.CombineWithFilePath("binlogs/device-tests-uwp-build.binlog").FullPath,
+ };
});
});
-
-Task ("test-uwp-emu")
- .IsDependentOn ("build-uwp")
+Task("test-uwp-emu")
+ .IsDependentOn("build-uwp")
.WithCriteria(IsRunningOnWindows())
- .Does (() =>
+ .Does(() =>
{
- var uninstallPS = new Action (() => {
+ var uninstallPS = new Action(() => {
try {
- StartProcess ("powershell",
+ StartProcess("powershell",
"$app = Get-AppxPackage -Name " + UWP_PACKAGE_ID + "; if ($app) { Remove-AppxPackage -Package $app.PackageFullName }");
} catch { }
});
// Try to uninstall the app if it exists from before
uninstallPS();
-
+
// Install the appx
var dependencies = GetFiles("./**/AppPackages/**/Dependencies/x86/*.appx");
foreach (var dep in dependencies) {
Information("Installing Dependency appx: {0}", dep);
StartProcess("powershell", "Add-AppxPackage -Path \"" + MakeAbsolute(dep).FullPath + "\"");
}
- var appxBundlePath = GetFiles("./**/AppPackages/**/*.appxbundle").First ();
+ var appxBundlePath = GetFiles("./**/AppPackages/**/*.appxbundle").First();
Information("Installing appx: {0}", appxBundlePath);
- StartProcess ("powershell", "Add-AppxPackage -Path \"" + MakeAbsolute(appxBundlePath).FullPath + "\"");
+ StartProcess("powershell", "Add-AppxPackage -Path \"" + MakeAbsolute(appxBundlePath).FullPath + "\"");
// Start the TCP Test results listener
Information("Started TCP Test Results Listener on port: {0}:{1}", TCP_LISTEN_HOST, TCP_LISTEN_PORT);
- var tcpListenerTask = DownloadTcpTextAsync (TCP_LISTEN_PORT, UWP_TEST_RESULTS_PATH);
+ var tcpListenerTask = DownloadTcpTextAsync(TCP_LISTEN_PORT, UWP_TEST_RESULTS_PATH);
// Launch the app
Information("Running appx: {0}", appxBundlePath);
@@ -317,12 +351,13 @@ Task ("test-uwp-emu")
// Wait for the test results to come back
Information("Waiting for tests...");
- tcpListenerTask.Wait ();
+ tcpListenerTask.Wait();
AddPlatformToTestResults(UWP_TEST_RESULTS_PATH, "UWP");
- // Uninstall the app (this will terminate it too)
+ // Uninstall the app(this will terminate it too)
uninstallPS();
});
+
RunTarget(TARGET);
diff --git a/README.md b/README.md
index 4c193add7..23cafdfd5 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Xamarin.Essentials gives developers essential cross-platform APIs for their mobi
iOS, Android, and UWP offer unique operating system and platform APIs that developers have access to, all in C# leveraging Xamarin. It is great that developers have 100% API access in C# with Xamarin, but these APIs are different per platform. This means developers have to learn three different APIs to access platform-specific features. With Xamarin.Essentials, developers have a single cross-platform API that works with any iOS, Android, or UWP application that can be accessed from shared code no matter how the user interface is created.
-[](https://gitter.im/xamarin/Essentials)
+[](https://discord.com/invite/Y8828kE)
## Build Status
diff --git a/Samples/Sample.Server.WebAuthenticator/Controllers/MobileAuthController.cs b/Samples/Sample.Server.WebAuthenticator/Controllers/MobileAuthController.cs
index 71cf15823..10c288c96 100644
--- a/Samples/Sample.Server.WebAuthenticator/Controllers/MobileAuthController.cs
+++ b/Samples/Sample.Server.WebAuthenticator/Controllers/MobileAuthController.cs
@@ -31,13 +31,18 @@ public async Task Get([FromRoute]string scheme)
}
else
{
+ var claims = auth.Principal.Identities.FirstOrDefault()?.Claims;
+ var email = string.Empty;
+ email = claims?.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value;
+
// Get parameters to send back to the callback
var qs = new Dictionary
- {
- { "access_token", auth.Properties.GetTokenValue("access_token") },
- { "refresh_token", auth.Properties.GetTokenValue("refresh_token") ?? string.Empty },
- { "expires", (auth.Properties.ExpiresUtc?.ToUnixTimeSeconds() ?? -1).ToString() }
- };
+ {
+ { "access_token", auth.Properties.GetTokenValue("access_token") },
+ { "refresh_token", auth.Properties.GetTokenValue("refresh_token") ?? string.Empty },
+ { "expires", (auth.Properties.ExpiresUtc?.ToUnixTimeSeconds() ?? -1).ToString() },
+ { "email", email }
+ };
// Build the result url
var url = callbackScheme + "://#" + string.Join(
diff --git a/Samples/Sample.Server.WebAuthenticator/Startup.cs b/Samples/Sample.Server.WebAuthenticator/Startup.cs
index cfd45d086..52a403314 100644
--- a/Samples/Sample.Server.WebAuthenticator/Startup.cs
+++ b/Samples/Sample.Server.WebAuthenticator/Startup.cs
@@ -62,6 +62,13 @@ public void ConfigureServices(IServiceCollection services)
=> WebHostEnvironment.ContentRootFileProvider.GetFileInfo($"AuthKey_{keyId}.p8"));
a.SaveTokens = true;
});
+
+ /*
+ * For Apple signin
+ * If you are running the app on Azure you must add the Configuration setting
+ * WEBSITE_LOAD_USER_PROFILE = 1
+ * Without this setting you will get a File Not Found exception when AppleAuthenticationHandler tries to generate a certificate using your Auth_{keyId].P8 file.
+ */
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/Samples/Samples.Android/MainActivity.cs b/Samples/Samples.Android/MainActivity.cs
index 383c77fdb..16c627aef 100644
--- a/Samples/Samples.Android/MainActivity.cs
+++ b/Samples/Samples.Android/MainActivity.cs
@@ -4,12 +4,18 @@
using Android.OS;
using Android.Runtime;
using Android.Widget;
+using Samples.View;
namespace Samples.Droid
{
[Activity(Label = "@string/app_name", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
+ [IntentFilter(
+ new[] { Xamarin.Essentials.Platform.Intent.ActionAppAction },
+ Categories = new[] { Intent.CategoryDefault })]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
+ static App formsApp;
+
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
@@ -23,19 +29,27 @@ protected override void OnCreate(Bundle bundle)
Xamarin.Essentials.Platform.ActivityStateChanged += Platform_ActivityStateChanged;
- LoadApplication(new App());
+ LoadApplication(formsApp ??= new App());
}
protected override void OnResume()
{
base.OnResume();
- Xamarin.Essentials.Platform.OnResume();
+ Xamarin.Essentials.Platform.OnResume(this);
+ }
+
+ protected override void OnNewIntent(Intent intent)
+ {
+ base.OnNewIntent(intent);
+
+ Xamarin.Essentials.Platform.OnNewIntent(intent);
}
protected override void OnDestroy()
{
base.OnDestroy();
+
Xamarin.Essentials.Platform.ActivityStateChanged -= Platform_ActivityStateChanged;
}
diff --git a/Samples/Samples.Android/Properties/AndroidManifest.xml b/Samples/Samples.Android/Properties/AndroidManifest.xml
index 95365fa47..22facf77a 100644
--- a/Samples/Samples.Android/Properties/AndroidManifest.xml
+++ b/Samples/Samples.Android/Properties/AndroidManifest.xml
@@ -8,7 +8,10 @@
+
+
+
diff --git a/Samples/Samples.Android/Resources/drawable-hdpi/app_info_action_icon.png b/Samples/Samples.Android/Resources/drawable-hdpi/app_info_action_icon.png
new file mode 100644
index 000000000..28bd78b2b
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-hdpi/app_info_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable-hdpi/battery_action_icon.png b/Samples/Samples.Android/Resources/drawable-hdpi/battery_action_icon.png
new file mode 100644
index 000000000..2b9ecc489
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-hdpi/battery_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable-xhdpi/app_info_action_icon.png b/Samples/Samples.Android/Resources/drawable-xhdpi/app_info_action_icon.png
new file mode 100644
index 000000000..4b0bd34ea
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-xhdpi/app_info_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable-xhdpi/battery_action_icon.png b/Samples/Samples.Android/Resources/drawable-xhdpi/battery_action_icon.png
new file mode 100644
index 000000000..0f0339dfa
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-xhdpi/battery_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable-xxhdpi/app_info_action_icon.png b/Samples/Samples.Android/Resources/drawable-xxhdpi/app_info_action_icon.png
new file mode 100644
index 000000000..b3209b088
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-xxhdpi/app_info_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable-xxhdpi/battery_action_icon.png b/Samples/Samples.Android/Resources/drawable-xxhdpi/battery_action_icon.png
new file mode 100644
index 000000000..554b363a1
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable-xxhdpi/battery_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable/app_info_action_icon.png b/Samples/Samples.Android/Resources/drawable/app_info_action_icon.png
new file mode 100644
index 000000000..28bd78b2b
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable/app_info_action_icon.png differ
diff --git a/Samples/Samples.Android/Resources/drawable/battery_action_icon.png b/Samples/Samples.Android/Resources/drawable/battery_action_icon.png
new file mode 100644
index 000000000..2b9ecc489
Binary files /dev/null and b/Samples/Samples.Android/Resources/drawable/battery_action_icon.png differ
diff --git a/Samples/Samples.Android/Samples.Android.csproj b/Samples/Samples.Android/Samples.Android.csproj
index 17e189237..9ac88fafe 100644
--- a/Samples/Samples.Android/Samples.Android.csproj
+++ b/Samples/Samples.Android/Samples.Android.csproj
@@ -44,7 +44,7 @@
true
false
armeabi-v7a;x86
- true
+ r8
Full
Xamarin.Forms.Platform.Android;Xamarin.Forms.Platform;Xamarin.Forms.Core;Xamarin.Forms.Xaml;Samples;FormsViewGroup;
@@ -62,14 +62,11 @@
-
-
-
-
-
-
-
+
+
+
+
@@ -114,6 +111,14 @@
+
+
+
+
+
+
+
+
diff --git a/Samples/Samples.Mac/AppDelegate.cs b/Samples/Samples.Mac/AppDelegate.cs
new file mode 100644
index 000000000..1a006d273
--- /dev/null
+++ b/Samples/Samples.Mac/AppDelegate.cs
@@ -0,0 +1,44 @@
+using AppKit;
+using CoreGraphics;
+using Foundation;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.MacOS;
+
+namespace Samples.Mac
+{
+ [Register(nameof(AppDelegate))]
+ public class AppDelegate : FormsApplicationDelegate
+ {
+ static App formsApp;
+
+ NSWindow window;
+
+ public AppDelegate()
+ {
+ var style = NSWindowStyle.Closable | NSWindowStyle.Resizable | NSWindowStyle.Titled;
+
+ var screenSize = NSScreen.MainScreen.Frame.Size;
+ var rect = new CGRect(0, 0, 1024, 768);
+ rect.Offset((screenSize.Width - rect.Width) / 2, (screenSize.Height - rect.Height) / 2);
+
+ window = new NSWindow(rect, style, NSBackingStore.Buffered, false)
+ {
+ Title = "Xamarin.Essentials",
+ TitleVisibility = NSWindowTitleVisibility.Hidden,
+ };
+ }
+
+ public override NSWindow MainWindow => window;
+
+ public override void DidFinishLaunching(NSNotification notification)
+ {
+ Forms.Init();
+
+ LoadApplication(formsApp ??= new App());
+
+ base.DidFinishLaunching(notification);
+ }
+
+ public override bool ApplicationShouldTerminateAfterLastWindowClosed(NSApplication sender) => true;
+ }
+}
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png
new file mode 100644
index 000000000..d0b5a8098
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png
new file mode 100644
index 000000000..f4c8d2904
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png
new file mode 100644
index 000000000..ebb5a0fe4
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png
new file mode 100644
index 000000000..0986d31be
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png
new file mode 100644
index 000000000..f4c8d2904
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png
new file mode 100644
index 000000000..a142c83fb
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png
new file mode 100644
index 000000000..0986d31be
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png
new file mode 100644
index 000000000..412d6ca9b
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png
new file mode 100644
index 000000000..a142c83fb
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png
new file mode 100644
index 000000000..e99022ae8
Binary files /dev/null and b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ
diff --git a/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..6b2854529
--- /dev/null
+++ b/Samples/Samples.Mac/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images": [
+ {
+ "filename": "AppIcon-16.png",
+ "size": "16x16",
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-16@2x.png",
+ "size": "16x16",
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-32.png",
+ "size": "32x32",
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-32@2x.png",
+ "size": "32x32",
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-128.png",
+ "size": "128x128",
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-128@2x.png",
+ "size": "128x128",
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-256.png",
+ "size": "256x256",
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-256@2x.png",
+ "size": "256x256",
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-512.png",
+ "size": "512x512",
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "filename": "AppIcon-512@2x.png",
+ "size": "512x512",
+ "scale": "2x",
+ "idiom": "mac"
+ }
+ ],
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Samples/Samples.Mac/Assets.xcassets/Contents.json b/Samples/Samples.Mac/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..4caf392f9
--- /dev/null
+++ b/Samples/Samples.Mac/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Samples/Samples.Mac/Entitlements.plist b/Samples/Samples.Mac/Entitlements.plist
new file mode 100644
index 000000000..9ae599370
--- /dev/null
+++ b/Samples/Samples.Mac/Entitlements.plist
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Samples/Samples.Mac/Info.plist b/Samples/Samples.Mac/Info.plist
new file mode 100644
index 000000000..dbd109b59
--- /dev/null
+++ b/Samples/Samples.Mac/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleName
+ Xamarin.Essentials
+ CFBundleIdentifier
+ com.xamarin.essentials
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 10.12
+ CFBundleDevelopmentRegion
+ en
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ ????
+ NSHumanReadableCopyright
+ © Microsoft Corporation. All rights reserved.
+ NSPrincipalClass
+ NSApplication
+ NSMainStoryboardFile
+ Main
+ XSAppIconAssets
+ Assets.xcassets/AppIcon.appiconset
+ NSLocationWhenInUseUsageDescription
+ Access to your location is required for cool things to happen!
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ xamarinessentials
+ CFBundleURLSchemes
+
+ xamarinessentials
+
+ CFBundleTypeRole
+ Editor
+
+
+
+
diff --git a/Samples/Samples.Mac/Main.cs b/Samples/Samples.Mac/Main.cs
new file mode 100644
index 000000000..2d1f2545c
--- /dev/null
+++ b/Samples/Samples.Mac/Main.cs
@@ -0,0 +1,14 @@
+using AppKit;
+
+namespace Samples.Mac
+{
+ static class MainClass
+ {
+ static void Main(string[] args)
+ {
+ NSApplication.Init();
+ NSApplication.SharedApplication.Delegate = new AppDelegate();
+ NSApplication.Main(args);
+ }
+ }
+}
diff --git a/Samples/Samples.Mac/Main.storyboard b/Samples/Samples.Mac/Main.storyboard
new file mode 100644
index 000000000..05fe4395c
--- /dev/null
+++ b/Samples/Samples.Mac/Main.storyboard
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/Samples.Mac/Resources/FileSystemTemplate.txt b/Samples/Samples.Mac/Resources/FileSystemTemplate.txt
new file mode 100644
index 000000000..184c6db9e
--- /dev/null
+++ b/Samples/Samples.Mac/Resources/FileSystemTemplate.txt
@@ -0,0 +1,4 @@
+This file was loaded from the app package.
+
+You can use this as a starting point for your comments...
+
diff --git a/Samples/Samples.Mac/Samples.Mac.csproj b/Samples/Samples.Mac/Samples.Mac.csproj
new file mode 100644
index 000000000..88774546a
--- /dev/null
+++ b/Samples/Samples.Mac/Samples.Mac.csproj
@@ -0,0 +1,106 @@
+
+
+
+ Debug
+ AnyCPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}
+ {A3F8F2AB-B479-4A4A-A458-A89E7DC349F1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ Exe
+ Samples.Mac
+ Samples.Mac
+ v2.0
+ Xamarin.Mac
+ Resources
+
+
+ true
+ portable
+ false
+ bin\Debug
+ DEBUG;
+ prompt
+ 4
+ false
+ Mac Developer
+ false
+ false
+ false
+ true
+ true
+ true
+
+
+
+
+
+ true
+ portable
+ true
+ bin\Release
+
+ prompt
+ 4
+ false
+ true
+ false
+ true
+ true
+ true
+ SdkOnly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {CD6D6AE6-83A1-41B1-BD7C-C555A77C288B}
+ Xamarin.Essentials
+
+
+ {B4227123-2EEB-494A-A221-C061B5659AED}
+ Samples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples.Tizen/Program.cs b/Samples/Samples.Tizen/Program.cs
index adf3e711e..bb570d642 100644
--- a/Samples/Samples.Tizen/Program.cs
+++ b/Samples/Samples.Tizen/Program.cs
@@ -5,11 +5,13 @@ namespace Samples.Tizen
{
class Program : FormsApplication
{
+ static App formsApp;
+
protected override void OnCreate()
{
base.OnCreate();
- LoadApplication(new App());
+ LoadApplication(formsApp ??= new App());
}
static void Main(string[] args)
diff --git a/Samples/Samples.Tizen/tizen-manifest.xml b/Samples/Samples.Tizen/tizen-manifest.xml
index b8fb3c7f7..c67be599f 100755
--- a/Samples/Samples.Tizen/tizen-manifest.xml
+++ b/Samples/Samples.Tizen/tizen-manifest.xml
@@ -1,12 +1,15 @@
-
+
Samples.Tizen.png
+
+
+ http://tizen.org/privilege/appdir.shareddata
http://tizen.org/privilege/appmanager.launch
http://tizen.org/privilege/externalstorage
http://tizen.org/privilege/haptic
@@ -17,7 +20,10 @@
http://tizen.org/privilege/mediastorage
http://tizen.org/privilege/message.read
http://tizen.org/privilege/network.get
+ http://tizen.org/privilege/externalstorage.appdata
+ http://tizen.org/privilege/contact.read
+
true
true
diff --git a/Samples/Samples.UWP/App.xaml.cs b/Samples/Samples.UWP/App.xaml.cs
index 883f4a18d..273b2cdac 100644
--- a/Samples/Samples.UWP/App.xaml.cs
+++ b/Samples/Samples.UWP/App.xaml.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
@@ -14,13 +15,6 @@ public App()
{
InitializeComponent();
Suspending += OnSuspending;
-
- MainThread.BeginInvokeOnMainThread(() =>
- {
- Console.WriteLine("Success!");
- });
-
- var test = DeviceInfo.DeviceType;
}
protected override void OnLaunched(LaunchActivatedEventArgs e)
@@ -57,6 +51,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
// Ensure the current window is active
Window.Current.Activate();
+
+ Xamarin.Essentials.Platform.OnLaunched(e);
}
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
diff --git a/Samples/Samples.UWP/Assets/app_info_action_icon.png b/Samples/Samples.UWP/Assets/app_info_action_icon.png
new file mode 100644
index 000000000..b3209b088
Binary files /dev/null and b/Samples/Samples.UWP/Assets/app_info_action_icon.png differ
diff --git a/Samples/Samples.UWP/Assets/battery_action_icon.png b/Samples/Samples.UWP/Assets/battery_action_icon.png
new file mode 100644
index 000000000..554b363a1
Binary files /dev/null and b/Samples/Samples.UWP/Assets/battery_action_icon.png differ
diff --git a/Samples/Samples.UWP/MainPage.xaml.cs b/Samples/Samples.UWP/MainPage.xaml.cs
index cd0e977ae..690282df6 100644
--- a/Samples/Samples.UWP/MainPage.xaml.cs
+++ b/Samples/Samples.UWP/MainPage.xaml.cs
@@ -4,13 +4,15 @@ namespace Samples.UWP
{
public sealed partial class MainPage : Xamarin.Forms.Platform.UWP.WindowsPage
{
+ static Samples.App formsApp;
+
public MainPage()
{
InitializeComponent();
Platform.MapServiceToken = "RJHqIE53Onrqons5CNOx~FrDr3XhjDTyEXEjng-CRoA~Aj69MhNManYUKxo6QcwZ0wmXBtyva0zwuHB04rFYAPf7qqGJ5cHb03RCDw1jIW8l";
- LoadApplication(new Samples.App());
+ LoadApplication(formsApp ??= new Samples.App());
}
}
}
diff --git a/Samples/Samples.UWP/Samples.UWP.csproj b/Samples/Samples.UWP/Samples.UWP.csproj
index 9e849cb12..f706b8de1 100644
--- a/Samples/Samples.UWP/Samples.UWP.csproj
+++ b/Samples/Samples.UWP/Samples.UWP.csproj
@@ -119,8 +119,8 @@
-
-
+
+
@@ -159,6 +159,8 @@
+
+
diff --git a/Samples/Samples.iOS/AppDelegate.cs b/Samples/Samples.iOS/AppDelegate.cs
index 3976b598c..b1f5114e3 100644
--- a/Samples/Samples.iOS/AppDelegate.cs
+++ b/Samples/Samples.iOS/AppDelegate.cs
@@ -1,5 +1,7 @@
-using Foundation;
+using System;
+using Foundation;
using Microsoft.AppCenter.Distribute;
+using Samples.View;
using UIKit;
namespace Samples.iOS
@@ -7,12 +9,15 @@ namespace Samples.iOS
[Register(nameof(AppDelegate))]
public partial class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
+ static App formsApp;
+
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Xamarin.Forms.Forms.Init();
Xamarin.Forms.FormsMaterial.Init();
+
Distribute.DontCheckForUpdatesInDebug();
- LoadApplication(new App());
+ LoadApplication(formsApp ??= new App());
return base.FinishedLaunching(app, options);
}
@@ -24,5 +29,8 @@ public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
return base.OpenUrl(app, url, options);
}
+
+ public override void PerformActionForShortcutItem(UIApplication application, UIApplicationShortcutItem shortcutItem, UIOperationHandler completionHandler)
+ => Xamarin.Essentials.Platform.PerformActionForShortcutItem(application, shortcutItem, completionHandler);
}
}
diff --git a/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/Contents.json b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/Contents.json
new file mode 100644
index 000000000..b4e8d34b5
--- /dev/null
+++ b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/Contents.json
@@ -0,0 +1,533 @@
+{
+ "images": [
+ {
+ "idiom": "universal"
+ },
+ {
+ "filename": "app_info_action_icon.png",
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "filename": "app_info_action_icon@2x.png",
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "filename": "app_info_action_icon@3x.png",
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "idiom": "iphone"
+ },
+ {
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "idiom": "ipad"
+ },
+ {
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "idiom": "watch"
+ },
+ {
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "idiom": "mac"
+ },
+ {
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "idiom": "car"
+ },
+ {
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "scale": "3x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "car"
+ }
+ ],
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ },
+ "properties": {
+ "template-rendering-intent": "template"
+ }
+}
\ No newline at end of file
diff --git a/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon.png b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon.png
new file mode 100644
index 000000000..8ad602bc9
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon.png differ
diff --git a/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@2x.png b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@2x.png
new file mode 100644
index 000000000..644de0354
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@2x.png differ
diff --git a/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@3x.png b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@3x.png
new file mode 100644
index 000000000..4d55ee2cd
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/app_info_action_icon.imageset/app_info_action_icon@3x.png differ
diff --git a/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/Contents.json b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/Contents.json
new file mode 100644
index 000000000..941417708
--- /dev/null
+++ b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/Contents.json
@@ -0,0 +1,533 @@
+{
+ "images": [
+ {
+ "idiom": "universal"
+ },
+ {
+ "filename": "battery_action_icon.png",
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "filename": "battery_action_icon@2x.png",
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "filename": "battery_action_icon@3x.png",
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "idiom": "iphone"
+ },
+ {
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "idiom": "ipad"
+ },
+ {
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "idiom": "watch"
+ },
+ {
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "idiom": "mac"
+ },
+ {
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "idiom": "car"
+ },
+ {
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "scale": "3x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "subtype": "retina4",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "iphone"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "ipad"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{130,145}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "screenWidth": "{146,165}",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "watch"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "1x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "mac"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "2x",
+ "idiom": "car"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ],
+ "scale": "3x",
+ "idiom": "car"
+ }
+ ],
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ },
+ "properties": {
+ "template-rendering-intent": "template"
+ }
+}
\ No newline at end of file
diff --git a/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon.png b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon.png
new file mode 100644
index 000000000..313d80fb2
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon.png differ
diff --git a/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@2x.png b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@2x.png
new file mode 100644
index 000000000..60dd5f27f
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@2x.png differ
diff --git a/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@3x.png b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@3x.png
new file mode 100644
index 000000000..8535fbdfc
Binary files /dev/null and b/Samples/Samples.iOS/Assets.xcassets/battery_action_icon.imageset/battery_action_icon@3x.png differ
diff --git a/Samples/Samples.iOS/Info.plist b/Samples/Samples.iOS/Info.plist
index 94697620f..06926b44c 100644
--- a/Samples/Samples.iOS/Info.plist
+++ b/Samples/Samples.iOS/Info.plist
@@ -82,5 +82,13 @@
Get Location
NSLocationAlwaysUsageDescription
Get Location
+ NSPhotoLibraryAddUsageDescription
+ Pick Photos
+ NSPhotoLibraryUsageDescription
+ Pick Photos
+ NSMicrophoneUsageDescription
+ Catpure Video
+ NSContactsUsageDescription
+ Contacts
diff --git a/Samples/Samples.iOS/Resources/app_info_action_icon@2x.png b/Samples/Samples.iOS/Resources/app_info_action_icon@2x.png
new file mode 100644
index 000000000..b3209b088
Binary files /dev/null and b/Samples/Samples.iOS/Resources/app_info_action_icon@2x.png differ
diff --git a/Samples/Samples.iOS/Resources/battery_action_icon@2x.png b/Samples/Samples.iOS/Resources/battery_action_icon@2x.png
new file mode 100644
index 000000000..554b363a1
Binary files /dev/null and b/Samples/Samples.iOS/Resources/battery_action_icon@2x.png differ
diff --git a/Samples/Samples.iOS/Samples.iOS.csproj b/Samples/Samples.iOS/Samples.iOS.csproj
index edecd23ad..3952e28b9 100644
--- a/Samples/Samples.iOS/Samples.iOS.csproj
+++ b/Samples/Samples.iOS/Samples.iOS.csproj
@@ -77,8 +77,8 @@
-
-
+
+
@@ -117,5 +117,35 @@
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/App.xaml.cs b/Samples/Samples/App.xaml.cs
index 4949d5445..c152f8573 100644
--- a/Samples/Samples/App.xaml.cs
+++ b/Samples/Samples/App.xaml.cs
@@ -1,4 +1,8 @@
-using Microsoft.AppCenter;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using Microsoft.AppCenter.Distribute;
@@ -20,13 +24,16 @@ public App()
InitializeComponent();
// Enable currently experimental features
+ Device.SetFlags(new string[] { "MediaElement_Experimental" });
VersionTracking.Track();
MainPage = new NavigationPage(new HomePage());
+
+ AppActions.OnAppAction += AppActions_OnAppAction;
}
- protected override void OnStart()
+ protected override async void OnStart()
{
if ((Device.RuntimePlatform == Device.Android && CommonConstants.AppCenterAndroid != "AC_ANDROID") ||
(Device.RuntimePlatform == Device.iOS && CommonConstants.AppCenteriOS != "AC_IOS") ||
@@ -40,6 +47,37 @@ protected override void OnStart()
typeof(Crashes),
typeof(Distribute));
}
+
+ await AppActions.SetAsync(
+ new AppAction("app_info", "App Info", icon: "app_info_action_icon"),
+ new AppAction("battery_info", "Battery Info"));
+ }
+
+ void AppActions_OnAppAction(object sender, AppActionEventArgs e)
+ {
+ // Don't handle events fired for old application instances
+ // and cleanup the old instance's event handler
+ if (Application.Current != this && Application.Current is App app)
+ {
+ AppActions.OnAppAction -= app.AppActions_OnAppAction;
+ return;
+ }
+
+ Device.BeginInvokeOnMainThread(async () =>
+ {
+ var page = e.AppAction.Id switch
+ {
+ "battery_info" => new BatteryPage(),
+ "app_info" => new AppInfoPage(),
+ _ => default(Page)
+ };
+
+ if (page != null)
+ {
+ await Application.Current.MainPage.Navigation.PopToRootAsync();
+ await Application.Current.MainPage.Navigation.PushAsync(page);
+ }
+ });
}
protected override void OnSleep()
diff --git a/Samples/Samples/Helpers/ViewHelpers.cs b/Samples/Samples/Helpers/ViewHelpers.cs
new file mode 100644
index 000000000..73fd41b19
--- /dev/null
+++ b/Samples/Samples/Helpers/ViewHelpers.cs
@@ -0,0 +1,32 @@
+using Xamarin.Forms;
+
+namespace Samples.Helpers
+{
+ public static class ViewHelpers
+ {
+ public static Rectangle GetAbsoluteBounds(this Xamarin.Forms.View element)
+ {
+ Element looper = element;
+
+ var absoluteX = element.X + element.Margin.Top;
+ var absoluteY = element.Y + element.Margin.Left;
+
+ // TODO: add logic to handle titles, headers, or other non-view bars
+
+ while (looper.Parent != null)
+ {
+ looper = looper.Parent;
+ if (looper is Xamarin.Forms.View v)
+ {
+ absoluteX += v.X + v.Margin.Top;
+ absoluteY += v.Y + v.Margin.Left;
+ }
+ }
+
+ return new Rectangle(absoluteX, absoluteY, element.Width, element.Height);
+ }
+
+ public static System.Drawing.Rectangle ToSystemRectangle(this Rectangle rect) =>
+ new System.Drawing.Rectangle((int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height);
+ }
+}
diff --git a/Samples/Samples/Model/PermissionItem.cs b/Samples/Samples/Model/PermissionItem.cs
index 34135dff7..94c592e35 100644
--- a/Samples/Samples/Model/PermissionItem.cs
+++ b/Samples/Samples/Model/PermissionItem.cs
@@ -3,12 +3,13 @@
using System.ComponentModel;
using System.Text;
using System.Windows.Input;
+using Samples.ViewModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Samples.Model
{
- public class PermissionItem : INotifyPropertyChanged
+ public class PermissionItem : ObservableObject
{
public PermissionItem(string title, Permissions.BasePermission permission)
{
@@ -19,6 +20,8 @@ public PermissionItem(string title, Permissions.BasePermission permission)
public string Title { get; set; }
+ public string Rationale { get; set; }
+
public PermissionStatus Status { get; set; }
public Permissions.BasePermission Permission { get; set; }
@@ -29,7 +32,7 @@ public PermissionItem(string title, Permissions.BasePermission permission)
try
{
Status = await Permission.CheckStatusAsync();
- NotifyPropertyChanged(nameof(Status));
+ OnPropertyChanged(nameof(Status));
}
catch (Exception ex)
{
@@ -43,7 +46,7 @@ public PermissionItem(string title, Permissions.BasePermission permission)
try
{
Status = await Permission.RequestAsync();
- NotifyPropertyChanged(nameof(Status));
+ OnPropertyChanged(nameof(Status));
}
catch (Exception ex)
{
@@ -51,9 +54,18 @@ public PermissionItem(string title, Permissions.BasePermission permission)
}
});
- public event PropertyChangedEventHandler PropertyChanged;
-
- public void NotifyPropertyChanged(string name)
- => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ public ICommand ShouldShowRationaleCommand =>
+ new Command(() =>
+ {
+ try
+ {
+ Rationale = $"Should show rationale: {Permission.ShouldShowRationale()}";
+ OnPropertyChanged(nameof(Rationale));
+ }
+ catch (Exception ex)
+ {
+ MessagingCenter.Send(this, nameof(PermissionException), ex);
+ }
+ });
}
}
diff --git a/Samples/Samples/Samples.csproj b/Samples/Samples/Samples.csproj
index fdf05730e..47a2eb636 100644
--- a/Samples/Samples/Samples.csproj
+++ b/Samples/Samples/Samples.csproj
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/Samples/Samples/View/ColorConvertersPage.xaml b/Samples/Samples/View/ColorConvertersPage.xaml
index 0ba4a38c9..86f39f1de 100644
--- a/Samples/Samples/View/ColorConvertersPage.xaml
+++ b/Samples/Samples/View/ColorConvertersPage.xaml
@@ -13,10 +13,6 @@
-
-
-
-
@@ -28,6 +24,8 @@
+
+
@@ -38,27 +36,21 @@
-
-
-
-
-
-
diff --git a/Samples/Samples/View/ContactsPage.xaml b/Samples/Samples/View/ContactsPage.xaml
new file mode 100644
index 000000000..c4e621529
--- /dev/null
+++ b/Samples/Samples/View/ContactsPage.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/ContactsPage.xaml.cs b/Samples/Samples/View/ContactsPage.xaml.cs
new file mode 100644
index 000000000..51c6ca2be
--- /dev/null
+++ b/Samples/Samples/View/ContactsPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class ContactsPage
+ {
+ public ContactsPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/FilePickerPage.xaml b/Samples/Samples/View/FilePickerPage.xaml
new file mode 100644
index 000000000..5cde8d1e5
--- /dev/null
+++ b/Samples/Samples/View/FilePickerPage.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/Samples/View/FilePickerPage.xaml.cs b/Samples/Samples/View/FilePickerPage.xaml.cs
new file mode 100644
index 000000000..b35a4af50
--- /dev/null
+++ b/Samples/Samples/View/FilePickerPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class FilePickerPage : BasePage
+ {
+ public FilePickerPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/FileSystemPage.xaml b/Samples/Samples/View/FileSystemPage.xaml
index ad5a05f42..44a80d83c 100644
--- a/Samples/Samples/View/FileSystemPage.xaml
+++ b/Samples/Samples/View/FileSystemPage.xaml
@@ -11,6 +11,11 @@
+
+
+
+
+
diff --git a/Samples/Samples/View/HapticFeedbackPage.xaml b/Samples/Samples/View/HapticFeedbackPage.xaml
new file mode 100644
index 000000000..a3898b8cd
--- /dev/null
+++ b/Samples/Samples/View/HapticFeedbackPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/Samples/View/HapticFeedbackPage.xaml.cs b/Samples/Samples/View/HapticFeedbackPage.xaml.cs
new file mode 100644
index 000000000..036e2af7b
--- /dev/null
+++ b/Samples/Samples/View/HapticFeedbackPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class HapticFeedbackPage : BasePage
+ {
+ public HapticFeedbackPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/HomePage.xaml b/Samples/Samples/View/HomePage.xaml
index e7cfa1217..2e0fa0438 100644
--- a/Samples/Samples/View/HomePage.xaml
+++ b/Samples/Samples/View/HomePage.xaml
@@ -10,9 +10,8 @@
diff --git a/Samples/Samples/View/LauncherPage.xaml.cs b/Samples/Samples/View/LauncherPage.xaml.cs
index badca1ec1..6f959602b 100644
--- a/Samples/Samples/View/LauncherPage.xaml.cs
+++ b/Samples/Samples/View/LauncherPage.xaml.cs
@@ -8,7 +8,6 @@
namespace Samples.View
{
- [XamlCompilation(XamlCompilationOptions.Compile)]
public partial class LauncherPage : BasePage
{
public LauncherPage()
diff --git a/Samples/Samples/View/MapsPage.xaml.cs b/Samples/Samples/View/MapsPage.xaml.cs
index 3da0b9cd6..0ce25a94b 100644
--- a/Samples/Samples/View/MapsPage.xaml.cs
+++ b/Samples/Samples/View/MapsPage.xaml.cs
@@ -2,7 +2,6 @@
namespace Samples.View
{
- [XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MapsPage : BasePage
{
public MapsPage()
diff --git a/Samples/Samples/View/MediaPickerPage.xaml b/Samples/Samples/View/MediaPickerPage.xaml
new file mode 100644
index 000000000..efac0d742
--- /dev/null
+++ b/Samples/Samples/View/MediaPickerPage.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/MediaPickerPage.xaml.cs b/Samples/Samples/View/MediaPickerPage.xaml.cs
new file mode 100644
index 000000000..0ce832cee
--- /dev/null
+++ b/Samples/Samples/View/MediaPickerPage.xaml.cs
@@ -0,0 +1,12 @@
+using Xamarin.Forms.Xaml;
+
+namespace Samples.View
+{
+ public partial class MediaPickerPage : BasePage
+ {
+ public MediaPickerPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/PermissionsPage.xaml b/Samples/Samples/View/PermissionsPage.xaml
index 3dad77f81..4db5b1195 100644
--- a/Samples/Samples/View/PermissionsPage.xaml
+++ b/Samples/Samples/View/PermissionsPage.xaml
@@ -15,7 +15,7 @@
-
+
@@ -23,6 +23,7 @@
+
@@ -32,9 +33,12 @@
+
-
-
+
+
+
+
diff --git a/Samples/Samples/View/ScreenshotPage.xaml b/Samples/Samples/View/ScreenshotPage.xaml
new file mode 100644
index 000000000..28e29499b
--- /dev/null
+++ b/Samples/Samples/View/ScreenshotPage.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/ScreenshotPage.xaml.cs b/Samples/Samples/View/ScreenshotPage.xaml.cs
new file mode 100644
index 000000000..de782af65
--- /dev/null
+++ b/Samples/Samples/View/ScreenshotPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class ScreenshotPage : BasePage
+ {
+ public ScreenshotPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/SharePage.xaml b/Samples/Samples/View/SharePage.xaml
index ccbc7e74d..aeee3415d 100644
--- a/Samples/Samples/View/SharePage.xaml
+++ b/Samples/Samples/View/SharePage.xaml
@@ -37,7 +37,7 @@
-
+
@@ -52,7 +52,7 @@
-
+
diff --git a/Samples/Samples/ViewModel/ClipboardViewModel.cs b/Samples/Samples/ViewModel/ClipboardViewModel.cs
index df2b7965c..732bb5f92 100644
--- a/Samples/Samples/ViewModel/ClipboardViewModel.cs
+++ b/Samples/Samples/ViewModel/ClipboardViewModel.cs
@@ -34,12 +34,24 @@ public string LastCopied
public override void OnAppearing()
{
- Clipboard.ClipboardContentChanged += OnClipboardContentChanged;
+ try
+ {
+ Clipboard.ClipboardContentChanged += OnClipboardContentChanged;
+ }
+ catch (FeatureNotSupportedException)
+ {
+ }
}
public override void OnDisappearing()
{
- Clipboard.ClipboardContentChanged -= OnClipboardContentChanged;
+ try
+ {
+ Clipboard.ClipboardContentChanged -= OnClipboardContentChanged;
+ }
+ catch (FeatureNotSupportedException)
+ {
+ }
}
void OnClipboardContentChanged(object sender, EventArgs args)
@@ -47,7 +59,10 @@ void OnClipboardContentChanged(object sender, EventArgs args)
LastCopied = $"Last copied text at {DateTime.UtcNow:T}";
}
- async void OnCopy() => await Clipboard.SetTextAsync(FieldValue);
+ async void OnCopy()
+ {
+ await Clipboard.SetTextAsync(FieldValue);
+ }
async void OnPaste()
{
diff --git a/Samples/Samples/ViewModel/ContactsViewModel.cs b/Samples/Samples/ViewModel/ContactsViewModel.cs
new file mode 100644
index 000000000..532a840f7
--- /dev/null
+++ b/Samples/Samples/ViewModel/ContactsViewModel.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ class ContactsViewModel : BaseViewModel
+ {
+ string name;
+
+ public string Name
+ {
+ get => name;
+ set => SetProperty(ref name, value);
+ }
+
+ string phones;
+
+ public string Phones
+ {
+ get => phones;
+ set => SetProperty(ref phones, value);
+ }
+
+ string emails;
+
+ public string Emails
+ {
+ get => emails;
+ set => SetProperty(ref emails, value);
+ }
+
+ string contactType;
+
+ public string ContactType
+ {
+ get => contactType;
+ set => SetProperty(ref contactType, value);
+ }
+
+ public ICommand GetContactCommand { get; }
+
+ public ContactsViewModel()
+ {
+ GetContactCommand = new Command(OnGetContact);
+ }
+
+ async void OnGetContact()
+ {
+ if (IsBusy)
+ return;
+ IsBusy = true;
+ try
+ {
+ Phones = string.Empty;
+ Emails = string.Empty;
+ Name = string.Empty;
+ ContactType = string.Empty;
+
+ var contact = await Contacts.PickContactAsync();
+ if (contact == null)
+ return;
+
+ foreach (var number in contact?.Numbers)
+ Phones += $"{number.PhoneNumber} ({number.ContactType})" + Environment.NewLine;
+
+ foreach (var email in contact?.Emails)
+ Emails += $"{email.EmailAddress} ({email.ContactType})" + Environment.NewLine;
+
+ Name = contact?.Name;
+ ContactType = contact?.ContactType.ToString();
+ }
+ catch (Exception ex)
+ {
+ MainThread.BeginInvokeOnMainThread(async () => await DisplayAlertAsync($"Error:{ex.Message}"));
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/DeviceInfoViewModel.cs b/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
index bbd0b06f6..6fab032d8 100644
--- a/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
+++ b/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
@@ -31,13 +31,9 @@ public DisplayInfo ScreenMetrics
public override void OnAppearing()
{
base.OnAppearing();
+
DeviceDisplay.MainDisplayInfoChanged += OnScreenMetricsChanged;
ScreenMetrics = DeviceDisplay.MainDisplayInfo;
-
- System.Threading.Tasks.Task.Run(() =>
- {
- var test = DeviceInfo.Idiom;
- });
}
public override void OnDisappearing()
diff --git a/Samples/Samples/ViewModel/FilePickerViewModel.cs b/Samples/Samples/ViewModel/FilePickerViewModel.cs
new file mode 100644
index 000000000..aeeea2b31
--- /dev/null
+++ b/Samples/Samples/ViewModel/FilePickerViewModel.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public class FilePickerViewModel : BaseViewModel
+ {
+ string text;
+ ImageSource image;
+ bool isImageVisible;
+
+ public FilePickerViewModel()
+ {
+ PickFileCommand = new Command(() => DoPickFile());
+ PickImageCommand = new Command(() => DoPickImage());
+ PickCustomTypeCommand = new Command(() => DoPickCustomType());
+ PickAndSendCommand = new Command(() => DoPickAndSend());
+ PickMultipleFilesCommand = new Command(() => DoPickMultipleFiles());
+ }
+
+ public ICommand PickFileCommand { get; }
+
+ public ICommand PickImageCommand { get; }
+
+ public ICommand PickCustomTypeCommand { get; }
+
+ public ICommand PickAndSendCommand { get; }
+
+ public ICommand PickMultipleFilesCommand { get; }
+
+ public string Text
+ {
+ get => text;
+ set => SetProperty(ref text, value);
+ }
+
+ public ImageSource Image
+ {
+ get => image;
+ set => SetProperty(ref image, value);
+ }
+
+ public bool IsImageVisible
+ {
+ get => isImageVisible;
+ set => SetProperty(ref isImageVisible, value);
+ }
+
+ async void DoPickFile()
+ {
+ await PickAndShow(PickOptions.Default);
+ }
+
+ async void DoPickImage()
+ {
+ var options = new PickOptions
+ {
+ PickerTitle = "Please select an image",
+ FileTypes = FilePickerFileType.Images,
+ };
+
+ await PickAndShow(options);
+ }
+
+ async void DoPickCustomType()
+ {
+ var customFileType =
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.iOS, new[] { "public.my.comic.extension" } }, // or general UTType values
+ { DevicePlatform.Android, new[] { "application/comics" } },
+ { DevicePlatform.UWP, new[] { ".cbr", ".cbz" } },
+ { DevicePlatform.Tizen, new[] { "*/*" } },
+ { DevicePlatform.macOS, new[] { "cbr", "cbz" } }, // or general UTType values
+ });
+
+ var options = new PickOptions
+ {
+ PickerTitle = "Please select a comic file",
+ FileTypes = customFileType,
+ };
+
+ await PickAndShow(options);
+ }
+
+ async void DoPickAndSend()
+ {
+ // pick a file
+ var result = await PickAndShow(PickOptions.Images);
+ if (result == null)
+ return;
+
+ // copy it locally
+ var copyPath = Path.Combine(FileSystem.CacheDirectory, result.FileName);
+ using (var destination = File.Create(copyPath))
+ using (var source = await result.OpenReadAsync())
+ await source.CopyToAsync(destination);
+
+ // send it via an email
+ await Email.ComposeAsync(new EmailMessage
+ {
+ Subject = "Test Subject",
+ Body = "This is the body. There should be an image attached.",
+ Attachments =
+ {
+ new EmailAttachment(copyPath)
+ }
+ });
+ }
+
+ async Task PickAndShow(PickOptions options)
+ {
+ try
+ {
+ var result = await FilePicker.PickAsync(options);
+
+ if (result != null)
+ {
+ Text = $"File Name: {result.FileName}";
+
+ if (result.FileName.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ||
+ result.FileName.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+ {
+ var stream = await result.OpenReadAsync();
+ Image = ImageSource.FromStream(() => stream);
+ IsImageVisible = true;
+ }
+ else
+ {
+ IsImageVisible = false;
+ }
+ }
+ else
+ {
+ Text = $"Pick cancelled.";
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ Text = ex.ToString();
+ IsImageVisible = false;
+ return null;
+ }
+ }
+
+ async void DoPickMultipleFiles()
+ {
+ try
+ {
+ var resultList = await FilePicker.PickMultipleAsync();
+
+ if (resultList != null && resultList.Any())
+ {
+ Text = "File Names: " + string.Join(", ", resultList.Select(result => result.FileName));
+
+ // only showing the first file's content
+ var firstResult = resultList.First();
+
+ if (firstResult.FileName.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ||
+ firstResult.FileName.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+ {
+ var stream = await firstResult.OpenReadAsync();
+ Image = ImageSource.FromStream(() => stream);
+ IsImageVisible = true;
+ }
+ else
+ {
+ IsImageVisible = false;
+ }
+ }
+ else
+ {
+ Text = $"Pick cancelled.";
+ }
+ }
+ catch (Exception ex)
+ {
+ Text = ex.ToString();
+ IsImageVisible = false;
+ }
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/FileSystemViewModel.cs b/Samples/Samples/ViewModel/FileSystemViewModel.cs
index d622c7fe7..9c779cce2 100644
--- a/Samples/Samples/ViewModel/FileSystemViewModel.cs
+++ b/Samples/Samples/ViewModel/FileSystemViewModel.cs
@@ -29,6 +29,10 @@ public FileSystemViewModel()
public ICommand DeleteFileCommand { get; }
+ public string AppDataDirectory => FileSystem.AppDataDirectory;
+
+ public string CacheDirectory => FileSystem.CacheDirectory;
+
public string CurrentContents
{
get => currentContents;
diff --git a/Samples/Samples/ViewModel/GeocodingViewModel.cs b/Samples/Samples/ViewModel/GeocodingViewModel.cs
index 5f6c156e4..4db043f1f 100644
--- a/Samples/Samples/ViewModel/GeocodingViewModel.cs
+++ b/Samples/Samples/ViewModel/GeocodingViewModel.cs
@@ -8,8 +8,8 @@ namespace Samples.ViewModel
{
public class GeocodingViewModel : BaseViewModel
{
- string lat = "47.673988";
- string lon = "-122.121513";
+ string lat = 47.67398.ToString();
+ string lon = (-122.121513).ToString();
string address = "Microsoft Building 25 Redmond WA USA";
string geocodeAddress;
string geocodePosition;
diff --git a/Samples/Samples/ViewModel/GeolocationViewModel.cs b/Samples/Samples/ViewModel/GeolocationViewModel.cs
index 8846a87c5..979d54802 100644
--- a/Samples/Samples/ViewModel/GeolocationViewModel.cs
+++ b/Samples/Samples/ViewModel/GeolocationViewModel.cs
@@ -100,6 +100,7 @@ string FormatLocation(Location location, Exception ex = null)
$"Longitude: {location.Longitude}\n" +
$"HorizontalAccuracy: {location.Accuracy}\n" +
$"Altitude: {(location.Altitude.HasValue ? location.Altitude.Value.ToString() : notAvailable)}\n" +
+ $"AltitudeRefSys: {location.AltitudeReferenceSystem.ToString()}\n" +
$"VerticalAccuracy: {(location.VerticalAccuracy.HasValue ? location.VerticalAccuracy.Value.ToString() : notAvailable)}\n" +
$"Heading: {(location.Course.HasValue ? location.Course.Value.ToString() : notAvailable)}\n" +
$"Speed: {(location.Speed.HasValue ? location.Speed.Value.ToString() : notAvailable)}\n" +
diff --git a/Samples/Samples/ViewModel/HapticFeedbackViewModel.cs b/Samples/Samples/ViewModel/HapticFeedbackViewModel.cs
new file mode 100644
index 000000000..65fe1fbf2
--- /dev/null
+++ b/Samples/Samples/ViewModel/HapticFeedbackViewModel.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public class HapticFeedbackViewModel : BaseViewModel
+ {
+ bool isSupported = true;
+
+ public HapticFeedbackViewModel()
+ {
+ ClickCommand = new Command(OnClick);
+ LongPressCommand = new Command(OnLongPress);
+ }
+
+ public ICommand ClickCommand { get; }
+
+ public ICommand LongPressCommand { get; }
+
+ public bool IsSupported
+ {
+ get => isSupported;
+ set => SetProperty(ref isSupported, value);
+ }
+
+ void OnClick()
+ {
+ try
+ {
+ HapticFeedback.Perform(HapticFeedbackType.Click);
+ }
+ catch (FeatureNotSupportedException)
+ {
+ IsSupported = false;
+ }
+ catch (Exception ex)
+ {
+ DisplayAlertAsync($"Unable to HapticFeedback: {ex.Message}");
+ }
+ }
+
+ void OnLongPress()
+ {
+ try
+ {
+ HapticFeedback.Perform(HapticFeedbackType.LongPress);
+ }
+ catch (FeatureNotSupportedException)
+ {
+ IsSupported = false;
+ }
+ catch (Exception ex)
+ {
+ DisplayAlertAsync($"Unable to HapticFeedback: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/HomeViewModel.cs b/Samples/Samples/ViewModel/HomeViewModel.cs
index 27fe1269f..b2ca693f3 100644
--- a/Samples/Samples/ViewModel/HomeViewModel.cs
+++ b/Samples/Samples/ViewModel/HomeViewModel.cs
@@ -78,6 +78,12 @@ public HomeViewModel()
typeof(ConnectivityPage),
"Check connectivity state and detect changes.",
new[] { "connectivity", "internet", "wifi" }),
+ new SampleItem(
+ "👶",
+ "Contacts",
+ typeof(ContactsPage),
+ "Get and add contacts in your device.",
+ new[] { "contacts", "people", "device" }),
new SampleItem(
"📱",
"Device Info",
@@ -90,6 +96,12 @@ public HomeViewModel()
typeof(EmailPage),
"Easily send email messages.",
new[] { "email", "share", "communication", "message" }),
+ new SampleItem(
+ "📁",
+ "File Picker",
+ typeof(FilePickerPage),
+ "Easily pick files from storage.",
+ new[] { "files", "picking", "filesystem", "storage" }),
new SampleItem(
"📁",
"File System",
@@ -150,6 +162,12 @@ public HomeViewModel()
typeof(OrientationSensorPage),
"Retrieve orientation of the device in 3D space.",
new[] { "orientation", "sensors", "hardware", "device" }),
+ new SampleItem(
+ "📷",
+ "Media Picker",
+ typeof(MediaPickerPage),
+ "Pick or capture a photo or video.",
+ new[] { "media", "picker", "video", "picture", "photo", "image", "movie" }),
new SampleItem(
"🔒",
"Permissions",
@@ -168,6 +186,12 @@ public HomeViewModel()
typeof(PreferencesPage),
"Quickly and easily add persistent preferences.",
new[] { "settings", "preferences", "prefs", "storage" }),
+ new SampleItem(
+ "📷",
+ "Screenshot",
+ typeof(ScreenshotPage),
+ "Quickly and easily take screenshots of your app.",
+ new[] { "screenshot", "picture", "media", "display" }),
new SampleItem(
"🔒",
"Secure Storage",
@@ -204,6 +228,12 @@ public HomeViewModel()
typeof(VibrationPage),
"Quickly and easily make the device vibrate.",
new[] { "vibration", "vibrate", "hardware", "device" }),
+ new SampleItem(
+ "📳",
+ "Haptic Feedback",
+ typeof(HapticFeedbackPage),
+ "Quickly and easily make the device provide haptic feedback",
+ new[] { "haptic", "feedback", "hardware", "device" }),
new SampleItem(
"🔓",
"Web Authenticator",
diff --git a/Samples/Samples/ViewModel/MapsViewModel.cs b/Samples/Samples/ViewModel/MapsViewModel.cs
index c5f252e25..4b972ea36 100644
--- a/Samples/Samples/ViewModel/MapsViewModel.cs
+++ b/Samples/Samples/ViewModel/MapsViewModel.cs
@@ -16,7 +16,7 @@ public string Name
set => SetProperty(ref name, value);
}
- string longitude = "-122.130603";
+ string longitude = (-122.130603).ToString();
public string Longitude
{
@@ -24,7 +24,7 @@ public string Longitude
set => SetProperty(ref longitude, value);
}
- string latitude = "47.645160";
+ string latitude = 47.645160.ToString();
public string Latitude
{
diff --git a/Samples/Samples/ViewModel/MediaPickerViewModel.cs b/Samples/Samples/ViewModel/MediaPickerViewModel.cs
new file mode 100644
index 000000000..3d07f5154
--- /dev/null
+++ b/Samples/Samples/ViewModel/MediaPickerViewModel.cs
@@ -0,0 +1,175 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public class MediaPickerViewModel : BaseViewModel
+ {
+ string photoPath;
+ string videoPath;
+
+ bool showPhoto;
+ bool showVideo;
+
+ public MediaPickerViewModel()
+ {
+ PickPhotoCommand = new Command(DoPickPhoto);
+ CapturePhotoCommand = new Command(DoCapturePhoto, () => MediaPicker.IsCaptureSupported);
+
+ PickVideoCommand = new Command(DoPickVideo);
+ CaptureVideoCommand = new Command(DoCaptureVideo, () => MediaPicker.IsCaptureSupported);
+ }
+
+ public ICommand PickPhotoCommand { get; }
+
+ public ICommand CapturePhotoCommand { get; }
+
+ public ICommand PickVideoCommand { get; }
+
+ public ICommand CaptureVideoCommand { get; }
+
+ public bool ShowPhoto
+ {
+ get => showPhoto;
+ set => SetProperty(ref showPhoto, value);
+ }
+
+ public bool ShowVideo
+ {
+ get => showVideo;
+ set => SetProperty(ref showVideo, value);
+ }
+
+ public string PhotoPath
+ {
+ get => photoPath;
+ set => SetProperty(ref photoPath, value);
+ }
+
+ public string VideoPath
+ {
+ get => videoPath;
+ set => SetProperty(ref videoPath, value);
+ }
+
+ async void DoPickPhoto()
+ {
+ try
+ {
+ var photo = await MediaPicker.PickPhotoAsync();
+
+ await LoadPhotoAsync(photo);
+
+ Console.WriteLine($"PickPhotoAsync COMPLETED: {PhotoPath}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"PickPhotoAsync THREW: {ex.Message}");
+ }
+ }
+
+ async void DoCapturePhoto()
+ {
+ try
+ {
+ var photo = await MediaPicker.CapturePhotoAsync();
+
+ await LoadPhotoAsync(photo);
+
+ Console.WriteLine($"CapturePhotoAsync COMPLETED: {PhotoPath}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"CapturePhotoAsync THREW: {ex.Message}");
+ }
+ }
+
+ async void DoPickVideo()
+ {
+ try
+ {
+ var video = await MediaPicker.PickVideoAsync();
+
+ await LoadVideoAsync(video);
+
+ Console.WriteLine($"PickVideoAsync COMPLETED: {PhotoPath}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"PickVideoAsync THREW: {ex.Message}");
+ }
+ }
+
+ async void DoCaptureVideo()
+ {
+ try
+ {
+ var photo = await MediaPicker.CaptureVideoAsync();
+
+ await LoadVideoAsync(photo);
+
+ Console.WriteLine($"CaptureVideoAsync COMPLETED: {PhotoPath}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"CaptureVideoAsync THREW: {ex.Message}");
+ }
+ }
+
+ async Task LoadPhotoAsync(FileResult photo)
+ {
+ // canceled
+ if (photo == null)
+ {
+ PhotoPath = null;
+ return;
+ }
+
+ // save the file into local storage
+ var newFile = Path.Combine(FileSystem.CacheDirectory, photo.FileName);
+ using (var stream = await photo.OpenReadAsync())
+ using (var newStream = File.OpenWrite(newFile))
+ {
+ await stream.CopyToAsync(newStream);
+ }
+
+ PhotoPath = newFile;
+ ShowVideo = false;
+ ShowPhoto = true;
+ }
+
+ async Task LoadVideoAsync(FileResult video)
+ {
+ // canceled
+ if (video == null)
+ {
+ VideoPath = null;
+ return;
+ }
+
+ // save the file into local storage
+ var newFile = Path.Combine(FileSystem.CacheDirectory, video.FileName);
+ using (var stream = await video.OpenReadAsync())
+ using (var newStream = File.OpenWrite(newFile))
+ {
+ await stream.CopyToAsync(newStream);
+ }
+
+ VideoPath = newFile;
+ ShowVideo = true;
+ ShowPhoto = false;
+ }
+
+ public override void OnDisappearing()
+ {
+ PhotoPath = null;
+ VideoPath = null;
+
+ base.OnDisappearing();
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/ScreenshotViewModel.cs b/Samples/Samples/ViewModel/ScreenshotViewModel.cs
new file mode 100644
index 000000000..5f5c86b86
--- /dev/null
+++ b/Samples/Samples/ViewModel/ScreenshotViewModel.cs
@@ -0,0 +1,54 @@
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ class ScreenshotViewModel : BaseViewModel
+ {
+ ImageSource screenshot;
+
+ public ScreenshotViewModel()
+ {
+ ScreenshotCommand = new Command(async () => await CaptureScreenshot(), () => Screenshot.IsCaptureSupported);
+ EmailCommand = new Command(async () => await EmailScreenshot(), () => Screenshot.IsCaptureSupported);
+ }
+
+ public ICommand ScreenshotCommand { get; }
+
+ public ICommand EmailCommand { get; }
+
+ public ImageSource Image
+ {
+ get => screenshot;
+ set => SetProperty(ref screenshot, value);
+ }
+
+ async Task CaptureScreenshot()
+ {
+ var mediaFile = await Screenshot.CaptureAsync();
+ var stream = await mediaFile.OpenReadAsync(ScreenshotFormat.Png);
+
+ Image = ImageSource.FromStream(() => stream);
+ }
+
+ async Task EmailScreenshot()
+ {
+ var mediaFile = await Screenshot.CaptureAsync();
+
+ var temp = Path.Combine(FileSystem.CacheDirectory, "screenshot.jpg");
+ using (var stream = await mediaFile.OpenReadAsync(ScreenshotFormat.Jpeg))
+ using (var file = File.Create(temp))
+ {
+ await stream.CopyToAsync(file);
+ }
+
+ await Email.ComposeAsync(new EmailMessage
+ {
+ Attachments = { new EmailAttachment(temp) }
+ });
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/ShareViewModel.cs b/Samples/Samples/ViewModel/ShareViewModel.cs
index 98bcd951c..9c269d62f 100644
--- a/Samples/Samples/ViewModel/ShareViewModel.cs
+++ b/Samples/Samples/ViewModel/ShareViewModel.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Windows.Input;
+using Samples.Helpers;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -23,8 +24,8 @@ class ShareViewModel : BaseViewModel
public ShareViewModel()
{
- RequestCommand = new Command(OnRequest);
- RequestFileCommand = new Command(OnFileRequest);
+ RequestCommand = new Command(OnRequest);
+ RequestFileCommand = new Command(OnFileRequest);
}
public bool ShareText
@@ -81,35 +82,40 @@ public string ShareFileAttachmentName
set => SetProperty(ref shareFileAttachmentName, value);
}
- async void OnRequest()
+ async void OnRequest(Xamarin.Forms.View element)
{
+ var bounds = element.GetAbsoluteBounds();
+
await Share.RequestAsync(new ShareTextRequest
{
Subject = Subject,
Text = ShareText ? Text : null,
Uri = ShareUri ? Uri : null,
- Title = Title
+ Title = Title,
+ PresentationSourceBounds = bounds.ToSystemRectangle()
});
}
- async void OnFileRequest()
+ async void OnFileRequest(Xamarin.Forms.View element)
{
- if (!string.IsNullOrWhiteSpace(ShareFileAttachmentContents))
+ if (string.IsNullOrWhiteSpace(ShareFileAttachmentContents))
+ return;
+
+ // create a temprary file
+ var fn = string.IsNullOrWhiteSpace(ShareFileAttachmentName)
+ ? "Attachment.txt"
+ : ShareFileAttachmentName.Trim();
+ var file = Path.Combine(FileSystem.CacheDirectory, fn);
+ File.WriteAllText(file, ShareFileAttachmentContents);
+
+ var bounds = element.GetAbsoluteBounds();
+
+ await Share.RequestAsync(new ShareFileRequest
{
- // create a temprary file
- var fn = string.IsNullOrWhiteSpace(ShareFileAttachmentName) ? "Attachment.txt" : ShareFileAttachmentName.Trim();
- var file = Path.Combine(FileSystem.CacheDirectory, fn);
- File.WriteAllText(file, ShareFileAttachmentContents);
-
- await Share.RequestAsync(new ShareFileRequest
- {
- Title = Title,
- File = new ShareFile(file),
- PresentationSourceBounds = Device.RuntimePlatform == Device.iOS && Device.Idiom == TargetIdiom.Tablet
- ? new System.Drawing.Rectangle(0, 20, 0, 0)
- : System.Drawing.Rectangle.Empty
- });
- }
+ Title = Title,
+ File = new ShareFile(file),
+ PresentationSourceBounds = bounds.ToSystemRectangle()
+ });
}
}
}
diff --git a/Tests/AppActions_Tests.cs b/Tests/AppActions_Tests.cs
new file mode 100644
index 000000000..c5933193b
--- /dev/null
+++ b/Tests/AppActions_Tests.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Xamarin.Essentials;
+using Xunit;
+
+namespace Tests
+{
+ public class AppActions_Tests
+ {
+ [Fact]
+ public void AppActions_SetActions() =>
+ Assert.ThrowsAsync(() => AppActions.SetAsync(new List()));
+
+ [Fact]
+ public void AppActions_GetActions() =>
+ Assert.ThrowsAsync(() => AppActions.GetAsync());
+
+ [Fact]
+ public void AppActions_IsSupported() =>
+ Assert.Throws(() => AppActions.IsSupported);
+ }
+}
diff --git a/Tests/Color_Tests.cs b/Tests/Color_Tests.cs
index edc45a674..f7f426b74 100644
--- a/Tests/Color_Tests.cs
+++ b/Tests/Color_Tests.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
using System.Drawing;
-using System.Text;
using Xamarin.Essentials;
using Xunit;
@@ -138,5 +136,42 @@ public void WithLuminosity()
Assert.Equal(29, color.G);
Assert.Equal(43, color.B);
}
+
+ [Theory]
+ [InlineData("black", 0, 0, 0)]
+ [InlineData("red", 0, 100, 100)]
+ [InlineData("white", 0, 0, 100)]
+ [InlineData("green", 120, 100, 50.2)]
+ [InlineData("lime", 120, 100, 100)]
+ [InlineData("yellow", 60, 100, 100)]
+ [InlineData("magenta", 300, 100, 100)]
+ [InlineData("cyan", 180, 100, 100)]
+ public void RgbToHsv(string name, double hTest, double sTest, double vTest)
+ {
+ var color = Color.FromName(name);
+ var (h, s, v) = color.ToHsv();
+ Assert.Equal(hTest, h);
+ Assert.Equal(sTest, s);
+ Assert.Equal(vTest, Math.Round(v, 1));
+ }
+
+ [Theory]
+ [InlineData("black", 0, 0, 0)]
+ [InlineData("red", 0, 100, 100)]
+ [InlineData("white", 0, 0, 100)]
+ [InlineData("green", 120, 100, 50.2)]
+ [InlineData("lime", 120, 100, 100)]
+ [InlineData("yellow", 60, 100, 100)]
+ [InlineData("magenta", 300, 100, 100)]
+ [InlineData("cyan", 180, 100, 100)]
+ public void HsvToRgba(string name, double hTest, double sTest, double vTest)
+ {
+ var colorExpected = Color.FromName(name);
+ var color = ColorExtensions.FromHsva(hTest, sTest, vTest, 50);
+ Assert.Equal(colorExpected.R, color.R);
+ Assert.Equal(colorExpected.G, color.G);
+ Assert.Equal(colorExpected.B, color.B);
+ Assert.Equal(50, color.A);
+ }
}
}
diff --git a/Tests/FilePicker_Tests.cs b/Tests/FilePicker_Tests.cs
new file mode 100644
index 000000000..77fbf1a90
--- /dev/null
+++ b/Tests/FilePicker_Tests.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using Xamarin.Essentials;
+using Xunit;
+
+namespace Tests
+{
+ public class FilePicker_Tests
+ {
+ [Fact]
+ public async Task PickAsync_Fail_On_NetStandard()
+ {
+ await Assert.ThrowsAsync(() => FilePicker.PickAsync());
+ }
+
+ [Fact]
+ public async Task PickMultipleAsync_Fail_On_NetStandard()
+ {
+ await Assert.ThrowsAsync(() => FilePicker.PickMultipleAsync());
+ }
+ }
+}
diff --git a/Tests/Launcher_Tests.cs b/Tests/Launcher_Tests.cs
index 975607be2..55071df77 100644
--- a/Tests/Launcher_Tests.cs
+++ b/Tests/Launcher_Tests.cs
@@ -25,6 +25,6 @@ public async Task Open_Uri_NetStandard() =>
[Fact]
public async Task Open_File_NetStandard() =>
- await Assert.ThrowsAsync(() => Launcher.OpenAsync(new OpenFileRequest()));
+ await Assert.ThrowsAsync(() => Launcher.OpenAsync(new OpenFileRequest()));
}
}
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 06123ac5a..4b89bfe8f 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -1,7 +1,7 @@
- netcoreapp2.1
+ netcoreapp3.1
false
XamarinEssentialsTests
portable
diff --git a/Xamarin.Essentials.sln b/Xamarin.Essentials.sln
index 3c085db48..54bdf185e 100644
--- a/Xamarin.Essentials.sln
+++ b/Xamarin.Essentials.sln
@@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29505.209
MinimumVisualStudioVersion = 15.0.26124.0
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Essentials", "Xamarin.Essentials\Xamarin.Essentials.csproj", "{CD6D6AE6-83A1-41B1-BD7C-C555A77C288B}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Essentials", "Xamarin.Essentials\Xamarin.Essentials.csproj", "{CD6D6AE6-83A1-41B1-BD7C-C555A77C288B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples\Samples.csproj", "{B4227123-2EEB-494A-A221-C061B5659AED}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples\Samples.csproj", "{B4227123-2EEB-494A-A221-C061B5659AED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.Android", "Samples\Samples.Android\Samples.Android.csproj", "{3C0CDEF3-495E-45F4-8B12-0E05EF11295C}"
EndProject
@@ -29,7 +29,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{6330
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{EE4495FA-9869-45CF-A11D-69F2218C6F62}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Server.WebAuthenticator", "Samples\Sample.Server.WebAuthenticator\Sample.Server.WebAuthenticator.csproj", "{553D51A8-8E79-40D9-9FB3-9FC2386FF886}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Server.WebAuthenticator", "Samples\Sample.Server.WebAuthenticator\Sample.Server.WebAuthenticator.csproj", "{553D51A8-8E79-40D9-9FB3-9FC2386FF886}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.Mac", "Samples\Samples.Mac\Samples.Mac.csproj", "{89899D16-4BD1-49B1-9903-9F6BB26C5DC5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -465,6 +467,34 @@ Global
{553D51A8-8E79-40D9-9FB3-9FC2386FF886}.Release|x64.Build.0 = Release|Any CPU
{553D51A8-8E79-40D9-9FB3-9FC2386FF886}.Release|x86.ActiveCfg = Release|Any CPU
{553D51A8-8E79-40D9-9FB3-9FC2386FF886}.Release|x86.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|ARM.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|x64.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Debug|x86.Build.0 = Debug|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|ARM.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|ARM.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|ARM64.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|iPhone.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|x64.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|x64.Build.0 = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|x86.ActiveCfg = Release|Any CPU
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -481,6 +511,7 @@ Global
{EE8FC716-27FC-405B-BD27-AF17E01A6671} = {EE4495FA-9869-45CF-A11D-69F2218C6F62}
{4BD0D88F-7E7A-4C3B-9E34-BF3717A8FF4B} = {EE4495FA-9869-45CF-A11D-69F2218C6F62}
{553D51A8-8E79-40D9-9FB3-9FC2386FF886} = {6330A0D0-E784-42A6-B975-451E609B907B}
+ {89899D16-4BD1-49B1-9903-9F6BB26C5DC5} = {6330A0D0-E784-42A6-B975-451E609B907B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E012047E-6826-4037-8D1A-5606CD7D345D}
diff --git a/Xamarin.Essentials/Accelerometer/Accelerometer.netstandard.tvos.cs b/Xamarin.Essentials/Accelerometer/Accelerometer.netstandard.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Accelerometer/Accelerometer.netstandard.tvos.cs
rename to Xamarin.Essentials/Accelerometer/Accelerometer.netstandard.tvos.macos.cs
diff --git a/Xamarin.Essentials/AppActions/AppActions.android.cs b/Xamarin.Essentials/AppActions/AppActions.android.cs
new file mode 100644
index 000000000..1c4c78361
--- /dev/null
+++ b/Xamarin.Essentials/AppActions/AppActions.android.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Android.Content;
+using Android.Content.PM;
+using Android.Graphics.Drawables;
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppActions
+ {
+ internal static bool PlatformIsSupported
+ => Platform.HasApiLevelNMr1;
+
+ static Task> PlatformGetAsync()
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+#if __ANDROID_25__
+ return Task.FromResult(Platform.ShortcutManager.DynamicShortcuts.Select(s => s.ToAppAction()));
+#else
+ return Task.FromResult>>(null);
+#endif
+ }
+
+ static Task PlatformSetAsync(IEnumerable actions)
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+#if __ANDROID_25__
+ Platform.ShortcutManager.SetDynamicShortcuts(actions.Select(a => a.ToShortcutInfo()).ToList());
+#endif
+ return Task.CompletedTask;
+ }
+
+ static AppAction ToAppAction(this ShortcutInfo shortcutInfo) =>
+ new AppAction(shortcutInfo.Id, shortcutInfo.ShortLabel, shortcutInfo.LongLabel);
+
+ const string extraAppActionId = "EXTRA_XE_APP_ACTION_ID";
+ const string extraAppActionTitle = "EXTRA_XE_APP_ACTION_TITLE";
+ const string extraAppActionSubtitle = "EXTRA_XE_APP_ACTION_SUBTITLE";
+ const string extraAppActionIcon = "EXTRA_XE_APP_ACTION_ICON";
+
+ internal static AppAction ToAppAction(this Intent intent)
+ => new AppAction(
+ intent.GetStringExtra(extraAppActionId),
+ intent.GetStringExtra(extraAppActionTitle),
+ intent.GetStringExtra(extraAppActionSubtitle),
+ intent.GetStringExtra(extraAppActionIcon));
+
+ static ShortcutInfo ToShortcutInfo(this AppAction action)
+ {
+ var shortcut = new ShortcutInfo.Builder(Platform.AppContext, action.Id)
+ .SetShortLabel(action.Title);
+
+ if (!string.IsNullOrWhiteSpace(action.Subtitle))
+ {
+ shortcut.SetLongLabel(action.Subtitle);
+ }
+
+ if (!string.IsNullOrWhiteSpace(action.Icon))
+ {
+ var iconResId = Platform.AppContext.Resources.GetIdentifier(action.Icon, "drawable", Platform.AppContext.PackageName);
+
+ shortcut.SetIcon(Icon.CreateWithResource(Platform.AppContext, iconResId));
+ }
+
+ var intent = new Intent(Platform.Intent.ActionAppAction);
+ intent.SetPackage(Platform.AppContext.PackageName);
+ intent.SetFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
+ intent.PutExtra(extraAppActionId, action.Id);
+ intent.PutExtra(extraAppActionTitle, action.Title);
+ intent.PutExtra(extraAppActionSubtitle, action.Subtitle);
+ intent.PutExtra(extraAppActionIcon, action.Icon);
+
+ shortcut.SetIntent(intent);
+
+ return shortcut.Build();
+ }
+ }
+}
diff --git a/Xamarin.Essentials/AppActions/AppActions.ios.cs b/Xamarin.Essentials/AppActions/AppActions.ios.cs
new file mode 100644
index 000000000..7f2aefb50
--- /dev/null
+++ b/Xamarin.Essentials/AppActions/AppActions.ios.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppActions
+ {
+ internal const string Type = "XE_APP_ACTION_TYPE";
+
+ internal static bool PlatformIsSupported
+ => Platform.HasOSVersion(9, 0);
+
+ static Task> PlatformGetAsync()
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+ return Task.FromResult(UIApplication.SharedApplication.ShortcutItems.Select(s => s.ToAppAction()));
+ }
+
+ static Task PlatformSetAsync(IEnumerable actions)
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+ UIApplication.SharedApplication.ShortcutItems = actions.Select(a => a.ToShortcutItem()).ToArray();
+
+ return Task.CompletedTask;
+ }
+
+ internal static AppAction ToAppAction(this UIApplicationShortcutItem shortcutItem)
+ {
+ string id = null;
+ if (shortcutItem.UserInfo.TryGetValue((NSString)"id", out var idObj))
+ id = idObj?.ToString();
+
+ string icon = null;
+ if (shortcutItem.UserInfo.TryGetValue((NSString)"icon", out var iconObj))
+ icon = iconObj?.ToString();
+
+ return new AppAction(id, shortcutItem.LocalizedTitle, shortcutItem.LocalizedSubtitle, icon);
+ }
+
+ static UIApplicationShortcutItem ToShortcutItem(this AppAction action)
+ {
+ var keys = new List();
+ var values = new List();
+
+ // id
+ keys.Add((NSString)"id");
+ values.Add((NSString)action.Id);
+
+ // icon
+ if (!string.IsNullOrEmpty(action.Icon))
+ {
+ keys.Add((NSString)"icon");
+ values.Add((NSString)action.Icon);
+ }
+
+ return new UIApplicationShortcutItem(
+ AppActions.Type,
+ action.Title,
+ action.Subtitle,
+ action.Icon != null ? UIApplicationShortcutIcon.FromTemplateImageName(action.Icon) : null,
+ new NSDictionary(keys.ToArray(), values.ToArray()));
+ }
+ }
+}
diff --git a/Xamarin.Essentials/AppActions/AppActions.netstandard.tvos.watchos.macos.tizen.cs b/Xamarin.Essentials/AppActions/AppActions.netstandard.tvos.watchos.macos.tizen.cs
new file mode 100644
index 000000000..23f3490d6
--- /dev/null
+++ b/Xamarin.Essentials/AppActions/AppActions.netstandard.tvos.watchos.macos.tizen.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppActions
+ {
+ internal static bool PlatformIsSupported
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task> PlatformGetAsync() =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformSetAsync(IEnumerable actions) =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+}
diff --git a/Xamarin.Essentials/AppActions/AppActions.shared.cs b/Xamarin.Essentials/AppActions/AppActions.shared.cs
new file mode 100644
index 000000000..44aad7d90
--- /dev/null
+++ b/Xamarin.Essentials/AppActions/AppActions.shared.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppActions
+ {
+ internal static bool IsSupported
+ => PlatformIsSupported;
+
+ public static Task> GetAsync()
+ => PlatformGetAsync();
+
+ public static Task SetAsync(params AppAction[] actions)
+ => PlatformSetAsync(actions);
+
+ public static Task SetAsync(IEnumerable actions)
+ => PlatformSetAsync(actions);
+
+ public static event EventHandler OnAppAction;
+
+ internal static void InvokeOnAppAction(object sender, AppAction appAction)
+ => OnAppAction?.Invoke(sender, new AppActionEventArgs(appAction));
+ }
+
+ public class AppActionEventArgs : EventArgs
+ {
+ public AppActionEventArgs(AppAction appAction)
+ : base() => AppAction = appAction;
+
+ public AppAction AppAction { get; }
+ }
+
+ public class AppAction
+ {
+ public AppAction(string id, string title, string subtitle = null, string icon = null)
+ {
+ Id = id ?? throw new ArgumentNullException(nameof(id));
+ Title = title ?? throw new ArgumentNullException(nameof(title));
+
+ Subtitle = subtitle;
+ Icon = icon;
+ }
+
+ public string Title { get; set; }
+
+ public string Subtitle { get; set; }
+
+ public string Id { get; set; }
+
+ internal string Icon { get; set; }
+ }
+}
diff --git a/Xamarin.Essentials/AppActions/AppActions.uwp.cs b/Xamarin.Essentials/AppActions/AppActions.uwp.cs
new file mode 100644
index 000000000..832bd4c05
--- /dev/null
+++ b/Xamarin.Essentials/AppActions/AppActions.uwp.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Windows.ApplicationModel.Activation;
+using Windows.UI.StartScreen;
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppActions
+ {
+ const string appActionPrefix = "XE_APP_ACTIONS-";
+
+ public static string IconDirectory { get; set; } = "Assets";
+
+ public static string IconExtension { get; set; } = "png";
+
+ internal static bool PlatformIsSupported
+ => true;
+
+ internal static async Task OnLaunched(LaunchActivatedEventArgs e)
+ {
+ if (e?.Arguments?.StartsWith(appActionPrefix) ?? false)
+ {
+ var id = ArgumentsToId(e.Arguments);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ var actions = await PlatformGetAsync();
+ var appAction = actions.FirstOrDefault(a => a.Id == id);
+
+ if (appAction != null)
+ AppActions.InvokeOnAppAction(null, appAction);
+ }
+ }
+ }
+
+ static async Task> PlatformGetAsync()
+ {
+ // Load existing items
+ var jumpList = await JumpList.LoadCurrentAsync();
+
+ var actions = new List();
+ foreach (var item in jumpList.Items)
+ actions.Add(item.ToAction());
+
+ return actions;
+ }
+
+ static async Task PlatformSetAsync(IEnumerable actions)
+ {
+ // Load existing items
+ var jumpList = await JumpList.LoadCurrentAsync();
+
+ // Set as custom, not system or frequent
+ jumpList.SystemGroupKind = JumpListSystemGroupKind.None;
+
+ // Clear the existing items
+ jumpList.Items.Clear();
+
+ // Add each action
+ foreach (var a in actions)
+ jumpList.Items.Add(a.ToJumpListItem());
+
+ // Save the changes
+ await jumpList.SaveAsync();
+ }
+
+ static AppAction ToAction(this JumpListItem item)
+ => new AppAction(ArgumentsToId(item.Arguments), item.DisplayName, item.Description);
+
+ static string ArgumentsToId(string arguments)
+ {
+ if (arguments?.StartsWith(appActionPrefix) ?? false)
+ return Encoding.Default.GetString(Convert.FromBase64String(arguments.Substring(appActionPrefix.Length)));
+
+ return default;
+ }
+
+ static JumpListItem ToJumpListItem(this AppAction action)
+ {
+ var id = appActionPrefix + Convert.ToBase64String(Encoding.Default.GetBytes(action.Id));
+ var item = JumpListItem.CreateWithArguments(id, action.Title);
+
+ if (!string.IsNullOrEmpty(action.Subtitle))
+ item.Description = action.Subtitle;
+
+ if (!string.IsNullOrEmpty(action.Icon))
+ {
+ var dir = IconDirectory.Trim('/', '\\').Replace('\\', '/');
+
+ var ext = IconExtension;
+ if (!string.IsNullOrEmpty(ext) && !ext.StartsWith("."))
+ ext = "." + ext;
+
+ item.Logo = new Uri($"ms-appx:///{dir}/{action.Icon}{ext}");
+ }
+
+ return item;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/AppInfo/AppInfo.android.cs b/Xamarin.Essentials/AppInfo/AppInfo.android.cs
index d397abd25..045b8a050 100644
--- a/Xamarin.Essentials/AppInfo/AppInfo.android.cs
+++ b/Xamarin.Essentials/AppInfo/AppInfo.android.cs
@@ -3,6 +3,11 @@
using Android.Content.PM;
using Android.Content.Res;
using Android.Provider;
+#if __ANDROID_29__
+using AndroidX.Core.Content.PM;
+#else
+using Android.Support.V4.Content.PM;
+#endif
namespace Xamarin.Essentials
{
@@ -33,7 +38,13 @@ static string PlatformGetBuild()
var packageName = Platform.AppContext.PackageName;
using (var info = pm.GetPackageInfo(packageName, PackageInfoFlags.MetaData))
{
+#if __ANDROID_28__
+ return PackageInfoCompat.GetLongVersionCode(info).ToString(CultureInfo.InvariantCulture);
+#else
+#pragma warning disable CS0618 // Type or member is obsolete
return info.VersionCode.ToString(CultureInfo.InvariantCulture);
+#pragma warning restore CS0618 // Type or member is obsolete
+#endif
}
}
diff --git a/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.cs b/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.cs
deleted file mode 100644
index 263c42338..000000000
--- a/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using Foundation;
-using UIKit;
-
-namespace Xamarin.Essentials
-{
- public static partial class AppInfo
- {
- static string PlatformGetPackageName() => GetBundleValue("CFBundleIdentifier");
-
- static string PlatformGetName() => GetBundleValue("CFBundleDisplayName") ?? GetBundleValue("CFBundleName");
-
- static string PlatformGetVersionString() => GetBundleValue("CFBundleShortVersionString");
-
- static string PlatformGetBuild() => GetBundleValue("CFBundleVersion");
-
- static string GetBundleValue(string key)
- => NSBundle.MainBundle.ObjectForInfoDictionary(key)?.ToString();
-
-#if __IOS__ || __TVOS__
- static void PlatformShowSettingsUI() =>
- UIApplication.SharedApplication.OpenUrl(new NSUrl(UIApplication.OpenSettingsUrlString));
-#else
- static void PlatformShowSettingsUI() =>
- throw new FeatureNotSupportedException();
-#endif
-
-#if __IOS__ || __TVOS__
- static AppTheme PlatformRequestedTheme()
- {
- if (!Platform.HasOSVersion(13, 0))
- return AppTheme.Unspecified;
-
- var uiStyle = Platform.GetCurrentUIViewController()?.TraitCollection?.UserInterfaceStyle ??
- UITraitCollection.CurrentTraitCollection.UserInterfaceStyle;
-
- return uiStyle switch
- {
- UIUserInterfaceStyle.Light => AppTheme.Light,
- UIUserInterfaceStyle.Dark => AppTheme.Dark,
- _ => AppTheme.Unspecified
- };
- }
-#else
- static AppTheme PlatformRequestedTheme() =>
- AppTheme.Unspecified;
-#endif
- }
-}
diff --git a/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.macos.cs b/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.macos.cs
new file mode 100644
index 000000000..3ac7f3521
--- /dev/null
+++ b/Xamarin.Essentials/AppInfo/AppInfo.ios.tvos.watchos.macos.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Foundation;
+#if __IOS__ || __TVOS__
+using UIKit;
+#elif __MACOS__
+using AppKit;
+#endif
+
+namespace Xamarin.Essentials
+{
+ public static partial class AppInfo
+ {
+ static string PlatformGetPackageName() => GetBundleValue("CFBundleIdentifier");
+
+ static string PlatformGetName() => GetBundleValue("CFBundleDisplayName") ?? GetBundleValue("CFBundleName");
+
+ static string PlatformGetVersionString() => GetBundleValue("CFBundleShortVersionString");
+
+ static string PlatformGetBuild() => GetBundleValue("CFBundleVersion");
+
+ static string GetBundleValue(string key)
+ => NSBundle.MainBundle.ObjectForInfoDictionary(key)?.ToString();
+
+#if __IOS__ || __TVOS__
+ static void PlatformShowSettingsUI() =>
+ UIApplication.SharedApplication.OpenUrl(new NSUrl(UIApplication.OpenSettingsUrlString));
+#elif __MACOS__
+ static void PlatformShowSettingsUI()
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ var prefsApp = ScriptingBridge.SBApplication.FromBundleIdentifier("com.apple.systempreferences");
+ prefsApp.SendMode = ScriptingBridge.AESendMode.NoReply;
+ prefsApp.Activate();
+ });
+ }
+#else
+ static void PlatformShowSettingsUI() =>
+ throw new FeatureNotSupportedException();
+#endif
+
+#if __IOS__ || __TVOS__
+ static AppTheme PlatformRequestedTheme()
+ {
+ if (!Platform.HasOSVersion(13, 0))
+ return AppTheme.Unspecified;
+
+ var uiStyle = Platform.GetCurrentUIViewController()?.TraitCollection?.UserInterfaceStyle ??
+ UITraitCollection.CurrentTraitCollection.UserInterfaceStyle;
+
+ return uiStyle switch
+ {
+ UIUserInterfaceStyle.Light => AppTheme.Light,
+ UIUserInterfaceStyle.Dark => AppTheme.Dark,
+ _ => AppTheme.Unspecified
+ };
+ }
+#elif __MACOS__
+ static AppTheme PlatformRequestedTheme()
+ {
+ if (DeviceInfo.Version >= new Version(10, 14))
+ {
+ var app = NSAppearance.CurrentAppearance?.FindBestMatch(new string[]
+ {
+ NSAppearance.NameAqua,
+ NSAppearance.NameDarkAqua
+ });
+
+ if (string.IsNullOrEmpty(app))
+ return AppTheme.Unspecified;
+
+ if (app == NSAppearance.NameDarkAqua)
+ return AppTheme.Dark;
+ }
+ return AppTheme.Light;
+ }
+#else
+ static AppTheme PlatformRequestedTheme() =>
+ AppTheme.Unspecified;
+#endif
+
+ internal static bool VerifyHasUrlScheme(string scheme)
+ {
+ var cleansed = scheme.Replace("://", string.Empty);
+ var schemes = GetCFBundleURLSchemes().ToList();
+ return schemes.Any(x => x != null && x.Equals(cleansed, StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ internal static IEnumerable GetCFBundleURLSchemes()
+ {
+ var schemes = new List();
+
+ NSObject nsobj = null;
+ if (!NSBundle.MainBundle.InfoDictionary.TryGetValue((NSString)"CFBundleURLTypes", out nsobj))
+ return schemes;
+
+ var array = nsobj as NSArray;
+
+ if (array == null)
+ return schemes;
+
+ for (nuint i = 0; i < array.Count; i++)
+ {
+ var d = array.GetItem(i);
+ if (d == null || !d.Any())
+ continue;
+
+ if (!d.TryGetValue((NSString)"CFBundleURLSchemes", out nsobj))
+ continue;
+
+ var a = nsobj as NSArray;
+ var urls = ConvertToIEnumerable(a).Select(x => x.ToString()).ToArray();
+ foreach (var url in urls)
+ schemes.Add(url);
+ }
+
+ return schemes;
+ }
+
+ static IEnumerable ConvertToIEnumerable(NSArray array)
+ where T : class, ObjCRuntime.INativeObject
+ {
+ for (nuint i = 0; i < array.Count; i++)
+ yield return array.GetItem(i);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/AssemblyInfo/AssemblyInfo.ios.tvos.watchos.cs b/Xamarin.Essentials/AssemblyInfo/AssemblyInfo.ios.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/AssemblyInfo/AssemblyInfo.ios.tvos.watchos.cs
rename to Xamarin.Essentials/AssemblyInfo/AssemblyInfo.ios.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Barometer/Barometer.netstandard.tvos.cs b/Xamarin.Essentials/Barometer/Barometer.netstandard.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Barometer/Barometer.netstandard.tvos.cs
rename to Xamarin.Essentials/Barometer/Barometer.netstandard.tvos.macos.cs
diff --git a/Xamarin.Essentials/Battery/Battery.macos.cs b/Xamarin.Essentials/Battery/Battery.macos.cs
new file mode 100644
index 000000000..4ae34d025
--- /dev/null
+++ b/Xamarin.Essentials/Battery/Battery.macos.cs
@@ -0,0 +1,44 @@
+using System;
+using CoreFoundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Battery
+ {
+ static CFRunLoopSource powerSourceNotification;
+
+ static void StartBatteryListeners()
+ {
+ powerSourceNotification = IOKit.CreatePowerSourceNotification(PowerSourceNotification);
+ CFRunLoop.Current.AddSource(powerSourceNotification, CFRunLoop.ModeDefault);
+ }
+
+ static void StopBatteryListeners()
+ {
+ if (powerSourceNotification != null)
+ {
+ CFRunLoop.Current.RemoveSource(powerSourceNotification, CFRunLoop.ModeDefault);
+ powerSourceNotification = null;
+ }
+ }
+
+ static void PowerSourceNotification()
+ => MainThread.BeginInvokeOnMainThread(OnBatteryInfoChanged);
+
+ static double PlatformChargeLevel => IOKit.GetInternalBatteryChargeLevel();
+
+ static BatteryState PlatformState => IOKit.GetInternalBatteryState();
+
+ static BatteryPowerSource PlatformPowerSource => IOKit.GetProvidingPowerSource();
+
+ static void StartEnergySaverListeners()
+ {
+ }
+
+ static void StopEnergySaverListeners()
+ {
+ }
+
+ static EnergySaverStatus PlatformEnergySaverStatus => EnergySaverStatus.Off;
+ }
+}
diff --git a/Xamarin.Essentials/Browser/Browser.macos.cs b/Xamarin.Essentials/Browser/Browser.macos.cs
new file mode 100644
index 000000000..ec9a80cc0
--- /dev/null
+++ b/Xamarin.Essentials/Browser/Browser.macos.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Browser
+ {
+ static Task PlatformOpenAsync(Uri uri, BrowserLaunchOptions options) =>
+ Task.FromResult(NSWorkspace.SharedWorkspace.OpenUrl(new NSUrl(uri.AbsoluteUri)));
+ }
+}
diff --git a/Xamarin.Essentials/Browser/Browser.shared.cs b/Xamarin.Essentials/Browser/Browser.shared.cs
index 01847c194..b530e96ff 100644
--- a/Xamarin.Essentials/Browser/Browser.shared.cs
+++ b/Xamarin.Essentials/Browser/Browser.shared.cs
@@ -38,6 +38,9 @@ public static Task OpenAsync(Uri uri, BrowserLaunchOptions options) =>
internal static Uri EscapeUri(Uri uri)
{
+ if (uri == null)
+ throw new ArgumentNullException(nameof(uri));
+
#if NETSTANDARD1_0
return uri;
#else
diff --git a/Xamarin.Essentials/Clipboard/Clipboard.macos.cs b/Xamarin.Essentials/Clipboard/Clipboard.macos.cs
new file mode 100644
index 000000000..3519862c2
--- /dev/null
+++ b/Xamarin.Essentials/Clipboard/Clipboard.macos.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Clipboard
+ {
+ static readonly string pasteboardType = NSPasteboard.NSPasteboardTypeString;
+ static readonly string[] pasteboardTypes = { pasteboardType };
+
+ static NSPasteboard Pasteboard => NSPasteboard.GeneralPasteboard;
+
+ static Task PlatformSetTextAsync(string text)
+ {
+ Pasteboard.DeclareTypes(pasteboardTypes, null);
+ Pasteboard.ClearContents();
+ Pasteboard.SetStringForType(text, pasteboardType);
+
+ return Task.CompletedTask;
+ }
+
+ static bool PlatformHasText =>
+ GetPasteboardText() != null;
+
+ static Task PlatformGetTextAsync()
+ => Task.FromResult(GetPasteboardText());
+
+ static string GetPasteboardText()
+ => Pasteboard.ReadObjectsForClasses(
+ new ObjCRuntime.Class[] { new ObjCRuntime.Class(typeof(NSString)) },
+ null)?[0]?.ToString();
+
+ static void StartClipboardListeners()
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static void StopClipboardListeners()
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+}
diff --git a/Xamarin.Essentials/Clipboard/Clipboard.shared.cs b/Xamarin.Essentials/Clipboard/Clipboard.shared.cs
index e6c8d50f2..f57232cae 100644
--- a/Xamarin.Essentials/Clipboard/Clipboard.shared.cs
+++ b/Xamarin.Essentials/Clipboard/Clipboard.shared.cs
@@ -6,7 +6,7 @@ namespace Xamarin.Essentials
public static partial class Clipboard
{
public static Task SetTextAsync(string text)
- => PlatformSetTextAsync(text);
+ => PlatformSetTextAsync(text ?? string.Empty);
public static bool HasText
=> PlatformHasText;
diff --git a/Xamarin.Essentials/Compass/Compass.netstandard.tvos.watchos.cs b/Xamarin.Essentials/Compass/Compass.netstandard.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Compass/Compass.netstandard.tvos.watchos.cs
rename to Xamarin.Essentials/Compass/Compass.netstandard.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.cs b/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.cs
rename to Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.macos.cs
diff --git a/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.reachability.cs b/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.macos.reachability.cs
similarity index 97%
rename from Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.reachability.cs
rename to Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.macos.reachability.cs
index 1242a573d..7823f086f 100644
--- a/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.reachability.cs
+++ b/Xamarin.Essentials/Connectivity/Connectivity.ios.tvos.macos.reachability.cs
@@ -33,8 +33,10 @@ internal static NetworkStatus RemoteHostStatus()
if (!IsReachableWithoutRequiringConnection(flags))
return NetworkStatus.NotReachable;
+#if __IOS__
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
return NetworkStatus.ReachableViaCarrierDataNetwork;
+#endif
return NetworkStatus.ReachableViaWiFiNetwork;
}
@@ -46,9 +48,11 @@ internal static NetworkStatus InternetConnectionStatus()
var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
+#if __IOS__
// If it's a WWAN connection..
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
status = NetworkStatus.ReachableViaCarrierDataNetwork;
+#endif
// If the connection is reachable and no connection is required, then assume it's WiFi
if (defaultNetworkAvailable)
@@ -73,12 +77,15 @@ internal static IEnumerable GetActiveConnectionType()
var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
- // If it's a WWAN connection..
+#if __IOS__
+ // If it's a WWAN connection.
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
{
status.Add(NetworkStatus.ReachableViaCarrierDataNetwork);
}
- else if (defaultNetworkAvailable)
+#endif
+
+ if (defaultNetworkAvailable)
{
status.Add(NetworkStatus.ReachableViaWiFiNetwork);
}
@@ -113,10 +120,12 @@ internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFl
// Do we need a connection to reach it?
var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0;
+#if __IOS__
// Since the network stack will automatically try to get the WAN up,
// probe that
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
noConnectionRequired = true;
+#endif
return isReachable && noConnectionRequired;
}
diff --git a/Xamarin.Essentials/Contacts/Contacts.android.cs b/Xamarin.Essentials/Contacts/Contacts.android.cs
new file mode 100644
index 000000000..871219274
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.android.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content;
+using Android.Provider;
+using Net = Android.Net;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ static async Task PlatformPickContactAsync()
+ {
+ using var intent = new Intent(Intent.ActionPick);
+ intent.SetType(ContactsContract.CommonDataKinds.Phone.ContentType);
+ var result = await IntermediateActivity.StartAsync(intent, Platform.requestCodePickContact).ConfigureAwait(false);
+
+ if (result?.Data != null)
+ return PlatformGetContacts(result.Data);
+
+ return null;
+ }
+
+ internal static Contact PlatformGetContacts(Net.Uri contactUri)
+ {
+ if (contactUri == null)
+ return default;
+
+ using var context = Platform.AppContext.ContentResolver;
+
+ using var cur = context.Query(contactUri, null, null, null, null);
+ var emails = new List();
+ var phones = new List();
+ var bDate = string.Empty;
+
+ if (cur.MoveToFirst())
+ {
+ var typeOfContact = cur.GetString(cur.GetColumnIndex(ContactsContract.CommonDataKinds.Phone.InterfaceConsts.Type));
+
+ var name = cur.GetString(cur.GetColumnIndex(ContactsContract.Contacts.InterfaceConsts.DisplayName));
+ var id = cur.GetString(cur.GetColumnIndex(ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId));
+ var idQ = new string[1] { id };
+ global::Android.Database.ICursor cursor = null;
+
+ GetPhones(ref idQ, ref cursor, phones, context);
+
+ GetEmails(ref idQ, ref cursor, emails, context);
+
+ cursor?.Dispose();
+
+ return new Contact(name, phones, emails, GetPhoneContactType(typeOfContact));
+ }
+
+ return default;
+
+ static void GetPhones(ref string[] idQ, ref global::Android.Database.ICursor cursor, List phones, ContentResolver context)
+ {
+ var projection = new string[2]
+ {
+ ContactsContract.CommonDataKinds.Phone.Number,
+ ContactsContract.CommonDataKinds.Phone.InterfaceConsts.Type,
+ };
+
+ var uri = ContactsContract.CommonDataKinds.Phone.ContentUri.BuildUpon().AppendQueryParameter(ContactsContract.RemoveDuplicateEntries, "1").Build();
+
+ cursor = context.Query(
+ uri,
+ null,
+ ContactsContract.CommonDataKinds.Phone.InterfaceConsts.ContactId + "=?",
+ idQ,
+ null);
+
+ if (cursor.MoveToFirst())
+ {
+ do
+ {
+ var phone = cursor.GetString(cursor.GetColumnIndex(projection[0]));
+ var phoneType = cursor.GetString(cursor.GetColumnIndex(projection[1]));
+
+ var contactType = GetPhoneContactType(phoneType);
+
+ phones.Add(new ContactPhone(phone, contactType));
+ }
+ while (cursor.MoveToNext());
+ }
+ cursor.Close();
+ }
+
+ static void GetEmails(ref string[] idQ, ref global::Android.Database.ICursor cursor, List emails, ContentResolver context)
+ {
+ var projection = new string[2]
+ {
+ ContactsContract.CommonDataKinds.Email.Address,
+ ContactsContract.CommonDataKinds.Email.InterfaceConsts.Type
+ };
+
+ var uri = ContactsContract.CommonDataKinds.Email.ContentUri.BuildUpon().AppendQueryParameter(ContactsContract.RemoveDuplicateEntries, "1").Build();
+
+ cursor = context.Query(uri, null, ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId + "=?", idQ, null);
+
+ while (cursor.MoveToNext())
+ {
+ var email = cursor.GetString(cursor.GetColumnIndex(projection[0]));
+ var emailType = cursor.GetString(cursor.GetColumnIndex(projection[1]));
+
+ var contactType = GetEmailContactType(emailType);
+
+ emails.Add(new ContactEmail(email, contactType));
+ }
+
+ cursor.Close();
+ }
+ }
+
+ static ContactType GetPhoneContactType(string type)
+ {
+ if (int.TryParse(type, out var typeInt))
+ {
+ try
+ {
+ var phoneKind = (PhoneDataKind)typeInt;
+ return phoneKind switch
+ {
+ PhoneDataKind.Home => ContactType.Personal,
+ PhoneDataKind.Mobile => ContactType.Personal,
+ PhoneDataKind.Main => ContactType.Personal,
+ PhoneDataKind.Work => ContactType.Work,
+ PhoneDataKind.WorkMobile => ContactType.Work,
+ PhoneDataKind.CompanyMain => ContactType.Work,
+ PhoneDataKind.WorkPager => ContactType.Work,
+ _ => ContactType.Unknown
+ };
+ }
+ catch (Exception)
+ {
+ return ContactType.Unknown;
+ }
+ }
+ return ContactType.Unknown;
+ }
+
+ static ContactType GetEmailContactType(string type)
+ {
+ if (int.TryParse(type, out var typeInt))
+ {
+ try
+ {
+ var emailKind = (EmailDataKind)typeInt;
+ return emailKind switch
+ {
+ EmailDataKind.Home => ContactType.Personal,
+ EmailDataKind.Work => ContactType.Work,
+ _ => ContactType.Unknown
+ };
+ }
+ catch (Exception)
+ {
+ return ContactType.Unknown;
+ }
+ }
+ return ContactType.Unknown;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Contacts/Contacts.ios.cs b/Xamarin.Essentials/Contacts/Contacts.ios.cs
new file mode 100644
index 000000000..5dfeca196
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.ios.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Contacts;
+using ContactsUI;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ static Task PlatformPickContactAsync()
+ {
+ var uiView = Platform.GetCurrentViewController();
+ if (uiView == null)
+ throw new ArgumentNullException($"The View Controller can't be null.");
+
+ var source = new TaskCompletionSource();
+
+ using var picker = new CNContactPickerViewController
+ {
+ Delegate = new ContactPickerDelegate(phoneContact =>
+ source?.TrySetResult(Contacts.GetContact(phoneContact)))
+ };
+
+ uiView.PresentViewController(picker, true, null);
+
+ return source.Task;
+ }
+
+ internal static Contact GetContact(CNContact contact)
+ {
+ if (contact == null)
+ return default;
+
+ try
+ {
+ var contactType = ToPhoneContact(contact.ContactType);
+ var phones = new List();
+
+ foreach (var item in contact.PhoneNumbers)
+ phones.Add(new ContactPhone(item?.Value?.StringValue, contactType));
+
+ var emails = new List();
+
+ foreach (var item in contact.EmailAddresses)
+ emails.Add(new ContactEmail(item?.Value?.ToString(), contactType));
+
+ var name = string.Empty;
+
+ if (string.IsNullOrEmpty(contact.MiddleName))
+ name = $"{contact.GivenName} {contact.FamilyName}";
+ else
+ name = $"{contact.GivenName} {contact.MiddleName} {contact.FamilyName}";
+
+ return new Contact(name, phones, emails, contactType);
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ finally
+ {
+ contact.Dispose();
+ }
+ }
+
+ static ContactType ToPhoneContact(CNContactType type) => type switch
+ {
+ CNContactType.Person => ContactType.Personal,
+ CNContactType.Organization => ContactType.Work,
+ _ => ContactType.Unknown,
+ };
+
+ class ContactPickerDelegate : CNContactPickerDelegate
+ {
+ public ContactPickerDelegate(Action didSelectContactHandler) =>
+ DidSelectContactHandler = didSelectContactHandler;
+
+ public ContactPickerDelegate(IntPtr handle)
+ : base(handle)
+ {
+ }
+
+ public Action DidSelectContactHandler { get; }
+
+ public override void ContactPickerDidCancel(CNContactPickerViewController picker)
+ {
+ DidSelectContactHandler?.Invoke(default);
+ picker.DismissModalViewController(true);
+ }
+
+ public override void DidSelectContact(CNContactPickerViewController picker, CNContact contact)
+ {
+ DidSelectContactHandler?.Invoke(contact);
+ picker.DismissModalViewController(true);
+ }
+
+ public override void DidSelectContactProperty(CNContactPickerViewController picker, CNContactProperty contactProperty) =>
+ picker.DismissModalViewController(true);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Contacts/Contacts.netstandard.macos.tvos.watchos.cs b/Xamarin.Essentials/Contacts/Contacts.netstandard.macos.tvos.watchos.cs
new file mode 100644
index 000000000..4d1576514
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.netstandard.macos.tvos.watchos.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ static Task PlatformPickContactAsync() => throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+}
diff --git a/Xamarin.Essentials/Contacts/Contacts.shared.cs b/Xamarin.Essentials/Contacts/Contacts.shared.cs
new file mode 100644
index 000000000..17452683f
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.shared.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ public static async Task PickContactAsync()
+ {
+ // iOS does not require permissions for the picker
+ if (DeviceInfo.Platform != DevicePlatform.iOS)
+ await Permissions.EnsureGrantedAsync();
+
+ return await PlatformPickContactAsync();
+ }
+ }
+
+ public enum ContactType
+ {
+ Unknown = 0,
+ Personal = 1,
+ Work = 2
+ }
+}
diff --git a/Xamarin.Essentials/Contacts/Contacts.tizen.cs b/Xamarin.Essentials/Contacts/Contacts.tizen.cs
new file mode 100644
index 000000000..df8b076cf
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.tizen.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Tizen.Applications;
+using Tizen.Pims.Contacts;
+using TizenContact = Tizen.Pims.Contacts.ContactsViews.Contact;
+using TizenEmail = Tizen.Pims.Contacts.ContactsViews.Email;
+using TizenName = Tizen.Pims.Contacts.ContactsViews.Name;
+using TizenNumber = Tizen.Pims.Contacts.ContactsViews.Number;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ static async Task PlatformPickContactAsync()
+ {
+ Permissions.EnsureDeclared();
+ await Permissions.EnsureGrantedAsync();
+
+ var tcs = new TaskCompletionSource();
+
+ var appControl = new AppControl();
+ appControl.Operation = AppControlOperations.Pick;
+ appControl.ExtraData.Add(AppControlData.SectionMode, "single");
+ appControl.LaunchMode = AppControlLaunchMode.Single;
+ appControl.Mime = "application/vnd.tizen.contact";
+
+ AppControl.SendLaunchRequest(appControl, (request, reply, result) =>
+ {
+ Contact contact = null;
+
+ if (result == AppControlReplyResult.Succeeded)
+ {
+ var contactId = reply.ExtraData.Get>(AppControlData.Selected)?.FirstOrDefault();
+
+ if (int.TryParse(contactId, out var contactInt))
+ {
+ var mgr = new ContactsManager();
+
+ var record = mgr.Database.Get(TizenContact.Uri, contactInt);
+
+ if (record != null)
+ {
+ string name = null;
+ var emails = new List();
+ var phones = new List();
+
+ var recordName = record.GetChildRecord(TizenContact.Name, 0);
+ if (recordName != null)
+ {
+ var first = recordName.Get(TizenName.First) ?? string.Empty;
+ var last = recordName.Get(TizenName.Last) ?? string.Empty;
+
+ name = $"{first} {last}".Trim();
+ }
+
+ var emailCount = record.GetChildRecordCount(TizenContact.Email);
+ for (var i = 0; i < emailCount; i++)
+ {
+ var item = record.GetChildRecord(TizenContact.Email, i);
+ var addr = item.Get(TizenEmail.Address);
+ var type = (TizenEmail.Types)item.Get(TizenEmail.Type);
+
+ emails.Add(new ContactEmail(addr, GetContactType(type)));
+ }
+
+ var phoneCount = record.GetChildRecordCount(TizenContact.Number);
+ for (var i = 0; i < phoneCount; i++)
+ {
+ var item = record.GetChildRecord(TizenContact.Number, i);
+ var number = item.Get(TizenNumber.NumberData);
+ var type = (TizenNumber.Types)item.Get(TizenNumber.Type);
+
+ phones.Add(new ContactPhone(number, GetContactType(type)));
+ }
+
+ contact = new Contact(name, phones, emails, ContactType.Unknown);
+ }
+ }
+ }
+
+ tcs.TrySetResult(contact);
+ });
+
+ return await tcs.Task;
+ }
+
+ static ContactType GetContactType(TizenEmail.Types emailType)
+ => emailType switch
+ {
+ TizenEmail.Types.Home => ContactType.Personal,
+ TizenEmail.Types.Mobile => ContactType.Personal,
+ TizenEmail.Types.Work => ContactType.Work,
+ _ => ContactType.Unknown
+ };
+
+ static ContactType GetContactType(TizenNumber.Types numberType)
+ => numberType switch
+ {
+ TizenNumber.Types.Car => ContactType.Personal,
+ TizenNumber.Types.Cell => ContactType.Personal,
+ TizenNumber.Types.Home => ContactType.Personal,
+ TizenNumber.Types.Main => ContactType.Personal,
+ TizenNumber.Types.Message => ContactType.Personal,
+ TizenNumber.Types.Video => ContactType.Personal,
+ TizenNumber.Types.Voice => ContactType.Personal,
+ TizenNumber.Types.Work => ContactType.Work,
+ TizenNumber.Types.Pager => ContactType.Work,
+ TizenNumber.Types.Assistant => ContactType.Work,
+ TizenNumber.Types.Company => ContactType.Work,
+ TizenNumber.Types.Fax => ContactType.Work,
+ _ => ContactType.Unknown
+ };
+ }
+}
diff --git a/Xamarin.Essentials/Contacts/Contacts.uwp.cs b/Xamarin.Essentials/Contacts/Contacts.uwp.cs
new file mode 100644
index 000000000..038c71655
--- /dev/null
+++ b/Xamarin.Essentials/Contacts/Contacts.uwp.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Windows.ApplicationModel.Contacts;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Contacts
+ {
+ static async Task PlatformPickContactAsync()
+ {
+ var contactPicker = new ContactPicker();
+
+ try
+ {
+ var contactSelected = await contactPicker.PickContactAsync();
+
+ if (contactSelected == null)
+ return null;
+
+ var phones = new List();
+ var emails = new List();
+
+ foreach (var item in contactSelected.Phones)
+ phones.Add(new ContactPhone(item.Number, GetPhoneContactType(item.Kind)));
+
+ phones = phones.Distinct().ToList();
+
+ foreach (var item in contactSelected.Emails)
+ emails.Add(new ContactEmail(item.Address, GetEmailContactType(item.Kind)));
+
+ emails = emails.Distinct().ToList();
+
+ return new Contact(
+ contactSelected.Name,
+ phones,
+ emails,
+ ContactType.Unknown);
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+
+ static ContactType GetPhoneContactType(ContactPhoneKind type)
+ => type switch
+ {
+ ContactPhoneKind.Home => ContactType.Personal,
+ ContactPhoneKind.HomeFax => ContactType.Personal,
+ ContactPhoneKind.Mobile => ContactType.Personal,
+ ContactPhoneKind.Work => ContactType.Work,
+ ContactPhoneKind.Pager => ContactType.Work,
+ ContactPhoneKind.BusinessFax => ContactType.Work,
+ ContactPhoneKind.Company => ContactType.Work,
+ _ => ContactType.Unknown
+ };
+
+ static ContactType GetEmailContactType(ContactEmailKind type) => type switch
+ {
+ ContactEmailKind.Personal => ContactType.Personal,
+ ContactEmailKind.Work => ContactType.Work,
+ _ => ContactType.Unknown,
+ };
+ }
+}
diff --git a/Xamarin.Essentials/DeviceDisplay/DeviceDisplay.macos.cs b/Xamarin.Essentials/DeviceDisplay/DeviceDisplay.macos.cs
new file mode 100644
index 000000000..35bbaf967
--- /dev/null
+++ b/Xamarin.Essentials/DeviceDisplay/DeviceDisplay.macos.cs
@@ -0,0 +1,68 @@
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class DeviceDisplay
+ {
+ static uint keepScreenOnId = 0;
+ static NSObject screenMetricsObserver;
+
+ static bool PlatformKeepScreenOn
+ {
+ get
+ {
+ return keepScreenOnId != 0;
+ }
+
+ set
+ {
+ if (KeepScreenOn == value)
+ return;
+
+ if (value)
+ {
+ IOKit.PreventUserIdleDisplaySleep("KeepScreenOn", out keepScreenOnId);
+ }
+ else
+ {
+ if (IOKit.AllowUserIdleDisplaySleep(keepScreenOnId))
+ keepScreenOnId = 0;
+ }
+ }
+ }
+
+ static DisplayInfo GetMainDisplayInfo()
+ {
+ var mainScreen = NSScreen.MainScreen;
+ var frame = mainScreen.Frame;
+ var scale = mainScreen.BackingScaleFactor;
+
+ return new DisplayInfo(
+ width: frame.Width,
+ height: frame.Height,
+ density: scale,
+ orientation: DisplayOrientation.Portrait,
+ rotation: DisplayRotation.Rotation0);
+ }
+
+ static void StartScreenMetricsListeners()
+ {
+ if (screenMetricsObserver == null)
+ {
+ screenMetricsObserver = NSNotificationCenter.DefaultCenter.AddObserver(NSApplication.DidChangeScreenParametersNotification, OnDidChangeScreenParameters);
+ }
+ }
+
+ static void StopScreenMetricsListeners()
+ {
+ screenMetricsObserver?.Dispose();
+ }
+
+ static void OnDidChangeScreenParameters(NSNotification notification)
+ {
+ var metrics = GetMainDisplayInfo();
+ OnMainDisplayInfoChanged(metrics);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/DeviceInfo/DeviceInfo.android.cs b/Xamarin.Essentials/DeviceInfo/DeviceInfo.android.cs
index d9ea01510..ea7617f1e 100644
--- a/Xamarin.Essentials/DeviceInfo/DeviceInfo.android.cs
+++ b/Xamarin.Essentials/DeviceInfo/DeviceInfo.android.cs
@@ -87,15 +87,23 @@ static DeviceIdiom DetectIdiom(UiMode uiMode)
static DeviceType GetDeviceType()
{
var isEmulator =
+ (Build.Brand.StartsWith("generic", StringComparison.InvariantCulture) && Build.Device.StartsWith("generic", StringComparison.InvariantCulture)) ||
Build.Fingerprint.StartsWith("generic", StringComparison.InvariantCulture) ||
Build.Fingerprint.StartsWith("unknown", StringComparison.InvariantCulture) ||
+ Build.Hardware.Contains("goldfish") ||
+ Build.Hardware.Contains("ranchu") ||
Build.Model.Contains("google_sdk") ||
Build.Model.Contains("Emulator") ||
Build.Model.Contains("Android SDK built for x86") ||
Build.Manufacturer.Contains("Genymotion") ||
Build.Manufacturer.Contains("VS Emulator") ||
- (Build.Brand.StartsWith("generic", StringComparison.InvariantCulture) && Build.Device.StartsWith("generic", StringComparison.InvariantCulture)) ||
- Build.Product.Equals("google_sdk", StringComparison.InvariantCulture);
+ Build.Product.Contains("emulator") ||
+ Build.Product.Contains("google_sdk") ||
+ Build.Product.Contains("sdk") ||
+ Build.Product.Contains("sdk_google") ||
+ Build.Product.Contains("sdk_x86") ||
+ Build.Product.Contains("simulator") ||
+ Build.Product.Contains("vbox86p");
if (isEmulator)
return DeviceType.Virtual;
diff --git a/Xamarin.Essentials/DeviceInfo/DeviceInfo.macos.cs b/Xamarin.Essentials/DeviceInfo/DeviceInfo.macos.cs
new file mode 100644
index 000000000..d1a217773
--- /dev/null
+++ b/Xamarin.Essentials/DeviceInfo/DeviceInfo.macos.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Runtime.InteropServices;
+using Foundation;
+using ObjCRuntime;
+
+namespace Xamarin.Essentials
+{
+ public static partial class DeviceInfo
+ {
+ [DllImport(Constants.SystemConfigurationLibrary)]
+ static extern IntPtr SCDynamicStoreCopyComputerName(IntPtr store, IntPtr encoding);
+
+ [DllImport(Constants.CoreFoundationLibrary)]
+ static extern void CFRelease(IntPtr cf);
+
+ static string GetModel() =>
+ IOKit.GetPlatformExpertPropertyValue("model")?.ToString() ?? string.Empty;
+
+ static string GetManufacturer() => "Apple";
+
+ static string GetDeviceName()
+ {
+ var computerNameHandle = SCDynamicStoreCopyComputerName(IntPtr.Zero, IntPtr.Zero);
+
+ if (computerNameHandle == IntPtr.Zero)
+ return null;
+
+ try
+ {
+ return NSString.FromHandle(computerNameHandle);
+ }
+ finally
+ {
+ CFRelease(computerNameHandle);
+ }
+ }
+
+ static string GetVersionString()
+ {
+ using var info = new NSProcessInfo();
+ return info.OperatingSystemVersion.ToString();
+ }
+
+ static DevicePlatform GetPlatform() => DevicePlatform.macOS;
+
+ static DeviceIdiom GetIdiom() => DeviceIdiom.Desktop;
+
+ static DeviceType GetDeviceType() => DeviceType.Physical;
+ }
+}
diff --git a/Xamarin.Essentials/Email/Email.android.cs b/Xamarin.Essentials/Email/Email.android.cs
index 7367ecf43..23cb0f792 100644
--- a/Xamarin.Essentials/Email/Email.android.cs
+++ b/Xamarin.Essentials/Email/Email.android.cs
@@ -85,7 +85,7 @@ static Intent CreateIntent(EmailMessage message)
var uris = new List();
foreach (var attachment in message.Attachments)
{
- uris.Add(Platform.GetShareableFileUri(attachment.FullPath));
+ uris.Add(Platform.GetShareableFileUri(attachment));
}
if (uris.Count > 1)
diff --git a/Xamarin.Essentials/Email/Email.ios.cs b/Xamarin.Essentials/Email/Email.ios.cs
index 06c04d3c6..75a3e5c2f 100644
--- a/Xamarin.Essentials/Email/Email.ios.cs
+++ b/Xamarin.Essentials/Email/Email.ios.cs
@@ -9,19 +9,9 @@ namespace Xamarin.Essentials
{
public static partial class Email
{
- internal static bool IsComposeSupported
- {
- get
- {
- var can = MFMailComposeViewController.CanSendMail;
- if (!can)
- {
- var url = NSUrl.FromString("mailto:");
- NSRunLoop.Main.InvokeOnMainThread(() => can = UIApplication.SharedApplication.CanOpenUrl(url));
- }
- return can;
- }
- }
+ internal static bool IsComposeSupported =>
+ MFMailComposeViewController.CanSendMail ||
+ MainThread.InvokeOnMainThread(() => UIApplication.SharedApplication.CanOpenUrl(NSUrl.FromString("mailto:")));
static Task PlatformComposeAsync(EmailMessage message)
{
@@ -54,7 +44,7 @@ static Task ComposeWithMailCompose(EmailMessage message)
foreach (var attachment in message.Attachments)
{
var data = NSData.FromFile(attachment.FullPath);
- controller.AddAttachmentData(data, attachment.ContentType, attachment.AttachmentName);
+ controller.AddAttachmentData(data, attachment.ContentType, attachment.FileName);
}
}
@@ -63,7 +53,7 @@ static Task ComposeWithMailCompose(EmailMessage message)
controller.Finished += (sender, e) =>
{
controller.DismissViewController(true, null);
- tcs.SetResult(e.Result == MFMailComposeResult.Sent);
+ tcs.TrySetResult(e.Result == MFMailComposeResult.Sent);
};
parentController.PresentViewController(controller, true, null);
diff --git a/Xamarin.Essentials/Email/Email.macos.cs b/Xamarin.Essentials/Email/Email.macos.cs
new file mode 100644
index 000000000..6c2859df3
--- /dev/null
+++ b/Xamarin.Essentials/Email/Email.macos.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Email
+ {
+ internal static bool IsComposeSupported =>
+ MainThread.InvokeOnMainThread(() => NSWorkspace.SharedWorkspace.UrlForApplication(NSUrl.FromString("mailto:")) != null);
+
+ static Task PlatformComposeAsync(EmailMessage message)
+ {
+ var url = GetMailToUri(message);
+
+ using var nsurl = NSUrl.FromString(url);
+ NSWorkspace.SharedWorkspace.OpenUrl(nsurl);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Email/Email.shared.cs b/Xamarin.Essentials/Email/Email.shared.cs
index d3cf8bde3..bc0d670dd 100644
--- a/Xamarin.Essentials/Email/Email.shared.cs
+++ b/Xamarin.Essentials/Email/Email.shared.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Threading.Tasks;
namespace Xamarin.Essentials
@@ -30,15 +28,15 @@ static string GetMailToUri(EmailMessage message)
var parts = new List();
if (!string.IsNullOrEmpty(message?.Body))
- parts.Add("body=" + WebUtility.UrlEncode(message.Body));
+ parts.Add("body=" + Uri.EscapeDataString(message.Body));
if (!string.IsNullOrEmpty(message?.Subject))
- parts.Add("subject=" + WebUtility.UrlEncode(message.Subject));
- if (message?.To.Count > 0)
- parts.Add("to=" + WebUtility.UrlEncode(string.Join(",", message.To)));
- if (message?.Cc.Count > 0)
- parts.Add("cc=" + WebUtility.UrlEncode(string.Join(",", message.Cc)));
- if (message?.Bcc.Count > 0)
- parts.Add("bcc=" + WebUtility.UrlEncode(string.Join(",", message.Bcc)));
+ parts.Add("subject=" + Uri.EscapeDataString(message.Subject));
+ if (message?.To?.Count > 0)
+ parts.Add("to=" + Uri.EscapeDataString(string.Join(",", message.To)));
+ if (message?.Cc?.Count > 0)
+ parts.Add("cc=" + Uri.EscapeDataString(string.Join(",", message.Cc)));
+ if (message?.Bcc?.Count > 0)
+ parts.Add("bcc=" + Uri.EscapeDataString(string.Join(",", message.Bcc)));
var uri = "mailto:";
if (parts.Count > 0)
diff --git a/Xamarin.Essentials/Email/Email.uwp.cs b/Xamarin.Essentials/Email/Email.uwp.cs
index b2591a023..1ee88236f 100644
--- a/Xamarin.Essentials/Email/Email.uwp.cs
+++ b/Xamarin.Essentials/Email/Email.uwp.cs
@@ -38,7 +38,7 @@ static async Task PlatformComposeAsync(EmailMessage message)
var file = attachment.File ?? await StorageFile.GetFileFromPathAsync(path);
var stream = RandomAccessStreamReference.CreateFromFile(file);
- var nativeAttachment = new NativeEmailAttachment(attachment.AttachmentName, stream);
+ var nativeAttachment = new NativeEmailAttachment(attachment.FileName, stream);
if (!string.IsNullOrEmpty(attachment.ContentType))
nativeAttachment.MimeType = attachment.ContentType;
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.android.cs b/Xamarin.Essentials/FilePicker/FilePicker.android.cs
new file mode 100644
index 000000000..120c5da48
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.android.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content;
+using Android.Provider;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static async Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ {
+ // we only need the permission when accessing the file, but it's more natural
+ // to ask the user first, then show the picker.
+ await Permissions.RequestAsync();
+
+ // Essentials supports >= API 19 where this action is available
+ var action = Intent.ActionOpenDocument;
+
+ var intent = new Intent(action);
+ intent.SetType("*/*");
+ intent.AddFlags(ActivityFlags.GrantPersistableUriPermission);
+ intent.PutExtra(Intent.ExtraAllowMultiple, allowMultiple);
+
+ var allowedTypes = options?.FileTypes?.Value?.ToArray();
+ if (allowedTypes?.Length > 0)
+ intent.PutExtra(Intent.ExtraMimeTypes, allowedTypes);
+
+ var pickerIntent = Intent.CreateChooser(intent, options?.PickerTitle ?? "Select file");
+
+ try
+ {
+ var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker);
+ var resultList = new List();
+
+ var clipData = new List();
+
+ if (result.ClipData == null)
+ {
+ clipData.Add(result.Data);
+ }
+ else
+ {
+ for (var i = 0; i < result.ClipData.ItemCount; i++)
+ clipData.Add(result.ClipData.GetItemAt(i).Uri);
+ }
+
+ foreach (var contentUri in clipData)
+ {
+ Platform.AppContext.ContentResolver.TakePersistableUriPermission(
+ contentUri,
+ ActivityFlags.GrantReadUriPermission);
+
+ resultList.Add(new FileResult(contentUri));
+ }
+
+ return resultList;
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ }
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static FilePickerFileType PlatformImageFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Android, new[] { "image/png", "image/jpeg" } }
+ });
+
+ public static FilePickerFileType PlatformPngFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Android, new[] { "image/png" } }
+ });
+
+ public static FilePickerFileType PlatformVideoFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Android, new[] { "video/*" } }
+ });
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.ios.cs b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs
new file mode 100644
index 000000000..f45e5567d
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Foundation;
+using MobileCoreServices;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ {
+ if (allowMultiple && !Platform.HasOSVersion(11, 0))
+ throw new FeatureNotSupportedException("Multiple file picking is only available on iOS 11 or later.");
+
+ var allowedUtis = options?.FileTypes?.Value?.ToArray() ?? new string[]
+ {
+ UTType.Content,
+ UTType.Item,
+ "public.data"
+ };
+
+ var tcs = new TaskCompletionSource>();
+
+ // Note: Importing (UIDocumentPickerMode.Import) makes a local copy of the document,
+ // while opening (UIDocumentPickerMode.Open) opens the document directly. We do the
+ // latter, so the user accesses the original file.
+ var documentPicker = new UIDocumentPickerViewController(allowedUtis, UIDocumentPickerMode.Open);
+ documentPicker.AllowsMultipleSelection = allowMultiple;
+ documentPicker.Delegate = new PickerDelegate
+ {
+ PickHandler = urls =>
+ {
+ try
+ {
+ // there was a cancellation
+ if (urls?.Any() ?? false)
+ tcs.TrySetResult(urls.Select(url => new UIDocumentFileResult(url)));
+ else
+ tcs.TrySetResult(Enumerable.Empty());
+ }
+ catch (Exception ex)
+ {
+ // pass exception to task so that it doesn't get lost in the UI main loop
+ tcs.SetException(ex);
+ }
+ }
+ };
+
+ var parentController = Platform.GetCurrentViewController();
+
+ parentController.PresentViewController(documentPicker, true, null);
+
+ return tcs.Task;
+ }
+
+ class PickerDelegate : UIDocumentPickerDelegate
+ {
+ public Action> PickHandler { get; set; }
+
+ public override void WasCancelled(UIDocumentPickerViewController controller)
+ => PickHandler?.Invoke(null);
+
+ public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl[] urls)
+ => PickHandler?.Invoke(urls);
+
+ public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url)
+ => PickHandler?.Invoke(new List { url });
+ }
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static FilePickerFileType PlatformImageFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.iOS, new[] { (string)UTType.Image } }
+ });
+
+ public static FilePickerFileType PlatformPngFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.iOS, new[] { (string)UTType.PNG } }
+ });
+
+ public static FilePickerFileType PlatformVideoFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.iOS, new string[] { UTType.MPEG4, UTType.Video, UTType.AVIMovie, UTType.AppleProtectedMPEG4Video, "mp4", "m4v", "mpg", "mpeg", "mp2", "mov", "avi", "mkv", "flv", "gifv", "qt" } }
+ });
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.macos.cs b/Xamarin.Essentials/FilePicker/FilePicker.macos.cs
new file mode 100644
index 000000000..002830a6f
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.macos.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AppKit;
+using MobileCoreServices;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ {
+ var openPanel = new NSOpenPanel
+ {
+ CanChooseFiles = true,
+ AllowsMultipleSelection = allowMultiple,
+ CanChooseDirectories = false
+ };
+
+ if (options.PickerTitle != null)
+ openPanel.Title = options.PickerTitle;
+
+ SetFileTypes(options, openPanel);
+
+ var resultList = new List();
+ var panelResult = openPanel.RunModal();
+ if (panelResult == (nint)(long)NSModalResponse.OK)
+ {
+ foreach (var url in openPanel.Urls)
+ resultList.Add(new FileResult(url.Path));
+ }
+
+ return Task.FromResult>(resultList);
+ }
+
+ static void SetFileTypes(PickOptions options, NSOpenPanel panel)
+ {
+ var allowedFileTypes = new List();
+
+ if (options?.FileTypes?.Value != null)
+ {
+ foreach (var type in options.FileTypes.Value)
+ {
+ allowedFileTypes.Add(type.TrimStart('*', '.'));
+ }
+ }
+
+ panel.AllowedFileTypes = allowedFileTypes.ToArray();
+ }
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static FilePickerFileType PlatformImageFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.macOS, new string[] { UTType.PNG, UTType.JPEG, "jpeg" } }
+ });
+
+ public static FilePickerFileType PlatformPngFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.macOS, new string[] { UTType.PNG } }
+ });
+
+ public static FilePickerFileType PlatformVideoFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.macOS, new string[] { UTType.MPEG4, UTType.Video, UTType.AVIMovie, UTType.AppleProtectedMPEG4Video, "mp4", "m4v", "mpg", "mpeg", "mp2", "mov", "avi", "mkv", "flv", "gifv", "qt" } }
+ });
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.netstandard.watchos.tvos.cs b/Xamarin.Essentials/FilePicker/FilePicker.netstandard.watchos.tvos.cs
new file mode 100644
index 000000000..f20f754e5
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.netstandard.watchos.tvos.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ => throw new NotImplementedInReferenceAssemblyException();
+ }
+
+ public partial class FilePickerFileType
+ {
+ static FilePickerFileType PlatformImageFileType()
+ => throw new NotImplementedInReferenceAssemblyException();
+
+ static FilePickerFileType PlatformPngFileType()
+ => throw new NotImplementedInReferenceAssemblyException();
+
+ static FilePickerFileType PlatformVideoFileType()
+ => throw new NotImplementedInReferenceAssemblyException();
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.shared.cs b/Xamarin.Essentials/FilePicker/FilePicker.shared.cs
new file mode 100644
index 000000000..1fdaeb07b
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.shared.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ public static async Task PickAsync(PickOptions options = null) =>
+ (await PlatformPickAsync(options))?.FirstOrDefault();
+
+ public static Task> PickMultipleAsync(PickOptions options = null) =>
+ PlatformPickAsync(options ?? PickOptions.Default, true);
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static readonly FilePickerFileType Images = PlatformImageFileType();
+ public static readonly FilePickerFileType Png = PlatformPngFileType();
+ public static readonly FilePickerFileType Videos = PlatformVideoFileType();
+
+ readonly IDictionary> fileTypes;
+
+ protected FilePickerFileType()
+ {
+ }
+
+ public FilePickerFileType(IDictionary> fileTypes) =>
+ this.fileTypes = fileTypes;
+
+ public IEnumerable Value => GetPlatformFileType(DeviceInfo.Platform);
+
+ protected virtual IEnumerable GetPlatformFileType(DevicePlatform platform)
+ {
+ if (fileTypes.TryGetValue(platform, out var type))
+ return type;
+
+ throw new PlatformNotSupportedException("This platform does not support this file type.");
+ }
+ }
+
+ public class PickOptions
+ {
+ public static PickOptions Default =>
+ new PickOptions
+ {
+ FileTypes = null,
+ };
+
+ public static PickOptions Images =>
+ new PickOptions
+ {
+ FileTypes = FilePickerFileType.Images
+ };
+
+ public string PickerTitle { get; set; }
+
+ public FilePickerFileType FileTypes { get; set; }
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs b/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs
new file mode 100644
index 000000000..8adac5012
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Tizen;
+using Tizen.Applications;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static async Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ {
+ Permissions.EnsureDeclared();
+ await Permissions.EnsureGrantedAsync();
+
+ var tcs = new TaskCompletionSource>();
+
+ var appControl = new AppControl();
+ appControl.Operation = AppControlOperations.Pick;
+ appControl.ExtraData.Add(AppControlData.SectionMode, allowMultiple ? "multiple" : "single");
+ appControl.LaunchMode = AppControlLaunchMode.Single;
+
+ var fileType = options?.FileTypes?.Value?.FirstOrDefault();
+ appControl.Mime = fileType ?? "*/*";
+
+ var fileResults = new List();
+
+ AppControl.SendLaunchRequest(appControl, (request, reply, result) =>
+ {
+ if (result == AppControlReplyResult.Succeeded)
+ {
+ if (reply.ExtraData.Count() > 0)
+ {
+ var selectedFiles = reply.ExtraData.Get>(AppControlData.Selected).ToList();
+ fileResults.AddRange(selectedFiles.Select(f => new FileResult(f)));
+ }
+ }
+
+ tcs.TrySetResult(fileResults);
+ });
+
+ return await tcs.Task;
+ }
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static FilePickerFileType PlatformImageFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Tizen, new[] { "image/*" } },
+ });
+
+ public static FilePickerFileType PlatformPngFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Tizen, new[] { "image/png" } }
+ });
+
+ public static FilePickerFileType PlatformVideoFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Tizen, new[] { "video/*" } }
+ });
+ }
+}
diff --git a/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs b/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs
new file mode 100644
index 000000000..c2e9e7512
--- /dev/null
+++ b/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Windows.Storage;
+using Windows.Storage.AccessCache;
+using Windows.Storage.Pickers;
+
+namespace Xamarin.Essentials
+{
+ public static partial class FilePicker
+ {
+ static async Task> PlatformPickAsync(PickOptions options, bool allowMultiple = false)
+ {
+ var picker = new FileOpenPicker
+ {
+ ViewMode = PickerViewMode.List,
+ SuggestedStartLocation = PickerLocationId.DocumentsLibrary
+ };
+
+ SetFileTypes(options, picker);
+
+ var resultList = new List();
+
+ if (allowMultiple)
+ {
+ var fileList = await picker.PickMultipleFilesAsync();
+ if (fileList != null)
+ resultList.AddRange(fileList);
+ }
+ else
+ {
+ var file = await picker.PickSingleFileAsync();
+ if (file != null)
+ resultList.Add(file);
+ }
+
+ foreach (var file in resultList)
+ StorageApplicationPermissions.FutureAccessList.Add(file);
+
+ return resultList.Select(storageFile => new FileResult(storageFile));
+ }
+
+ static void SetFileTypes(PickOptions options, FileOpenPicker picker)
+ {
+ var hasAtLeastOneType = false;
+
+ if (options?.FileTypes?.Value != null)
+ {
+ foreach (var type in options.FileTypes.Value)
+ {
+ if (type.StartsWith(".") || type.StartsWith("*."))
+ {
+ picker.FileTypeFilter.Add(type.TrimStart('*'));
+ hasAtLeastOneType = true;
+ }
+ }
+ }
+
+ if (!hasAtLeastOneType)
+ picker.FileTypeFilter.Add("*");
+ }
+ }
+
+ public partial class FilePickerFileType
+ {
+ public static FilePickerFileType PlatformImageFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.UWP, new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" } }
+ });
+
+ public static FilePickerFileType PlatformPngFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.UWP, new[] { "*.png" } }
+ });
+
+ public static FilePickerFileType PlatformVideoFileType() =>
+ new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.UWP, new[] { "*.mp4", "*.mov", "*.avi", "*.wmv", "*.m4v", "*.mpg", "*.mpeg", "*.mp2", "*.mkv", "*.flv", "*.gifv", "*.qt" } }
+ });
+ }
+}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.android.cs b/Xamarin.Essentials/FileSystem/FileSystem.android.cs
index bb376d493..3aafc4552 100644
--- a/Xamarin.Essentials/FileSystem/FileSystem.android.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.android.cs
@@ -1,7 +1,9 @@
using System;
using System.IO;
+using System.Net;
using System.Threading.Tasks;
using Android.App;
+using Android.Provider;
using Android.Webkit;
namespace Xamarin.Essentials
@@ -38,11 +40,96 @@ internal FileBase(Java.IO.File file)
{
}
+ internal FileBase(global::Android.Net.Uri contentUri)
+ : this(GetFullPath(contentUri))
+ {
+ this.contentUri = contentUri;
+ FileName = GetFileName(contentUri);
+ }
+
+ readonly global::Android.Net.Uri contentUri;
+
internal static string PlatformGetContentType(string extension) =>
MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension.TrimStart('.'));
+ static string GetFullPath(global::Android.Net.Uri contentUri)
+ {
+ // if this is a file, use that
+ if (contentUri.Scheme == "file")
+ return contentUri.Path;
+
+ // ask the content provider for the data column, which may contain the actual file path
+#pragma warning disable CS0618 // Type or member is obsolete
+ var path = QueryContentResolverColumn(contentUri, MediaStore.Files.FileColumns.Data);
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ if (!string.IsNullOrEmpty(path) && Path.IsPathRooted(path))
+ return path;
+
+ // fallback: use content URI
+ return contentUri.ToString();
+ }
+
+ static string GetFileName(global::Android.Net.Uri contentUri)
+ {
+ // resolve file name by querying content provider for display name
+ var filename = QueryContentResolverColumn(contentUri, MediaStore.MediaColumns.DisplayName);
+
+ if (string.IsNullOrWhiteSpace(filename))
+ {
+ filename = Path.GetFileName(WebUtility.UrlDecode(contentUri.ToString()));
+ }
+
+ if (!Path.HasExtension(filename))
+ filename = filename.TrimEnd('.') + '.' + GetFileExtensionFromUri(contentUri);
+
+ return filename;
+ }
+
+ static string QueryContentResolverColumn(global::Android.Net.Uri contentUri, string columnName)
+ {
+ string text = null;
+
+ var projection = new[] { columnName };
+ using var cursor = Application.Context.ContentResolver.Query(contentUri, projection, null, null, null);
+ if (cursor?.MoveToFirst() == true)
+ {
+ var columnIndex = cursor.GetColumnIndex(columnName);
+ if (columnIndex != -1)
+ text = cursor.GetString(columnIndex);
+ }
+
+ return text;
+ }
+
+ static string GetFileExtensionFromUri(global::Android.Net.Uri uri)
+ {
+ var mimeType = Application.Context.ContentResolver.GetType(uri);
+ return mimeType != null ? global::Android.Webkit.MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType) : string.Empty;
+ }
+
internal void PlatformInit(FileBase file)
{
}
+
+ internal virtual Task PlatformOpenReadAsync()
+ {
+ if (contentUri?.Scheme == "content")
+ {
+ var content = Application.Context.ContentResolver.OpenInputStream(contentUri);
+ return Task.FromResult(content);
+ }
+
+ var stream = File.OpenRead(FullPath);
+ return Task.FromResult(stream);
+ }
+ }
+
+ public partial class FileResult
+ {
+ internal FileResult(global::Android.Net.Uri contentUri)
+ : base(contentUri)
+ {
+ }
}
}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.ios.cs b/Xamarin.Essentials/FileSystem/FileSystem.ios.cs
new file mode 100644
index 000000000..f88dcb849
--- /dev/null
+++ b/Xamarin.Essentials/FileSystem/FileSystem.ios.cs
@@ -0,0 +1,83 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Foundation;
+using Photos;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ class UIDocumentFileResult : FileResult
+ {
+ readonly Stream fileStream;
+
+ internal UIDocumentFileResult(NSUrl url)
+ : base()
+ {
+ url.StartAccessingSecurityScopedResource();
+
+ var doc = new UIDocument(url);
+ FullPath = doc.FileUrl?.Path;
+ FileName = doc.LocalizedName ?? Path.GetFileName(FullPath);
+
+ // immediately open a file stream, in case iOS cleans up the picked file
+ fileStream = File.OpenRead(FullPath);
+
+ url.StopAccessingSecurityScopedResource();
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ // make sure we are at he beginning
+ fileStream.Seek(0, SeekOrigin.Begin);
+
+ return Task.FromResult(fileStream);
+ }
+ }
+
+ class UIImageFileResult : FileResult
+ {
+ readonly UIImage uiImage;
+ NSData data;
+
+ internal UIImageFileResult(UIImage image)
+ : base()
+ {
+ uiImage = image;
+
+ FullPath = Guid.NewGuid().ToString() + ".png";
+ FileName = FullPath;
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ data ??= uiImage.AsPNG();
+
+ return Task.FromResult(data.AsStream());
+ }
+ }
+
+ class PHAssetFileResult : FileResult
+ {
+ readonly PHAsset phAsset;
+
+ internal PHAssetFileResult(NSUrl url, PHAsset asset, string originalFilename)
+ : base()
+ {
+ phAsset = asset;
+
+ FullPath = url?.AbsoluteString;
+ FileName = originalFilename;
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ var tcsStream = new TaskCompletionSource();
+
+ PHImageManager.DefaultManager.RequestImageData(phAsset, null, new PHImageDataHandler((data, str, orientation, dict) =>
+ tcsStream.TrySetResult(data.AsStream())));
+
+ return tcsStream.Task;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.cs b/Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.macos.cs
similarity index 77%
rename from Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.cs
rename to Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.macos.cs
index b5ce655c2..f0ea77c2d 100644
--- a/Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.ios.tvos.watchos.macos.cs
@@ -20,7 +20,11 @@ static Task PlatformOpenAppPackageFileAsync(string filename)
throw new ArgumentNullException(nameof(filename));
filename = filename.Replace('\\', Path.DirectorySeparatorChar);
- var file = Path.Combine(NSBundle.MainBundle.BundlePath, filename);
+ var root = NSBundle.MainBundle.BundlePath;
+#if __MACOS__
+ root = Path.Combine(root, "Contents", "Resources");
+#endif
+ var file = Path.Combine(root, filename);
return Task.FromResult((Stream)File.OpenRead(file));
}
@@ -39,8 +43,9 @@ static string GetDirectory(NSSearchPathDirectory directory)
public partial class FileBase
{
internal FileBase(NSUrl file)
- : this(NSFileManager.DefaultManager.DisplayName(file?.Path))
+ : this(file?.Path)
{
+ FileName = NSFileManager.DefaultManager.DisplayName(file?.Path);
}
internal static string PlatformGetContentType(string extension)
@@ -50,11 +55,14 @@ internal static string PlatformGetContentType(string extension)
var id = UTType.CreatePreferredIdentifier(UTType.TagClassFilenameExtension, extension, null);
var mimeTypes = UTType.CopyAllTags(id, UTType.TagClassMIMEType);
- return mimeTypes.Length > 0 ? mimeTypes[0] : null;
+ return mimeTypes?.Length > 0 ? mimeTypes[0] : null;
}
internal void PlatformInit(FileBase file)
{
}
+
+ internal virtual Task PlatformOpenReadAsync() =>
+ Task.FromResult((Stream)File.OpenRead(FullPath));
}
}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.netstandard.cs b/Xamarin.Essentials/FileSystem/FileSystem.netstandard.cs
index 7e889a501..886192c61 100644
--- a/Xamarin.Essentials/FileSystem/FileSystem.netstandard.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.netstandard.cs
@@ -22,5 +22,8 @@ static string PlatformGetContentType(string extension) =>
internal void PlatformInit(FileBase file) =>
throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ internal virtual Task PlatformOpenReadAsync()
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
}
}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.shared.cs b/Xamarin.Essentials/FileSystem/FileSystem.shared.cs
index bd001dd2c..6e6c4c4ab 100644
--- a/Xamarin.Essentials/FileSystem/FileSystem.shared.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.shared.cs
@@ -22,14 +22,19 @@ public abstract partial class FileBase
string contentType;
+ // The caller must setup FullPath at least!!!
+ internal FileBase()
+ {
+ }
+
internal FileBase(string fullPath)
{
if (fullPath == null)
throw new ArgumentNullException(nameof(fullPath));
if (string.IsNullOrWhiteSpace(fullPath))
- throw new ArgumentException("The attachment file path cannot be an empty string.", nameof(fullPath));
+ throw new ArgumentException("The file path cannot be an empty string.", nameof(fullPath));
if (string.IsNullOrWhiteSpace(Path.GetFileName(fullPath)))
- throw new ArgumentException("The attachment file path must be a file path.", nameof(fullPath));
+ throw new ArgumentException("The file path must be a file path.", nameof(fullPath));
FullPath = fullPath;
}
@@ -38,6 +43,7 @@ public FileBase(FileBase file)
{
FullPath = file.FullPath;
ContentType = file.ContentType;
+ FileName = file.FileName;
PlatformInit(file);
}
@@ -48,7 +54,7 @@ internal FileBase(string fullPath, string contentType)
ContentType = contentType;
}
- public string FullPath { get; }
+ public string FullPath { get; internal set; }
public string ContentType
{
@@ -70,33 +76,33 @@ internal string GetContentType()
if (!string.IsNullOrWhiteSpace(content))
return content;
}
-
- // we haven't been able to determine this
- // leave it up to the sender
- return null;
+ return "application/octet-stream";
}
- string attachmentName;
+ string fileName;
- internal string AttachmentName
+ public string FileName
{
- get => GetAttachmentName();
- set => attachmentName = value;
+ get => GetFileName();
+ set => fileName = value;
}
- internal string GetAttachmentName()
+ internal string GetFileName()
{
// try the provided file name
- if (!string.IsNullOrWhiteSpace(attachmentName))
- return attachmentName;
+ if (!string.IsNullOrWhiteSpace(fileName))
+ return fileName;
// try get from the path
if (!string.IsNullOrWhiteSpace(FullPath))
return Path.GetFileName(FullPath);
// this should never happen as the path is validated in the constructor
- throw new InvalidOperationException($"Unable to determine the attachment file name from '{FullPath}'.");
+ throw new InvalidOperationException($"Unable to determine the file name from '{FullPath}'.");
}
+
+ public Task OpenReadAsync()
+ => PlatformOpenReadAsync();
}
public class ReadOnlyFile : FileBase
@@ -116,4 +122,27 @@ public ReadOnlyFile(FileBase file)
{
}
}
+
+ public partial class FileResult : FileBase
+ {
+ // The caller must setup FullPath at least!!!
+ internal FileResult()
+ {
+ }
+
+ public FileResult(string fullPath)
+ : base(fullPath)
+ {
+ }
+
+ public FileResult(string fullPath, string contentType)
+ : base(fullPath, contentType)
+ {
+ }
+
+ public FileResult(FileBase file)
+ : base(file)
+ {
+ }
+ }
}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.tizen.cs b/Xamarin.Essentials/FileSystem/FileSystem.tizen.cs
index 05dd6b8e7..b24ee15ef 100755
--- a/Xamarin.Essentials/FileSystem/FileSystem.tizen.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.tizen.cs
@@ -34,5 +34,13 @@ static string PlatformGetContentType(string extension)
internal void PlatformInit(FileBase file)
{
}
+
+ internal virtual async Task PlatformOpenReadAsync()
+ {
+ await Permissions.RequestAsync();
+
+ var stream = File.Open(FullPath, FileMode.Open, FileAccess.Read);
+ return Task.FromResult(stream).Result;
+ }
}
}
diff --git a/Xamarin.Essentials/FileSystem/FileSystem.uwp.cs b/Xamarin.Essentials/FileSystem/FileSystem.uwp.cs
index c7c8666ff..12a29b765 100644
--- a/Xamarin.Essentials/FileSystem/FileSystem.uwp.cs
+++ b/Xamarin.Essentials/FileSystem/FileSystem.uwp.cs
@@ -44,5 +44,16 @@ internal void PlatformInit(FileBase file)
// we can't do anything here, but Windows will take care of it
internal static string PlatformGetContentType(string extension) => null;
+
+ internal virtual Task PlatformOpenReadAsync() =>
+ File.OpenStreamForReadAsync();
+ }
+
+ public partial class FileResult
+ {
+ internal FileResult(IStorageFile file)
+ : base(file)
+ {
+ }
}
}
diff --git a/Xamarin.Essentials/Flashlight/Flashlight.netstandard.tvos.watchos.cs b/Xamarin.Essentials/Flashlight/Flashlight.netstandard.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Flashlight/Flashlight.netstandard.tvos.watchos.cs
rename to Xamarin.Essentials/Flashlight/Flashlight.netstandard.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Geocoding/Geocoding.ios.tvos.watchos.cs b/Xamarin.Essentials/Geocoding/Geocoding.ios.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Geocoding/Geocoding.ios.tvos.watchos.cs
rename to Xamarin.Essentials/Geocoding/Geocoding.ios.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Geolocation/Geolocation.ios.cs b/Xamarin.Essentials/Geolocation/Geolocation.ios.macos.cs
similarity index 96%
rename from Xamarin.Essentials/Geolocation/Geolocation.ios.cs
rename to Xamarin.Essentials/Geolocation/Geolocation.ios.macos.cs
index f48b8a816..d9bb0d311 100644
--- a/Xamarin.Essentials/Geolocation/Geolocation.ios.cs
+++ b/Xamarin.Essentials/Geolocation/Geolocation.ios.macos.cs
@@ -31,8 +31,7 @@ static async Task PlatformLocationAsync(GeolocationRequest request, Ca
// the location manager requires an active run loop
// so just use the main loop
- CLLocationManager manager = null;
- NSRunLoop.Main.InvokeOnMainThread(() => manager = new CLLocationManager());
+ var manager = MainThread.InvokeOnMainThread(() => new CLLocationManager());
var tcs = new TaskCompletionSource(manager);
diff --git a/Xamarin.Essentials/Geolocation/GeolocationRequest.ios.cs b/Xamarin.Essentials/Geolocation/GeolocationRequest.ios.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Geolocation/GeolocationRequest.ios.cs
rename to Xamarin.Essentials/Geolocation/GeolocationRequest.ios.macos.cs
diff --git a/Xamarin.Essentials/GlobalSuppressions.shared.cs b/Xamarin.Essentials/GlobalSuppressions.shared.cs
index 2fea182d0..194a41f5f 100644
--- a/Xamarin.Essentials/GlobalSuppressions.shared.cs
+++ b/Xamarin.Essentials/GlobalSuppressions.shared.cs
@@ -4,9 +4,12 @@
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "iOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.iOS")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "macOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.macOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "tvOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.tvOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "watchOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.watchOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "iOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.iOS")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "macOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.macOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "tvOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.tvOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "watchOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.watchOS")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1008:Opening parenthesis should be spaced correctly", Justification = "Clashed with rule 1003", Scope = "member", Target = "~M:Xamarin.Essentials.SmsMessage.#ctor(System.String,System.String)")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "macOS is what we want.", Scope = "member", Target = "~P:Xamarin.Essentials.DevicePlatform.macOS")]
diff --git a/Xamarin.Essentials/Gyroscope/Gyroscope.netstandard.tvos.cs b/Xamarin.Essentials/Gyroscope/Gyroscope.netstandard.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Gyroscope/Gyroscope.netstandard.tvos.cs
rename to Xamarin.Essentials/Gyroscope/Gyroscope.netstandard.tvos.macos.cs
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.android.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.android.cs
new file mode 100644
index 000000000..0e663ebcc
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.android.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Android.Views;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ internal static bool IsSupported => true;
+
+ static void PlatformPerform(HapticFeedbackType type)
+ {
+ Permissions.EnsureDeclared();
+
+ try
+ {
+ Platform.CurrentActivity?.Window?.DecorView?.PerformHapticFeedback(ConvertType(type));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"HapticFeedback Exception: {ex.Message}");
+ }
+ }
+
+ static FeedbackConstants ConvertType(HapticFeedbackType type) =>
+ type switch
+ {
+ HapticFeedbackType.LongPress => FeedbackConstants.LongPress,
+ _ => FeedbackConstants.ContextClick
+ };
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.ios.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.ios.cs
new file mode 100644
index 000000000..9ee893594
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.ios.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Threading.Tasks;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ internal static bool IsSupported => true;
+
+ static void PlatformPerform(HapticFeedbackType type)
+ {
+ switch (type)
+ {
+ case HapticFeedbackType.LongPress:
+ PlatformLongPress();
+ break;
+ default:
+ PlatformClick();
+ break;
+ }
+ }
+
+ static void PlatformClick()
+ {
+ if (Platform.HasOSVersion(10, 0))
+ {
+ var impact = new UIImpactFeedbackGenerator(UIImpactFeedbackStyle.Light);
+ impact.Prepare();
+ impact.ImpactOccurred();
+ impact.Dispose();
+ }
+ }
+
+ static void PlatformLongPress()
+ {
+ if (Platform.HasOSVersion(10, 0))
+ {
+ var impact = new UIImpactFeedbackGenerator(UIImpactFeedbackStyle.Medium);
+ impact.Prepare();
+ impact.ImpactOccurred();
+ impact.Dispose();
+ }
+ }
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.macos.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.macos.cs
new file mode 100644
index 000000000..95ee508bf
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.macos.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+using AppKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ internal static bool IsSupported => true;
+
+ static void PlatformPerform(HapticFeedbackType type)
+ {
+ if (type == HapticFeedbackType.LongPress)
+ NSHapticFeedbackManager.DefaultPerformer.PerformFeedback(NSHapticFeedbackPattern.Generic, NSHapticFeedbackPerformanceTime.Default);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.netstandard.tvos.watchos.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.netstandard.tvos.watchos.cs
new file mode 100644
index 000000000..1fd5f566c
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.netstandard.tvos.watchos.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ internal static bool IsSupported
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static void PlatformPerform(HapticFeedbackType type)
+ => throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.shared.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.shared.cs
new file mode 100644
index 000000000..afca5f261
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.shared.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ public static void Perform(HapticFeedbackType type = HapticFeedbackType.Click)
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+ PlatformPerform(type);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.tizen.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.tizen.cs
new file mode 100644
index 000000000..4a2d8ee28
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.tizen.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Diagnostics;
+using Tizen.System;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ internal static bool IsSupported => true;
+
+ static void PlatformPerform(HapticFeedbackType type)
+ {
+ Permissions.EnsureDeclared();
+ try
+ {
+ var feedback = new Feedback();
+ var pattern = ConvertType(type);
+ if (feedback.IsSupportedPattern(FeedbackType.Vibration, pattern))
+ feedback.Play(FeedbackType.Vibration, pattern);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"HapticFeedback Exception: {ex.Message}");
+ }
+ }
+
+ static string ConvertType(HapticFeedbackType type) =>
+ type switch
+ {
+ HapticFeedbackType.LongPress => "Hold",
+ _ => "Tap"
+ };
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedback.uwp.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedback.uwp.cs
new file mode 100644
index 000000000..716953475
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedback.uwp.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Windows.Devices.Haptics;
+
+namespace Xamarin.Essentials
+{
+ public static partial class HapticFeedback
+ {
+ const string vibrationDeviceApiType = "Windows.Devices.Haptics.VibrationDevice";
+
+ internal static bool IsSupported => true;
+
+ static async void PlatformPerform(HapticFeedbackType type)
+ {
+ try
+ {
+ if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent(vibrationDeviceApiType)
+ && await VibrationDevice.RequestAccessAsync() == VibrationAccessStatus.Allowed)
+ {
+ var controller = (await VibrationDevice.GetDefaultAsync())?.SimpleHapticsController;
+
+ if (controller != null)
+ {
+ var feedback = FindFeedback(controller, ConvertType(type));
+ if (feedback != null)
+ controller.SendHapticFeedback(feedback);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"HapticFeedback Exception: {ex.Message}");
+ }
+ }
+
+ static SimpleHapticsControllerFeedback FindFeedback(SimpleHapticsController controller, ushort type)
+ {
+ foreach (var feedback in controller.SupportedFeedback)
+ {
+ if (feedback.Waveform == type)
+ return feedback;
+ }
+ return null;
+ }
+
+ static ushort ConvertType(HapticFeedbackType type) =>
+ type switch
+ {
+ HapticFeedbackType.LongPress => KnownSimpleHapticsControllerWaveforms.Press,
+ _ => KnownSimpleHapticsControllerWaveforms.Click
+ };
+ }
+}
diff --git a/Xamarin.Essentials/HapticFeedback/HapticFeedbackType.shared.cs b/Xamarin.Essentials/HapticFeedback/HapticFeedbackType.shared.cs
new file mode 100644
index 000000000..23a65b341
--- /dev/null
+++ b/Xamarin.Essentials/HapticFeedback/HapticFeedbackType.shared.cs
@@ -0,0 +1,8 @@
+namespace Xamarin.Essentials
+{
+ public enum HapticFeedbackType
+ {
+ Click,
+ LongPress
+ }
+}
diff --git a/Xamarin.Essentials/Launcher/Launcher.android.cs b/Xamarin.Essentials/Launcher/Launcher.android.cs
index c2545223a..18cd10d0f 100644
--- a/Xamarin.Essentials/Launcher/Launcher.android.cs
+++ b/Xamarin.Essentials/Launcher/Launcher.android.cs
@@ -38,7 +38,7 @@ static Task PlatformOpenAsync(Uri uri)
static Task PlatformOpenAsync(OpenFileRequest request)
{
- var contentUri = Platform.GetShareableFileUri(request.File.FullPath);
+ var contentUri = Platform.GetShareableFileUri(request.File);
var intent = new Intent(Intent.ActionView);
intent.SetDataAndType(contentUri, request.File.ContentType);
diff --git a/Xamarin.Essentials/Launcher/Launcher.ios.tvos.cs b/Xamarin.Essentials/Launcher/Launcher.ios.tvos.cs
index 0abd4b8d2..5f81bde71 100644
--- a/Xamarin.Essentials/Launcher/Launcher.ios.tvos.cs
+++ b/Xamarin.Essentials/Launcher/Launcher.ios.tvos.cs
@@ -44,11 +44,21 @@ internal static NSUrl GetNativeUrl(Uri uri)
}
#if __IOS__
+ static UIDocumentInteractionController documentController;
+
static Task PlatformOpenAsync(OpenFileRequest request)
{
var fileUrl = NSUrl.FromFilename(request.File.FullPath);
- var documentController = UIDocumentInteractionController.FromUrl(fileUrl);
+ documentController = UIDocumentInteractionController.FromUrl(fileUrl);
+ documentController.Delegate = new DocumentControllerDelegate
+ {
+ DismissHandler = () =>
+ {
+ documentController?.Dispose();
+ documentController = null;
+ }
+ };
documentController.Uti = request.File.ContentType;
var vc = Platform.GetCurrentViewController();
@@ -67,6 +77,14 @@ static Task PlatformOpenAsync(OpenFileRequest request)
return Task.CompletedTask;
}
+
+ class DocumentControllerDelegate : UIDocumentInteractionControllerDelegate
+ {
+ public Action DismissHandler { get; set; }
+
+ public override void DidDismissOpenInMenu(UIDocumentInteractionController controller)
+ => DismissHandler?.Invoke();
+ }
#else
static Task PlatformOpenAsync(OpenFileRequest request) =>
throw new FeatureNotSupportedException();
diff --git a/Xamarin.Essentials/Launcher/Launcher.macos.cs b/Xamarin.Essentials/Launcher/Launcher.macos.cs
new file mode 100644
index 000000000..2dfac52f0
--- /dev/null
+++ b/Xamarin.Essentials/Launcher/Launcher.macos.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Launcher
+ {
+ static Task PlatformCanOpenAsync(Uri uri) =>
+ Task.FromResult(NSWorkspace.SharedWorkspace.UrlForApplication(GetNativeUrl(uri)) != null);
+
+ static Task PlatformOpenAsync(Uri uri) =>
+ Task.FromResult(NSWorkspace.SharedWorkspace.OpenUrl(GetNativeUrl(uri)));
+
+ static Task PlatformTryOpenAsync(Uri uri)
+ {
+ var nativeUrl = GetNativeUrl(uri);
+ var canOpen = NSWorkspace.SharedWorkspace.UrlForApplication(nativeUrl) != null;
+
+ if (canOpen)
+ return Task.FromResult(NSWorkspace.SharedWorkspace.OpenUrl(nativeUrl));
+
+ return Task.FromResult(canOpen);
+ }
+
+ internal static NSUrl GetNativeUrl(Uri uri)
+ {
+ try
+ {
+ return new NSUrl(uri.OriginalString);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("Unable to create Url from Original string, try absolute Uri: " + ex.Message);
+ return new NSUrl(uri.AbsoluteUri);
+ }
+ }
+
+ static Task PlatformOpenAsync(OpenFileRequest request) =>
+ Task.FromResult(NSWorkspace.SharedWorkspace.OpenFile(request.File.FullPath));
+ }
+}
diff --git a/Xamarin.Essentials/Launcher/Launcher.shared.cs b/Xamarin.Essentials/Launcher/Launcher.shared.cs
index 3ad9a6b39..6fab395f2 100644
--- a/Xamarin.Essentials/Launcher/Launcher.shared.cs
+++ b/Xamarin.Essentials/Launcher/Launcher.shared.cs
@@ -40,6 +40,11 @@ public static Task OpenAsync(Uri uri)
public static Task OpenAsync(OpenFileRequest request)
{
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+ if (request.File == null)
+ throw new ArgumentNullException(nameof(request.File));
+
return PlatformOpenAsync(request);
}
diff --git a/Xamarin.Essentials/Magnetometer/Magnetometer.netstandard.tvos.cs b/Xamarin.Essentials/Magnetometer/Magnetometer.netstandard.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Magnetometer/Magnetometer.netstandard.tvos.cs
rename to Xamarin.Essentials/Magnetometer/Magnetometer.netstandard.tvos.macos.cs
diff --git a/Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.cs b/Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.macos.cs
similarity index 62%
rename from Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.cs
rename to Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.macos.cs
index 5df45c3a0..03600aad5 100644
--- a/Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.cs
+++ b/Xamarin.Essentials/MainThread/MainThread.ios.tvos.watchos.macos.cs
@@ -12,5 +12,12 @@ static void PlatformBeginInvokeOnMainThread(Action action)
{
NSRunLoop.Main.BeginInvokeOnMainThread(action.Invoke);
}
+
+ internal static T InvokeOnMainThread(Func factory)
+ {
+ T value = default;
+ NSRunLoop.Main.InvokeOnMainThread(() => value = factory());
+ return value;
+ }
}
}
diff --git a/Xamarin.Essentials/Map/Map.ios.watchos.cs b/Xamarin.Essentials/Map/Map.ios.watchos.macos.cs
similarity index 81%
rename from Xamarin.Essentials/Map/Map.ios.watchos.cs
rename to Xamarin.Essentials/Map/Map.ios.watchos.macos.cs
index 6b37cec45..f559869fd 100644
--- a/Xamarin.Essentials/Map/Map.ios.watchos.cs
+++ b/Xamarin.Essentials/Map/Map.ios.watchos.macos.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
+using Contacts;
using CoreLocation;
using Foundation;
using MapKit;
@@ -32,19 +33,19 @@ internal static async Task PlatformOpenMapsAsync(Placemark placemark, MapLaunchO
Zip = placemark.PostalCode
}.Dictionary;
#else
- var address = new NSDictionary
+ var address = new NSMutableDictionary
{
- [Contacts.CNPostalAddressKey.City] = new NSString(placemark.Locality),
- [Contacts.CNPostalAddressKey.Country] = new NSString(placemark.CountryName),
- [Contacts.CNPostalAddressKey.State] = new NSString(placemark.AdminArea),
- [Contacts.CNPostalAddressKey.Street] = new NSString(placemark.Thoroughfare),
- [Contacts.CNPostalAddressKey.PostalCode] = new NSString(placemark.PostalCode),
- [Contacts.CNPostalAddressKey.IsoCountryCode] = new NSString(placemark.CountryCode)
+ [CNPostalAddressKey.City] = new NSString(placemark.Locality ?? string.Empty),
+ [CNPostalAddressKey.Country] = new NSString(placemark.CountryName ?? string.Empty),
+ [CNPostalAddressKey.State] = new NSString(placemark.AdminArea ?? string.Empty),
+ [CNPostalAddressKey.Street] = new NSString(placemark.Thoroughfare ?? string.Empty),
+ [CNPostalAddressKey.PostalCode] = new NSString(placemark.PostalCode ?? string.Empty),
+ [CNPostalAddressKey.IsoCountryCode] = new NSString(placemark.CountryCode ?? string.Empty)
};
#endif
var coder = new CLGeocoder();
- CLPlacemark[] placemarks = null;
+ CLPlacemark[] placemarks;
try
{
placemarks = await coder.GeocodeAddressAsync(address);
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs
new file mode 100644
index 000000000..33f70f6be
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content;
+using Android.Content.PM;
+using Android.Provider;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static bool PlatformIsCaptureSupported
+ => Platform.AppContext.PackageManager.HasSystemFeature(PackageManager.FeatureCameraAny);
+
+ static Task PlatformPickPhotoAsync(MediaPickerOptions options)
+ => PlatformPickAsync(options, true);
+
+ static Task PlatformPickVideoAsync(MediaPickerOptions options)
+ => PlatformPickAsync(options, false);
+
+ static async Task PlatformPickAsync(MediaPickerOptions options, bool photo)
+ {
+ // we only need the permission when accessing the file, but it's more natural
+ // to ask the user first, then show the picker.
+ await Permissions.RequestAsync();
+
+ var intent = new Intent(Intent.ActionGetContent);
+ intent.SetType(photo ? "image/*" : "video/*");
+
+ var pickerIntent = Intent.CreateChooser(intent, options?.Title);
+
+ try
+ {
+ var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeMediaPicker);
+
+ return new FileResult(result.Data);
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ }
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options)
+ => PlatformCaptureAsync(options, true);
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options)
+ => PlatformCaptureAsync(options, false);
+
+ static async Task PlatformCaptureAsync(MediaPickerOptions options, bool photo)
+ {
+ await Permissions.EnsureGrantedAsync();
+ await Permissions.EnsureGrantedAsync();
+
+ var capturePhotoIntent = new Intent(photo ? MediaStore.ActionImageCapture : MediaStore.ActionVideoCapture);
+ if (capturePhotoIntent.ResolveActivity(Platform.AppContext.PackageManager) != null)
+ {
+ try
+ {
+ var activity = Platform.GetCurrentActivity(true);
+
+ var storageDir = Platform.AppContext.ExternalCacheDir;
+ var tmpFile = Java.IO.File.CreateTempFile(Guid.NewGuid().ToString(), photo ? ".jpg" : ".mp4", storageDir);
+ tmpFile.DeleteOnExit();
+
+ capturePhotoIntent.AddFlags(ActivityFlags.GrantReadUriPermission);
+ capturePhotoIntent.AddFlags(ActivityFlags.GrantWriteUriPermission);
+
+ var result = await IntermediateActivity.StartAsync(capturePhotoIntent, Platform.requestCodeMediaCapture, tmpFile);
+
+ var outputUri = result.GetParcelableExtra(IntermediateActivity.OutputUriExtra) as global::Android.Net.Uri;
+
+ return new FileResult(outputUri);
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs
new file mode 100644
index 000000000..4420dba4c
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.ios.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Foundation;
+using MobileCoreServices;
+using Photos;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static UIImagePickerController picker;
+
+ static bool PlatformIsCaptureSupported
+ => UIImagePickerController.IsSourceTypeAvailable(UIImagePickerControllerSourceType.Camera);
+
+ static Task PlatformPickPhotoAsync(MediaPickerOptions options)
+ => PhotoAsync(options, true, true);
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options)
+ => PhotoAsync(options, true, false);
+
+ static Task PlatformPickVideoAsync(MediaPickerOptions options)
+ => PhotoAsync(options, false, true);
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options)
+ => PhotoAsync(options, false, false);
+
+ static async Task PhotoAsync(MediaPickerOptions options, bool photo, bool pickExisting)
+ {
+ var sourceType = pickExisting ? UIImagePickerControllerSourceType.PhotoLibrary : UIImagePickerControllerSourceType.Camera;
+ var mediaType = photo ? UTType.Image : UTType.Movie;
+
+ if (!UIImagePickerController.IsSourceTypeAvailable(sourceType))
+ throw new FeatureNotSupportedException();
+ if (!UIImagePickerController.AvailableMediaTypes(sourceType).Contains(mediaType))
+ throw new FeatureNotSupportedException();
+
+ if (!photo)
+ await Permissions.EnsureGrantedAsync();
+
+ // permission is not required on iOS 11 for the picker
+ if (!Platform.HasOSVersion(11, 0))
+ {
+ await Permissions.EnsureGrantedAsync();
+ }
+
+ var vc = Platform.GetCurrentViewController(true);
+
+ picker = new UIImagePickerController();
+ picker.SourceType = sourceType;
+ picker.MediaTypes = new string[] { mediaType };
+ picker.AllowsEditing = false;
+ if (!photo && !pickExisting)
+ picker.CameraCaptureMode = UIImagePickerControllerCameraCaptureMode.Video;
+
+ if (!string.IsNullOrWhiteSpace(options?.Title))
+ picker.Title = options.Title;
+
+ if (DeviceInfo.Idiom == DeviceIdiom.Tablet && picker.PopoverPresentationController != null && vc.View != null)
+ picker.PopoverPresentationController.SourceRect = vc.View.Bounds;
+
+ var tcs = new TaskCompletionSource(picker);
+ picker.Delegate = new PhotoPickerDelegate
+ {
+ CompletedHandler = info =>
+ tcs.TrySetResult(DictionaryToMediaFile(info))
+ };
+
+ await vc.PresentViewControllerAsync(picker, true);
+
+ var result = await tcs.Task;
+
+ await vc.DismissViewControllerAsync(true);
+
+ picker?.Dispose();
+ picker = null;
+
+ return result;
+ }
+
+ static FileResult DictionaryToMediaFile(NSDictionary info)
+ {
+ if (info == null)
+ return null;
+
+ PHAsset phAsset = null;
+ NSUrl assetUrl = null;
+
+ if (Platform.HasOSVersion(11, 0))
+ {
+ assetUrl = info[UIImagePickerController.ImageUrl] as NSUrl;
+
+ // Try the MediaURL sometimes used for videos
+ if (assetUrl == null)
+ assetUrl = info[UIImagePickerController.MediaURL] as NSUrl;
+
+ if (assetUrl != null)
+ {
+ if (!assetUrl.Scheme.Equals("assets-library", StringComparison.InvariantCultureIgnoreCase))
+ return new UIDocumentFileResult(assetUrl);
+
+ phAsset = info.ValueForKey(UIImagePickerController.PHAsset) as PHAsset;
+ }
+ }
+
+ if (phAsset == null)
+ {
+ assetUrl = info[UIImagePickerController.ReferenceUrl] as NSUrl;
+
+ if (assetUrl != null)
+ phAsset = PHAsset.FetchAssets(new NSUrl[] { assetUrl }, null)?.LastObject as PHAsset;
+ }
+
+ if (phAsset == null || assetUrl == null)
+ {
+ var img = info.ValueForKey(UIImagePickerController.OriginalImage) as UIImage;
+
+ if (img != null)
+ return new UIImageFileResult(img);
+ }
+
+ if (phAsset == null || assetUrl == null)
+ return null;
+
+ string originalFilename;
+
+ if (Platform.HasOSVersion(9, 0))
+ originalFilename = PHAssetResource.GetAssetResources(phAsset).FirstOrDefault()?.OriginalFilename;
+ else
+ originalFilename = phAsset.ValueForKey(new NSString("filename")) as NSString;
+
+ return new PHAssetFileResult(assetUrl, phAsset, originalFilename);
+ }
+
+ class PhotoPickerDelegate : UIImagePickerControllerDelegate
+ {
+ public Action CompletedHandler { get; set; }
+
+ public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info) =>
+ CompletedHandler?.Invoke(info);
+
+ public override void Canceled(UIImagePickerController picker) =>
+ CompletedHandler?.Invoke(null);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.macos.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.macos.cs
new file mode 100644
index 000000000..c3ea462c8
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.macos.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static bool PlatformIsCaptureSupported
+ => false;
+
+ static async Task PlatformPickPhotoAsync(MediaPickerOptions options)
+ => new FileResult(await FilePicker.PickAsync(new PickOptions
+ {
+ FileTypes = FilePickerFileType.Images
+ }));
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options)
+ => PlatformPickPhotoAsync(options);
+
+ static async Task PlatformPickVideoAsync(MediaPickerOptions options)
+ => new FileResult(await FilePicker.PickAsync(new PickOptions
+ {
+ FileTypes = FilePickerFileType.Videos
+ }));
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options)
+ => PlatformPickVideoAsync(options);
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.netstandard.watchos.tvos.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.netstandard.watchos.tvos.cs
new file mode 100644
index 000000000..2e8f85dca
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.netstandard.watchos.tvos.cs
@@ -0,0 +1,24 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static bool PlatformIsCaptureSupported =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static Task PlatformPickPhotoAsync(MediaPickerOptions options) =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options) =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static Task PlatformPickVideoAsync(MediaPickerOptions options) =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options) =>
+ throw new NotImplementedInReferenceAssemblyException();
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.shared.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.shared.cs
new file mode 100644
index 000000000..b6565fc96
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.shared.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ public static bool IsCaptureSupported
+ => PlatformIsCaptureSupported;
+
+ public static Task PickPhotoAsync(MediaPickerOptions options = null) =>
+ PlatformPickPhotoAsync(options);
+
+ public static Task CapturePhotoAsync(MediaPickerOptions options = null)
+ {
+ if (!IsCaptureSupported)
+ throw new FeatureNotSupportedException();
+
+ return PlatformCapturePhotoAsync(options);
+ }
+
+ public static Task PickVideoAsync(MediaPickerOptions options = null) =>
+ PlatformPickVideoAsync(options);
+
+ public static Task CaptureVideoAsync(MediaPickerOptions options = null)
+ {
+ if (!IsCaptureSupported)
+ throw new FeatureNotSupportedException();
+
+ return PlatformCaptureVideoAsync(options);
+ }
+ }
+
+ public class MediaPickerOptions
+ {
+ public string Title { get; set; }
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.tizen.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.tizen.cs
new file mode 100644
index 000000000..d1291e0d2
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.tizen.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Applications;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static bool PlatformIsCaptureSupported
+ => true;
+
+ static async Task PlatformPickPhotoAsync(MediaPickerOptions options)
+ => new FileResult(await FilePicker.PickAsync(new PickOptions
+ {
+ FileTypes = FilePickerFileType.Images
+ }));
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options)
+ => PlatformMediaAsync(options, true);
+
+ static async Task PlatformPickVideoAsync(MediaPickerOptions options)
+ => new FileResult(await FilePicker.PickAsync(new PickOptions
+ {
+ FileTypes = FilePickerFileType.Videos
+ }));
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options)
+ => PlatformMediaAsync(options, false);
+
+ static async Task PlatformMediaAsync(MediaPickerOptions options, bool photo)
+ {
+ Permissions.EnsureDeclared();
+
+ await Permissions.EnsureGrantedAsync();
+
+ var tcs = new TaskCompletionSource();
+
+ var appControl = new AppControl();
+ appControl.Operation = photo ? AppControlOperations.ImageCapture : AppControlOperations.VideoCapture;
+ appControl.LaunchMode = AppControlLaunchMode.Group;
+
+ var appId = AppControl.GetMatchedApplicationIds(appControl)?.FirstOrDefault();
+
+ if (!string.IsNullOrEmpty(appId))
+ appControl.ApplicationId = appId;
+
+ AppControl.SendLaunchRequest(appControl, (request, reply, result) =>
+ {
+ if (result == AppControlReplyResult.Succeeded && reply.ExtraData.Count() > 0)
+ {
+ var file = reply.ExtraData.Get>(AppControlData.Selected)?.FirstOrDefault();
+ tcs.TrySetResult(new FileResult(file));
+ }
+ else
+ {
+ tcs.TrySetCanceled();
+ }
+ });
+
+ return await tcs.Task;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.uwp.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.uwp.cs
new file mode 100644
index 000000000..32239984e
--- /dev/null
+++ b/Xamarin.Essentials/MediaPicker/MediaPicker.uwp.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Windows.Media.Capture;
+using Windows.Storage.Pickers;
+
+namespace Xamarin.Essentials
+{
+ public static partial class MediaPicker
+ {
+ static bool PlatformIsCaptureSupported
+ => true;
+
+ static Task PlatformPickPhotoAsync(MediaPickerOptions options)
+ => PickAsync(options, true);
+
+ static Task PlatformPickVideoAsync(MediaPickerOptions options)
+ => PickAsync(options, false);
+
+ static async Task PickAsync(MediaPickerOptions options, bool photo)
+ {
+ var picker = new FileOpenPicker();
+
+ var defaultTypes = photo ? FilePickerFileType.Images.Value : FilePickerFileType.Videos.Value;
+
+ // set picker properties
+ foreach (var filter in defaultTypes.Select(t => t.TrimStart('*')))
+ picker.FileTypeFilter.Add(filter);
+ picker.SuggestedStartLocation = photo ? PickerLocationId.PicturesLibrary : PickerLocationId.VideosLibrary;
+ picker.ViewMode = PickerViewMode.Thumbnail;
+
+ // show the picker
+ var result = await picker.PickSingleFileAsync();
+
+ // cancelled
+ if (result == null)
+ return null;
+
+ // picked
+ return new FileResult(result);
+ }
+
+ static Task PlatformCapturePhotoAsync(MediaPickerOptions options)
+ => CaptureAsync(options, false);
+
+ static Task PlatformCaptureVideoAsync(MediaPickerOptions options)
+ => CaptureAsync(options, false);
+
+ static async Task CaptureAsync(MediaPickerOptions options, bool photo)
+ {
+ var captureUi = new CameraCaptureUI();
+
+ if (photo)
+ captureUi.PhotoSettings.Format = CameraCaptureUIPhotoFormat.Jpeg;
+ else
+ captureUi.VideoSettings.Format = CameraCaptureUIVideoFormat.Mp4;
+
+ var file = await captureUi.CaptureFileAsync(photo ? CameraCaptureUIMode.Photo : CameraCaptureUIMode.Video);
+
+ if (file != null)
+ return new FileResult(file);
+
+ return null;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.tvos.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.tvos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.tvos.cs
rename to Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.tvos.macos.cs
diff --git a/Xamarin.Essentials/Permissions/Permissions.android.cs b/Xamarin.Essentials/Permissions/Permissions.android.cs
index cbed73cc0..f8afa8f69 100644
--- a/Xamarin.Essentials/Permissions/Permissions.android.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.android.cs
@@ -35,7 +35,7 @@ public abstract partial class BasePlatformPermission : BasePermission
new Dictionary)>();
static readonly object locker = new object();
- static int requestCode = 0;
+ static int requestCode;
public virtual (string androidPermission, bool isRuntime)[] RequiredPermissions { get; }
@@ -103,9 +103,7 @@ public override async Task RequestAsync()
{
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;
+ requestCode = Platform.NextRequestCode();
requests.Add(permissionId, (requestCode, tcs));
}
@@ -129,6 +127,9 @@ public override async Task RequestAsync()
public override void EnsureDeclared()
{
+ if (RequiredPermissions == null || RequiredPermissions.Length <= 0)
+ return;
+
foreach (var (androidPermission, isRuntime) in RequiredPermissions)
{
var ap = androidPermission;
@@ -137,6 +138,21 @@ public override void EnsureDeclared()
}
}
+ public override bool ShouldShowRationale()
+ {
+ if (RequiredPermissions == null || RequiredPermissions.Length <= 0)
+ return false;
+
+ var activity = Platform.GetCurrentActivity(true);
+ foreach (var (androidPermission, isRuntime) in RequiredPermissions)
+ {
+ if (isRuntime && ActivityCompat.ShouldShowRequestPermissionRationale(activity, androidPermission))
+ return true;
+ }
+
+ return false;
+ }
+
internal static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
lock (locker)
@@ -301,6 +317,14 @@ public override (string androidPermission, bool isRuntime)[] RequiredPermissions
if (IsDeclaredInManifest(Manifest.Permission.UseSip))
permissions.Add((Manifest.Permission.UseSip, true));
+#if __ANDROID_26__
+ if (Platform.HasApiLevelO)
+ {
+ if (IsDeclaredInManifest(Manifest.Permission.AnswerPhoneCalls))
+ permissions.Add((Manifest.Permission.AnswerPhoneCalls, true));
+ }
+#endif
+
#pragma warning disable CS0618 // Type or member is obsolete
if (IsDeclaredInManifest(Manifest.Permission.ProcessOutgoingCalls))
{
diff --git a/Xamarin.Essentials/Permissions/Permissions.ios.cs b/Xamarin.Essentials/Permissions/Permissions.ios.cs
index fc07dd12d..232ba3325 100644
--- a/Xamarin.Essentials/Permissions/Permissions.ios.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.ios.cs
@@ -103,14 +103,21 @@ internal static PermissionStatus GetAddressBookPermissionStatus()
};
}
+ static ABAddressBook addressBook;
+
internal static Task RequestAddressBookPermission()
{
- var addressBook = new ABAddressBook();
+ addressBook = new ABAddressBook();
var tcs = new TaskCompletionSource();
addressBook.RequestAccess((success, error) =>
- tcs.TrySetResult(success ? PermissionStatus.Granted : PermissionStatus.Denied));
+ {
+ tcs.TrySetResult(success ? PermissionStatus.Granted : PermissionStatus.Denied);
+
+ addressBook?.Dispose();
+ addressBook = null;
+ });
return tcs.Task;
}
diff --git a/Xamarin.Essentials/Permissions/Permissions.ios.tvos.watchos.cs b/Xamarin.Essentials/Permissions/Permissions.ios.tvos.watchos.cs
index 8de3d1582..244c3f307 100644
--- a/Xamarin.Essentials/Permissions/Permissions.ios.tvos.watchos.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.ios.tvos.watchos.cs
@@ -41,6 +41,8 @@ public override void EnsureDeclared()
}
}
+ public override bool ShouldShowRationale() => false;
+
internal void EnsureMainThread()
{
if (!MainThread.IsMainThread)
@@ -235,6 +237,10 @@ public partial class Sensors : BasePlatformPermission
{
}
+ public partial class Speech : BasePlatformPermission
+ {
+ }
+
public partial class Sms : BasePlatformPermission
{
}
diff --git a/Xamarin.Essentials/Permissions/Permissions.macos.cs b/Xamarin.Essentials/Permissions/Permissions.macos.cs
new file mode 100644
index 000000000..8c05cef6c
--- /dev/null
+++ b/Xamarin.Essentials/Permissions/Permissions.macos.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using CoreLocation;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Permissions
+ {
+ public static bool IsKeyDeclaredInInfoPlist(string usageKey) =>
+ NSBundle.MainBundle.InfoDictionary.ContainsKey(new NSString(usageKey));
+
+ public static TimeSpan LocationTimeout { get; set; } = TimeSpan.FromSeconds(10);
+
+ public abstract class BasePlatformPermission : BasePermission
+ {
+ protected virtual Func> RecommendedInfoPlistKeys { get; }
+
+ protected virtual Func> RequiredInfoPlistKeys { get; }
+
+ public override Task CheckStatusAsync() =>
+ Task.FromResult(PermissionStatus.Granted);
+
+ public override Task RequestAsync() =>
+ Task.FromResult(PermissionStatus.Granted);
+
+ public override bool ShouldShowRationale() => false;
+
+ public override void EnsureDeclared()
+ {
+ var plistKeys = RequiredInfoPlistKeys?.Invoke();
+ if (plistKeys != null)
+ {
+ foreach (var requiredKey in plistKeys)
+ {
+ if (!IsKeyDeclaredInInfoPlist(requiredKey))
+ throw new PermissionException($"You must set `{requiredKey}` in your Info.plist file to use the Permission: {GetType().Name}.");
+ }
+ }
+
+ plistKeys = RecommendedInfoPlistKeys?.Invoke();
+ if (plistKeys != null)
+ {
+ foreach (var recommendedKey in plistKeys)
+ {
+ // NOTE: This is not a problem as macOS has a "default" message. But, this is still something
+ // the developer must do. We use a Console instead of a Debug because we always want to
+ // print this message.
+ if (!IsKeyDeclaredInInfoPlist(recommendedKey))
+ Console.WriteLine($"You must set `{recommendedKey}` in your Info.plist file to enable a user-friendly message for the Permission: {GetType().Name}.");
+ }
+ }
+ }
+
+ internal void EnsureMainThread()
+ {
+ if (!MainThread.IsMainThread)
+ throw new PermissionException("Permission request must be invoked on main thread.");
+ }
+ }
+
+ public partial class EventPermissions : BasePlatformPermission
+ {
+ }
+
+ public partial class Battery : BasePlatformPermission
+ {
+ }
+
+ public partial class CalendarRead : BasePlatformPermission
+ {
+ }
+
+ public partial class CalendarWrite : BasePlatformPermission
+ {
+ }
+
+ public partial class Camera : BasePlatformPermission
+ {
+ }
+
+ public partial class ContactsRead : BasePlatformPermission
+ {
+ }
+
+ public partial class ContactsWrite : BasePlatformPermission
+ {
+ }
+
+ public partial class Flashlight : BasePlatformPermission
+ {
+ }
+
+ public partial class LaunchApp : BasePlatformPermission
+ {
+ }
+
+ public partial class LocationWhenInUse : BasePlatformPermission
+ {
+ protected override Func> RecommendedInfoPlistKeys =>
+ () => new string[] { "NSLocationWhenInUseUsageDescription" };
+
+ public override Task CheckStatusAsync()
+ {
+ EnsureDeclared();
+
+ return Task.FromResult(GetLocationStatus());
+ }
+
+ public override async Task RequestAsync()
+ {
+ EnsureDeclared();
+
+ var status = GetLocationStatus();
+ if (status == PermissionStatus.Granted || status == PermissionStatus.Disabled)
+ return status;
+
+ EnsureMainThread();
+
+ return await RequestLocationAsync();
+ }
+
+ internal static PermissionStatus GetLocationStatus()
+ {
+ if (!CLLocationManager.LocationServicesEnabled)
+ return PermissionStatus.Disabled;
+
+ var status = CLLocationManager.Status;
+
+ return status switch
+ {
+ CLAuthorizationStatus.AuthorizedAlways => PermissionStatus.Granted,
+ CLAuthorizationStatus.AuthorizedWhenInUse => PermissionStatus.Granted,
+ CLAuthorizationStatus.Denied => PermissionStatus.Denied,
+ CLAuthorizationStatus.Restricted => PermissionStatus.Restricted,
+ _ => PermissionStatus.Unknown,
+ };
+ }
+
+ static CLLocationManager locationManager;
+
+ internal static Task RequestLocationAsync()
+ {
+ locationManager = new CLLocationManager();
+
+ var tcs = new TaskCompletionSource(locationManager);
+
+ var previousState = CLLocationManager.Status;
+
+ locationManager.AuthorizationChanged += LocationAuthCallback;
+ locationManager.StartUpdatingLocation();
+ locationManager.StopUpdatingLocation();
+
+ 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;
+ }
+ }
+ }
+
+ public partial class LocationAlways : BasePlatformPermission
+ {
+ }
+
+ public partial class Maps : BasePlatformPermission
+ {
+ }
+
+ public partial class Media : BasePlatformPermission
+ {
+ }
+
+ public partial class Microphone : BasePlatformPermission
+ {
+ }
+
+ public partial class NetworkState : BasePlatformPermission
+ {
+ }
+
+ public partial class Phone : BasePlatformPermission
+ {
+ }
+
+ public partial class Photos : BasePlatformPermission
+ {
+ }
+
+ public partial class Reminders : BasePlatformPermission
+ {
+ }
+
+ public partial class Sensors : BasePlatformPermission
+ {
+ }
+
+ public partial class Speech : BasePlatformPermission
+ {
+ }
+
+ public partial class Sms : BasePlatformPermission
+ {
+ }
+
+ public partial class StorageRead : BasePlatformPermission
+ {
+ }
+
+ public partial class StorageWrite : BasePlatformPermission
+ {
+ }
+
+ public partial class Vibrate : BasePlatformPermission
+ {
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Permissions/Permissions.netstandard.cs b/Xamarin.Essentials/Permissions/Permissions.netstandard.cs
index ece516d63..87dd4f87d 100644
--- a/Xamarin.Essentials/Permissions/Permissions.netstandard.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.netstandard.cs
@@ -14,6 +14,9 @@ public override Task RequestAsync() =>
public override void EnsureDeclared() =>
throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ public override bool ShouldShowRationale() =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
}
public partial class Battery : BasePlatformPermission
diff --git a/Xamarin.Essentials/Permissions/Permissions.shared.cs b/Xamarin.Essentials/Permissions/Permissions.shared.cs
index 959cfee4f..c828b6423 100644
--- a/Xamarin.Essentials/Permissions/Permissions.shared.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.shared.cs
@@ -12,6 +12,10 @@ public static Task RequestAsync()
where TPermission : BasePermission, new() =>
new TPermission().RequestAsync();
+ public static void ShouldShowRationale()
+ where TPermission : BasePermission, new() =>
+ new TPermission().ShouldShowRationale();
+
internal static void EnsureDeclared()
where TPermission : BasePermission, new() =>
new TPermission().EnsureDeclared();
@@ -37,6 +41,8 @@ public BasePermission()
public abstract Task RequestAsync();
public abstract void EnsureDeclared();
+
+ public abstract bool ShouldShowRationale();
}
public partial class Battery
diff --git a/Xamarin.Essentials/Permissions/Permissions.tizen.cs b/Xamarin.Essentials/Permissions/Permissions.tizen.cs
index 9bd7a8ba2..3c1a875ed 100644
--- a/Xamarin.Essentials/Permissions/Permissions.tizen.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.tizen.cs
@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using System.Linq;
+using System.Linq;
using System.Threading.Tasks;
using Tizen.Security;
@@ -34,7 +33,7 @@ public override Task RequestAsync()
async Task CheckPrivilegeAsync(bool ask)
{
- if (!RequiredPrivileges.Any())
+ if (RequiredPrivileges == null || !RequiredPrivileges.Any())
return PermissionStatus.Granted;
EnsureDeclared();
@@ -43,7 +42,8 @@ async Task CheckPrivilegeAsync(bool ask)
foreach (var (tizenPrivilege, isRuntime) in tizenPrivileges)
{
- if (PrivacyPrivilegeManager.CheckPermission(tizenPrivilege) == CheckResult.Ask)
+ var checkResult = PrivacyPrivilegeManager.CheckPermission(tizenPrivilege);
+ if (checkResult == CheckResult.Ask)
{
if (ask)
{
@@ -63,7 +63,7 @@ void OnResponseFetched(object sender, RequestResponseEventArgs e)
}
return PermissionStatus.Denied;
}
- else if (PrivacyPrivilegeManager.CheckPermission(tizenPrivilege) == CheckResult.Deny)
+ else if (checkResult == CheckResult.Deny)
{
return PermissionStatus.Denied;
}
@@ -73,12 +73,17 @@ void OnResponseFetched(object sender, RequestResponseEventArgs e)
public override void EnsureDeclared()
{
+ if (RequiredPrivileges == null)
+ return;
+
foreach (var (tizenPrivilege, isRuntime) in RequiredPrivileges)
{
if (!IsPrivilegeDeclared(tizenPrivilege))
throw new PermissionException($"You need to declare the privilege: `{tizenPrivilege}` in your tizen-manifest.xml");
}
}
+
+ public override bool ShouldShowRationale() => false;
}
public partial class Battery : BasePlatformPermission
@@ -101,10 +106,14 @@ public override (string tizenPrivilege, bool isRuntime)[] RequiredPrivileges =>
public partial class ContactsRead : BasePlatformPermission
{
+ public override (string tizenPrivilege, bool isRuntime)[] RequiredPrivileges =>
+ new[] { ("http://tizen.org/privilege/contact.read", true) };
}
public partial class ContactsWrite : BasePlatformPermission
{
+ public override (string tizenPrivilege, bool isRuntime)[] RequiredPrivileges =>
+ new[] { ("http://tizen.org/privilege/contact.write", true) };
}
public partial class Flashlight : BasePlatformPermission
@@ -188,10 +197,14 @@ public partial class Speech : BasePlatformPermission
public partial class StorageRead : BasePlatformPermission
{
+ public override (string tizenPrivilege, bool isRuntime)[] RequiredPrivileges =>
+ new[] { ("http://tizen.org/privilege/mediastorage", true) };
}
public partial class StorageWrite : BasePlatformPermission
{
+ public override (string tizenPrivilege, bool isRuntime)[] RequiredPrivileges =>
+ new[] { ("http://tizen.org/privilege/mediastorage", true) };
}
public partial class Vibrate : BasePlatformPermission
diff --git a/Xamarin.Essentials/Permissions/Permissions.uwp.cs b/Xamarin.Essentials/Permissions/Permissions.uwp.cs
index 5cd270a82..1f582be2d 100644
--- a/Xamarin.Essentials/Permissions/Permissions.uwp.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.uwp.cs
@@ -48,6 +48,8 @@ public override void EnsureDeclared()
throw new PermissionException($"You need to declare the capability `{d}` in your AppxManifest.xml file");
}
}
+
+ public override bool ShouldShowRationale() => false;
}
public partial class Battery : BasePlatformPermission
diff --git a/Xamarin.Essentials/PhoneDialer/PhoneDialer.macos.cs b/Xamarin.Essentials/PhoneDialer/PhoneDialer.macos.cs
new file mode 100644
index 000000000..b895a40b8
--- /dev/null
+++ b/Xamarin.Essentials/PhoneDialer/PhoneDialer.macos.cs
@@ -0,0 +1,19 @@
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class PhoneDialer
+ {
+ internal static bool IsSupported =>
+ MainThread.InvokeOnMainThread(() => NSWorkspace.SharedWorkspace.UrlForApplication(NSUrl.FromString($"tel:0000000000")) != null);
+
+ static void PlatformOpen(string number)
+ {
+ ValidateOpen(number);
+
+ var nsurl = NSUrl.FromString($"tel:{number}");
+ NSWorkspace.SharedWorkspace.OpenUrl(nsurl);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Platform/Platform.android.cs b/Xamarin.Essentials/Platform/Platform.android.cs
index 7a1e830de..8cb422c04 100644
--- a/Xamarin.Essentials/Platform/Platform.android.cs
+++ b/Xamarin.Essentials/Platform/Platform.android.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +12,10 @@
using Android.Net;
using Android.Net.Wifi;
using Android.OS;
+using Android.Provider;
+using Android.Util;
+using Android.Views;
+using AndroidIntent = Android.Content.Intent;
using AndroidUri = Android.Net.Uri;
namespace Xamarin.Essentials
@@ -25,6 +30,23 @@ public static partial class Platform
public static event EventHandler ActivityStateChanged;
+ internal const int requestCodeFilePicker = 11001;
+ internal const int requestCodeMediaPicker = 11002;
+ internal const int requestCodeMediaCapture = 11003;
+ internal const int requestCodePickContact = 11004;
+
+ internal const int requestCodeStart = 12000;
+
+ static int requestCode = requestCodeStart;
+
+ internal static int NextRequestCode()
+ {
+ if (++requestCode >= 12999)
+ requestCode = requestCodeStart;
+
+ return requestCode;
+ }
+
internal static void OnActivityStateChanged(Activity activity, ActivityState ev)
=> ActivityStateChanged?.Invoke(null, new ActivityStateChangedEventArgs(activity, ev));
@@ -78,8 +100,27 @@ public static void Init(Activity activity, Bundle bundle)
public static void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) =>
Permissions.OnRequestPermissionsResult(requestCode, permissions, grantResults);
- public static void OnResume() =>
+ public static void OnNewIntent(AndroidIntent intent)
+ => CheckAppActions(intent);
+
+ public static void OnResume(Activity activity = null)
+ {
+ if (activity != null)
+ CheckAppActions(activity.Intent);
+
WebAuthenticator.OnResume(null);
+ }
+
+ static void CheckAppActions(AndroidIntent intent)
+ {
+ if (intent?.Action == Intent.ActionAppAction)
+ {
+ var appAction = intent.ToAppAction();
+
+ if (!string.IsNullOrEmpty(appAction?.Id))
+ AppActions.InvokeOnAppAction(Platform.CurrentActivity, appAction);
+ }
+ }
internal static bool HasSystemFeature(string systemFeature)
{
@@ -92,20 +133,20 @@ internal static bool HasSystemFeature(string systemFeature)
return false;
}
- internal static bool IsIntentSupported(Intent intent)
+ internal static bool IsIntentSupported(AndroidIntent intent)
{
var manager = AppContext.PackageManager;
var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
return activities.Any();
}
- internal static AndroidUri GetShareableFileUri(string filename)
+ internal static AndroidUri GetShareableFileUri(FileBase file)
{
Java.IO.File sharedFile;
- if (FileProvider.IsFileInPublicLocation(filename))
+ if (FileProvider.IsFileInPublicLocation(file.FullPath))
{
// we are sharing a file in a "shared/public" location
- sharedFile = new Java.IO.File(filename);
+ sharedFile = new Java.IO.File(file.FullPath);
}
else
{
@@ -117,10 +158,21 @@ internal static AndroidUri GetShareableFileUri(string filename)
tmpDir.DeleteOnExit();
// create the new temprary file
- var tmpFile = new Java.IO.File(tmpDir, System.IO.Path.GetFileName(filename));
- System.IO.File.Copy(filename, tmpFile.CanonicalPath);
+ var tmpFile = new Java.IO.File(tmpDir, file.FileName);
tmpFile.DeleteOnExit();
+ var fileUri = AndroidUri.Parse(file.FullPath);
+ if (fileUri.Scheme == "content")
+ {
+ using var stream = Application.Context.ContentResolver.OpenInputStream(fileUri);
+ using var destStream = System.IO.File.Create(tmpFile.CanonicalPath);
+ stream.CopyTo(destStream);
+ }
+ else
+ {
+ System.IO.File.Copy(file.FullPath, tmpFile.CanonicalPath);
+ }
+
sharedFile = tmpFile;
}
@@ -212,6 +264,14 @@ internal static bool HasApiLevel(BuildVersionCodes versionCode) =>
internal static PowerManager PowerManager =>
AppContext.GetSystemService(Context.PowerService) as PowerManager;
+#if __ANDROID_25__
+ internal static ShortcutManager ShortcutManager =>
+ AppContext.GetSystemService(Context.ShortcutService) as ShortcutManager;
+#endif
+
+ internal static IWindowManager WindowManager =>
+ AppContext.GetSystemService(Context.WindowService) as IWindowManager;
+
internal static Java.Util.Locale GetLocale()
{
var resources = AppContext.Resources;
@@ -221,7 +281,9 @@ internal static Java.Util.Locale GetLocale()
return config.Locales.Get(0);
#endif
+#pragma warning disable CS0618 // Type or member is obsolete
return config.Locale;
+#pragma warning restore CS0618 // Type or member is obsolete
}
internal static void SetLocale(Java.Util.Locale locale)
@@ -230,15 +292,24 @@ internal static void SetLocale(Java.Util.Locale locale)
var resources = AppContext.Resources;
var config = resources.Configuration;
+#if __ANDROID_24__
if (HasApiLevelN)
config.SetLocale(locale);
else
+#endif
+#pragma warning disable CS0618 // Type or member is obsolete
config.Locale = locale;
+#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
}
+
+ public static class Intent
+ {
+ public const string ActionAppAction = "ACTION_XE_APP_ACTION";
+ }
}
public enum ActivityState
@@ -308,4 +379,116 @@ void Application.IActivityLifecycleCallbacks.OnActivityStarted(Activity activity
void Application.IActivityLifecycleCallbacks.OnActivityStopped(Activity activity) =>
Platform.OnActivityStateChanged(activity, ActivityState.Stopped);
}
+
+ [Activity(ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
+ class IntermediateActivity : Activity
+ {
+ const string launchedExtra = "launched";
+ const string actualIntentExtra = "actual_intent";
+ const string guidExtra = "guid";
+ const string requestCodeExtra = "request_code";
+ const string outputExtra = "output";
+
+ internal const string OutputUriExtra = "output_uri";
+
+ static readonly ConcurrentDictionary> pendingTasks =
+ new ConcurrentDictionary>();
+
+ bool launched;
+ Intent actualIntent;
+ string guid;
+ int requestCode;
+ string output;
+ global::Android.Net.Uri outputUri;
+
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ var extras = savedInstanceState ?? Intent.Extras;
+
+ // read the values
+ launched = extras.GetBoolean(launchedExtra, false);
+ actualIntent = extras.GetParcelable(actualIntentExtra) as Intent;
+ guid = extras.GetString(guidExtra);
+ requestCode = extras.GetInt(requestCodeExtra, -1);
+ output = extras.GetString(outputExtra, null);
+
+ if (!string.IsNullOrEmpty(output))
+ {
+ var javaFile = new Java.IO.File(output);
+ var providerAuthority = Platform.AppContext.PackageName + ".fileProvider";
+ outputUri = FileProvider.GetUriForFile(Platform.AppContext, providerAuthority, javaFile);
+ actualIntent.PutExtra(MediaStore.ExtraOutput, outputUri);
+ }
+
+ // if this is the first time, lauch the real activity
+ if (!launched)
+ StartActivityForResult(actualIntent, requestCode);
+ }
+
+ protected override void OnSaveInstanceState(Bundle outState)
+ {
+ // make sure we mark this activity as launched
+ outState.PutBoolean(launchedExtra, true);
+
+ // save the values
+ outState.PutParcelable(actualIntentExtra, actualIntent);
+ outState.PutString(guidExtra, guid);
+ outState.PutInt(requestCodeExtra, requestCode);
+ outState.PutString(outputExtra, output);
+
+ base.OnSaveInstanceState(outState);
+ }
+
+ protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
+ {
+ base.OnActivityResult(requestCode, resultCode, data);
+
+ // we have a valid GUID, so handle the task
+ if (!string.IsNullOrEmpty(guid) && pendingTasks.TryRemove(guid, out var tcs) && tcs != null)
+ {
+ if (resultCode == Result.Canceled)
+ {
+ tcs.TrySetCanceled();
+ }
+ else
+ {
+ if (outputUri != null)
+ data.PutExtra(OutputUriExtra, outputUri);
+
+ tcs.TrySetResult(data);
+ }
+ }
+
+ // close the intermediate activity
+ Finish();
+ }
+
+ public static Task StartAsync(Intent intent, int requestCode, Java.IO.File extraOutput = null)
+ {
+ // make sure we have the activity
+ var activity = Platform.GetCurrentActivity(true);
+
+ var tcs = new TaskCompletionSource();
+
+ // create a new task
+ var guid = Guid.NewGuid().ToString();
+ pendingTasks[guid] = tcs;
+
+ // create the intermediate intent, and add the real intent to it
+ var intermediateIntent = new Intent(activity, typeof(IntermediateActivity));
+ intermediateIntent.PutExtra(actualIntentExtra, intent);
+ intermediateIntent.PutExtra(guidExtra, guid);
+ intermediateIntent.PutExtra(requestCodeExtra, requestCode);
+
+ if (extraOutput != null)
+ intermediateIntent.PutExtra(outputExtra, extraOutput.AbsolutePath);
+
+ // start the intermediate activity
+ activity.StartActivityForResult(intermediateIntent, requestCode);
+
+ return tcs.Task;
+ }
+ }
}
diff --git a/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs b/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs
index 504be71b2..5a962c4c5 100644
--- a/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs
+++ b/Xamarin.Essentials/Platform/Platform.ios.tvos.watchos.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using Foundation;
using ObjCRuntime;
using UIKit;
@@ -21,6 +22,18 @@ public static bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
=> WebAuthenticator.OpenUrl(new Uri(url.AbsoluteString));
#endif
+#if __IOS__
+ public static void PerformActionForShortcutItem(UIApplication application, UIApplicationShortcutItem shortcutItem, UIOperationHandler completionHandler)
+ {
+ if (shortcutItem.Type == AppActions.Type)
+ {
+ var appAction = shortcutItem.ToAppAction();
+
+ AppActions.InvokeOnAppAction(application, shortcutItem.ToAppAction());
+ }
+ }
+#endif
+
#if __IOS__
[DllImport(Constants.SystemLibrary, EntryPoint = "sysctlbyname")]
#else
diff --git a/Xamarin.Essentials/Platform/Platform.macos.cs b/Xamarin.Essentials/Platform/Platform.macos.cs
new file mode 100644
index 000000000..fbe0981d9
--- /dev/null
+++ b/Xamarin.Essentials/Platform/Platform.macos.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Runtime.InteropServices;
+using AppKit;
+using CoreFoundation;
+using Foundation;
+using ObjCRuntime;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Platform
+ {
+ internal static NSWindow GetCurrentWindow(bool throwIfNull = true)
+ {
+ var window = NSApplication.SharedApplication.MainWindow;
+
+ if (throwIfNull && window == null)
+ throw new InvalidOperationException("Could not find current window.");
+
+ return window;
+ }
+ }
+
+ internal static class IOKit
+ {
+ const string IOKitLibrary = "/System/Library/Frameworks/IOKit.framework/IOKit";
+ const string IOPlatformExpertDeviceClassName = "IOPlatformExpertDevice";
+
+ const uint kIOPMAssertionLevelOff = 0;
+ const uint kIOPMAssertionLevelOn = 255;
+
+ const string kIOPMACPowerKey = "AC Power";
+ const string kIOPMUPSPowerKey = "UPS Power";
+ const string kIOPMBatteryPowerKey = "Battery Power";
+
+ const string kIOPSCurrentCapacityKey = "Current Capacity";
+ const string kIOPSMaxCapacityKey = "Max Capacity";
+ const string kIOPSTypeKey = "Type";
+ const string kIOPSInternalBatteryType = "InternalBattery";
+ const string kIOPSIsPresentKey = "Is Present";
+ const string kIOPSIsChargingKey = "Is Charging";
+ const string kIOPSIsChargedKey = "Is Charged";
+ const string kIOPSIsFinishingChargeKey = "Is Finishing Charge";
+
+ static readonly CFString kIOPMAssertionTypePreventUserIdleDisplaySleep = "PreventUserIdleDisplaySleep";
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOPSCopyPowerSourcesInfo();
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOPSGetProvidingPowerSourceType(IntPtr snapshot);
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOPSCopyPowerSourcesList(IntPtr blob);
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOPSGetPowerSourceDescription(IntPtr blob, IntPtr ps);
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOPSNotificationCreateRunLoopSource(IOPowerSourceCallback callback, IntPtr context);
+
+ [DllImport(IOKitLibrary)]
+ static extern uint IOServiceGetMatchingService(uint masterPort, IntPtr matching);
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IOServiceMatching(string s);
+
+ [DllImport(IOKitLibrary)]
+ static extern IntPtr IORegistryEntryCreateCFProperty(uint entry, IntPtr key, IntPtr allocator, uint options);
+
+ [DllImport(IOKitLibrary)]
+ static extern uint IOPMAssertionCreateWithName(IntPtr type, uint level, IntPtr name, out uint id);
+
+ [DllImport(IOKitLibrary)]
+ static extern uint IOPMAssertionRelease(uint id);
+
+ [DllImport(IOKitLibrary)]
+ static extern int IOObjectRelease(uint o);
+
+ [DllImport(Constants.CoreFoundationLibrary)]
+ static extern void CFRelease(IntPtr obj);
+
+ static bool TryGet(this NSDictionary dic, string key, out T value)
+ where T : NSObject
+ {
+ if (dic != null && dic.TryGetValue((NSString)key, out var obj) && obj is T val)
+ {
+ value = val;
+ return true;
+ }
+
+ value = default(T);
+ return false;
+ }
+
+ internal static T GetPlatformExpertPropertyValue(CFString property)
+ where T : NSObject
+ {
+ uint platformExpertRef = 0;
+ try
+ {
+ platformExpertRef = IOServiceGetMatchingService(0, IOServiceMatching(IOPlatformExpertDeviceClassName));
+ if (platformExpertRef == 0)
+ return default(T);
+
+ var propertyRef = IORegistryEntryCreateCFProperty(platformExpertRef, property.Handle, IntPtr.Zero, 0);
+ if (propertyRef == IntPtr.Zero)
+ return default(T);
+
+ return Runtime.GetNSObject(propertyRef, true);
+ }
+ finally
+ {
+ if (platformExpertRef != 0)
+ IOObjectRelease(platformExpertRef);
+ }
+ }
+
+ internal static bool PreventUserIdleDisplaySleep(CFString name, out uint id)
+ {
+ var result = IOPMAssertionCreateWithName(
+ kIOPMAssertionTypePreventUserIdleDisplaySleep.Handle,
+ kIOPMAssertionLevelOn,
+ name.Handle,
+ out var newId);
+
+ if (result == 0)
+ id = newId;
+ else
+ id = 0;
+
+ return result == 0;
+ }
+
+ internal static bool AllowUserIdleDisplaySleep(uint id)
+ {
+ var result = IOPMAssertionRelease(id);
+ return result == 0;
+ }
+
+ internal static BatteryState GetInternalBatteryState()
+ {
+ var infoHandle = IntPtr.Zero;
+ var sourcesRef = IntPtr.Zero;
+ try
+ {
+ var hasBattery = false;
+ var fullyCharged = true;
+
+ infoHandle = IOPSCopyPowerSourcesInfo();
+ sourcesRef = IOPSCopyPowerSourcesList(infoHandle);
+ var sources = NSArray.ArrayFromHandle(sourcesRef);
+ foreach (var source in sources)
+ {
+ var dicRef = IOPSGetPowerSourceDescription(infoHandle, source.Handle);
+ var dic = Runtime.GetNSObject(dicRef, false);
+
+ // we only care about internal batteries
+ if (dic.TryGet(kIOPSTypeKey, out NSString type) && type == kIOPSInternalBatteryType &&
+ dic.TryGet(kIOPSIsPresentKey, out NSNumber present) && present?.BoolValue == true)
+ {
+ // at least one is a battery
+ hasBattery = true;
+
+ // if any of the batteries are charging, then we are charging
+ if (dic.TryGet(kIOPSIsChargingKey, out NSNumber charging) && charging?.BoolValue == true)
+ return BatteryState.Charging;
+
+ // if any are not [almost] fully charged, then we are not full
+ if ((!dic.TryGet(kIOPSIsChargedKey, out NSNumber charged) || charged?.BoolValue != true) ||
+ (!dic.TryGet(kIOPSIsFinishingChargeKey, out NSNumber finishing) && finishing?.BoolValue != true))
+ fullyCharged = false;
+ }
+ }
+
+ if (!hasBattery)
+ return BatteryState.NotPresent;
+
+ if (fullyCharged)
+ return BatteryState.Full;
+
+ // we weren't able to work out what was happening, so try and guess
+ var typeHandle = IOPSGetProvidingPowerSourceType(infoHandle);
+ if (NSString.FromHandle(typeHandle) == kIOPMBatteryPowerKey)
+ return BatteryState.Discharging;
+
+ return BatteryState.NotCharging;
+ }
+ finally
+ {
+ if (infoHandle != IntPtr.Zero)
+ CFRelease(infoHandle);
+ if (sourcesRef != IntPtr.Zero)
+ CFRelease(sourcesRef);
+ }
+ }
+
+ internal static double GetInternalBatteryChargeLevel()
+ {
+ var infoHandle = IntPtr.Zero;
+ var sourcesRef = IntPtr.Zero;
+ try
+ {
+ var totalCurrent = 0.0;
+ var totalMax = 0.0;
+
+ infoHandle = IOPSCopyPowerSourcesInfo();
+ sourcesRef = IOPSCopyPowerSourcesList(infoHandle);
+ var sources = NSArray.ArrayFromHandle(sourcesRef);
+ foreach (var source in sources)
+ {
+ var dicRef = IOPSGetPowerSourceDescription(infoHandle, source.Handle);
+ var dic = Runtime.GetNSObject(dicRef, false);
+
+ // we only care about internal batteries that have capacity information
+ if (dic.TryGet(kIOPSTypeKey, out NSString type) && type == kIOPSInternalBatteryType &&
+ dic.TryGet(kIOPSIsPresentKey, out NSNumber present) && present?.BoolValue == true &&
+ dic.TryGet(kIOPSCurrentCapacityKey, out NSNumber current) && current?.Int32Value > 0 &&
+ dic.TryGet(kIOPSMaxCapacityKey, out NSNumber max) && max?.Int32Value > 0)
+ {
+ // aggregate the values
+ totalCurrent += current.Int32Value;
+ totalMax += max.Int32Value;
+ }
+ }
+
+ // something went wrong, or there is no battery
+ if (totalMax <= 0)
+ return 1.0;
+
+ return totalCurrent / totalMax;
+ }
+ finally
+ {
+ if (infoHandle != IntPtr.Zero)
+ CFRelease(infoHandle);
+ if (sourcesRef != IntPtr.Zero)
+ CFRelease(sourcesRef);
+ }
+ }
+
+ internal static BatteryPowerSource GetProvidingPowerSource()
+ {
+ var infoHandle = IntPtr.Zero;
+ try
+ {
+ infoHandle = IOPSCopyPowerSourcesInfo();
+ var typeHandle = IOPSGetProvidingPowerSourceType(infoHandle);
+ switch (NSString.FromHandle(typeHandle))
+ {
+ case kIOPMBatteryPowerKey:
+ return BatteryPowerSource.Battery;
+ case kIOPMACPowerKey:
+ case kIOPMUPSPowerKey:
+ return BatteryPowerSource.AC;
+ default:
+ return BatteryPowerSource.Unknown;
+ }
+ }
+ finally
+ {
+ if (infoHandle != IntPtr.Zero)
+ CFRelease(infoHandle);
+ }
+ }
+
+ internal static CFRunLoopSource CreatePowerSourceNotification(Action callback)
+ {
+ var sourceRef = IOPSNotificationCreateRunLoopSource(new IOPowerSourceCallback(_ => callback()), IntPtr.Zero);
+
+ if (sourceRef == default)
+ return null;
+
+ return new CFRunLoopSource(sourceRef, true);
+ }
+
+ delegate void IOPowerSourceCallback(IntPtr context);
+ }
+}
diff --git a/Xamarin.Essentials/Platform/Platform.uwp.cs b/Xamarin.Essentials/Platform/Platform.uwp.cs
index 01420bb38..c805c8cd8 100644
--- a/Xamarin.Essentials/Platform/Platform.uwp.cs
+++ b/Xamarin.Essentials/Platform/Platform.uwp.cs
@@ -9,5 +9,8 @@ public static partial class Platform
internal const string AppManifestUapXmlns = "http://schemas.microsoft.com/appx/manifest/uap/windows10";
public static string MapServiceToken { get; set; }
+
+ public static async void OnLaunched(LaunchActivatedEventArgs e)
+ => await AppActions.OnLaunched(e);
}
}
diff --git a/Xamarin.Essentials/Preferences/Preferences.ios.tvos.watchos.cs b/Xamarin.Essentials/Preferences/Preferences.ios.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/Preferences/Preferences.ios.tvos.watchos.cs
rename to Xamarin.Essentials/Preferences/Preferences.ios.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Screenshot/Screenshot.android.cs b/Xamarin.Essentials/Screenshot/Screenshot.android.cs
new file mode 100644
index 000000000..c385968d5
--- /dev/null
+++ b/Xamarin.Essentials/Screenshot/Screenshot.android.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Android.Graphics;
+using Android.Views;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Screenshot
+ {
+ static bool PlatformIsCaptureSupported =>
+ true;
+
+ static Task PlatformCaptureAsync()
+ {
+ if (Platform.WindowManager?.DefaultDisplay?.Flags.HasFlag(DisplayFlags.Secure) == true)
+ throw new UnauthorizedAccessException("Unable to take a screenshot of a secure window.");
+
+ var view = Platform.GetCurrentActivity(true)?.Window?.DecorView?.RootView;
+ if (view == null)
+ throw new NullReferenceException("Unable to find the main window.");
+
+ var bitmap = Bitmap.CreateBitmap(view.Width, view.Height, Bitmap.Config.Argb8888);
+
+ using (var canvas = new Canvas(bitmap))
+ {
+ var drawable = view.Background;
+ if (drawable != null)
+ drawable.Draw(canvas);
+ else
+ canvas.DrawColor(Color.White);
+
+ view.Draw(canvas);
+ }
+
+ var result = new ScreenshotResult(bitmap);
+
+ return Task.FromResult(result);
+ }
+ }
+
+ public partial class ScreenshotResult
+ {
+ readonly Bitmap bmp;
+
+ internal ScreenshotResult(Bitmap bmp)
+ : base()
+ {
+ this.bmp = bmp;
+
+ Width = bmp.Width;
+ Height = bmp.Height;
+ }
+
+ internal async Task PlatformOpenReadAsync(ScreenshotFormat format)
+ {
+ var stream = new MemoryStream();
+
+ var f = format switch
+ {
+ ScreenshotFormat.Jpeg => Bitmap.CompressFormat.Jpeg,
+ _ => Bitmap.CompressFormat.Png,
+ };
+
+ await bmp.CompressAsync(f, 100, stream).ConfigureAwait(false);
+ stream.Position = 0;
+
+ return stream;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Screenshot/Screenshot.ios.tvos.cs b/Xamarin.Essentials/Screenshot/Screenshot.ios.tvos.cs
new file mode 100644
index 000000000..2b193081e
--- /dev/null
+++ b/Xamarin.Essentials/Screenshot/Screenshot.ios.tvos.cs
@@ -0,0 +1,44 @@
+using System.IO;
+using System.Threading.Tasks;
+using UIKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Screenshot
+ {
+ internal static bool PlatformIsCaptureSupported =>
+ UIScreen.MainScreen != null;
+
+ static Task PlatformCaptureAsync()
+ {
+ var img = UIScreen.MainScreen.Capture();
+ var result = new ScreenshotResult(img);
+
+ return Task.FromResult(result);
+ }
+ }
+
+ public partial class ScreenshotResult
+ {
+ readonly UIImage uiImage;
+
+ internal ScreenshotResult(UIImage image)
+ {
+ uiImage = image;
+
+ Width = (int)(image.Size.Width * image.CurrentScale);
+ Height = (int)(image.Size.Height * image.CurrentScale);
+ }
+
+ internal Task PlatformOpenReadAsync(ScreenshotFormat format)
+ {
+ var data = format switch
+ {
+ ScreenshotFormat.Jpeg => uiImage.AsJPEG(),
+ _ => uiImage.AsPNG()
+ };
+
+ return Task.FromResult(data.AsStream());
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Screenshot/Screenshot.netstandard.tizen.watchos.macos.cs b/Xamarin.Essentials/Screenshot/Screenshot.netstandard.tizen.watchos.macos.cs
new file mode 100644
index 000000000..5f44ba501
--- /dev/null
+++ b/Xamarin.Essentials/Screenshot/Screenshot.netstandard.tizen.watchos.macos.cs
@@ -0,0 +1,24 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Screenshot
+ {
+ static bool PlatformIsCaptureSupported =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformCaptureAsync() =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+
+ public partial class ScreenshotResult
+ {
+ ScreenshotResult()
+ {
+ }
+
+ internal Task PlatformOpenReadAsync(ScreenshotFormat format) =>
+ throw ExceptionUtils.NotSupportedOrImplementedException;
+ }
+}
diff --git a/Xamarin.Essentials/Screenshot/Screenshot.shared.cs b/Xamarin.Essentials/Screenshot/Screenshot.shared.cs
new file mode 100644
index 000000000..1668363ca
--- /dev/null
+++ b/Xamarin.Essentials/Screenshot/Screenshot.shared.cs
@@ -0,0 +1,35 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Screenshot
+ {
+ public static bool IsCaptureSupported
+ => PlatformIsCaptureSupported;
+
+ public static Task CaptureAsync()
+ {
+ if (!IsCaptureSupported)
+ throw new FeatureNotSupportedException();
+
+ return PlatformCaptureAsync();
+ }
+ }
+
+ public partial class ScreenshotResult
+ {
+ public int Width { get; }
+
+ public int Height { get; }
+
+ public Task OpenReadAsync(ScreenshotFormat format = ScreenshotFormat.Png) =>
+ PlatformOpenReadAsync(format);
+ }
+
+ public enum ScreenshotFormat
+ {
+ Png,
+ Jpeg
+ }
+}
diff --git a/Xamarin.Essentials/Screenshot/Screenshot.uwp.cs b/Xamarin.Essentials/Screenshot/Screenshot.uwp.cs
new file mode 100644
index 000000000..0ec297eee
--- /dev/null
+++ b/Xamarin.Essentials/Screenshot/Screenshot.uwp.cs
@@ -0,0 +1,66 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices.WindowsRuntime;
+using System.Threading.Tasks;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media.Imaging;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Screenshot
+ {
+ internal static bool PlatformIsCaptureSupported =>
+ true;
+
+ static async Task PlatformCaptureAsync()
+ {
+ var element = Window.Current?.Content as FrameworkElement;
+ if (element == null)
+ throw new InvalidOperationException("Unable to find main window content.");
+
+ var bmp = new RenderTargetBitmap();
+ await bmp.RenderAsync(element).AsTask().ConfigureAwait(false);
+
+ return new ScreenshotResult(bmp);
+ }
+ }
+
+ public partial class ScreenshotResult
+ {
+ readonly RenderTargetBitmap bmp;
+ byte[] bytes;
+
+ internal ScreenshotResult(RenderTargetBitmap bmp)
+ {
+ this.bmp = bmp;
+
+ Width = bmp.PixelWidth;
+ Height = bmp.PixelHeight;
+ }
+
+ internal async Task PlatformOpenReadAsync(ScreenshotFormat format)
+ {
+ if (bytes == null)
+ {
+ var pixels = await bmp.GetPixelsAsync().AsTask().ConfigureAwait(false);
+ bytes = pixels.ToArray();
+ }
+
+ var f = format switch
+ {
+ ScreenshotFormat.Jpeg => BitmapEncoder.JpegEncoderId,
+ _ => BitmapEncoder.PngEncoderId
+ };
+
+ var ms = new InMemoryRandomAccessStream();
+
+ var encoder = await BitmapEncoder.CreateAsync(f, ms).AsTask().ConfigureAwait(false);
+ encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)Width, (uint)Height, 96, 96, bytes);
+ await encoder.FlushAsync().AsTask().ConfigureAwait(false);
+
+ return ms.AsStreamForRead();
+ }
+ }
+}
diff --git a/Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.cs b/Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.macos.cs
similarity index 100%
rename from Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.cs
rename to Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.macos.cs
diff --git a/Xamarin.Essentials/Share/Share.android.cs b/Xamarin.Essentials/Share/Share.android.cs
index 03783bad8..905029830 100644
--- a/Xamarin.Essentials/Share/Share.android.cs
+++ b/Xamarin.Essentials/Share/Share.android.cs
@@ -39,7 +39,7 @@ static Task PlatformRequestAsync(ShareTextRequest request)
static Task PlatformRequestAsync(ShareFileRequest request)
{
- var contentUri = Platform.GetShareableFileUri(request.File.FullPath);
+ var contentUri = Platform.GetShareableFileUri(request.File);
var intent = new Intent(Intent.ActionSend);
intent.SetType(request.File.ContentType);
diff --git a/Xamarin.Essentials/Share/Share.macos.cs b/Xamarin.Essentials/Share/Share.macos.cs
new file mode 100644
index 000000000..d61f9c379
--- /dev/null
+++ b/Xamarin.Essentials/Share/Share.macos.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Share
+ {
+ static Task PlatformRequestAsync(ShareTextRequest request)
+ {
+ var items = new List();
+ if (!string.IsNullOrWhiteSpace(request.Title))
+ items.Add(new NSString(request.Title));
+ if (!string.IsNullOrWhiteSpace(request.Text))
+ items.Add(new NSString(request.Text));
+ if (!string.IsNullOrWhiteSpace(request.Uri))
+ items.Add(NSUrl.FromString(request.Uri));
+
+ return PlatformShowRequestAsync(request, items);
+ }
+
+ static Task PlatformRequestAsync(ShareFileRequest request)
+ {
+ var items = new List();
+ if (!string.IsNullOrWhiteSpace(request.Title))
+ items.Add(new NSString(request.Title));
+ if (request.File != null)
+ items.Add(NSUrl.FromFilename(request.File.FullPath));
+
+ return PlatformShowRequestAsync(request, items);
+ }
+
+ static Task PlatformShowRequestAsync(ShareRequestBase request, List items)
+ {
+ var window = Platform.GetCurrentWindow();
+ var view = window.ContentView;
+
+ var rect = request.PresentationSourceBounds.ToPlatformRectangle();
+ rect.Y = view.Bounds.Height - rect.Bottom;
+
+ var picker = new NSSharingServicePicker(items.ToArray());
+ picker.ShowRelativeToRect(rect, view, NSRectEdge.MinYEdge);
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Sms/Sms.macos.cs b/Xamarin.Essentials/Sms/Sms.macos.cs
new file mode 100644
index 000000000..32082ca94
--- /dev/null
+++ b/Xamarin.Essentials/Sms/Sms.macos.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using AppKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Sms
+ {
+ internal static bool IsComposeSupported =>
+ MainThread.InvokeOnMainThread(() => NSWorkspace.SharedWorkspace.UrlForApplication(NSUrl.FromString("sms:")) != null);
+
+ static Task PlatformComposeAsync(SmsMessage message)
+ {
+ var recipients = string.Join(",", message.Recipients.Select(r => Uri.EscapeDataString(r)));
+
+ var uri = $"sms:/open?addresses={recipients}";
+
+ if (!string.IsNullOrEmpty(message?.Body))
+ uri += "&body=" + Uri.EscapeDataString(message.Body);
+
+ var nsurl = NSUrl.FromString(uri);
+ NSWorkspace.SharedWorkspace.OpenUrl(nsurl);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/TextToSpeech/TextToSpeech.android.cs b/Xamarin.Essentials/TextToSpeech/TextToSpeech.android.cs
index d3c767162..66d60791e 100644
--- a/Xamarin.Essentials/TextToSpeech/TextToSpeech.android.cs
+++ b/Xamarin.Essentials/TextToSpeech/TextToSpeech.android.cs
@@ -54,7 +54,7 @@ internal static Task> PlatformGetLocalesAsync()
}
}
- internal class TextToSpeechImplementation : Java.Lang.Object, AndroidTextToSpeech.IOnInitListener,
+ class TextToSpeechImplementation : Java.Lang.Object, AndroidTextToSpeech.IOnInitListener,
#pragma warning disable CS0618
AndroidTextToSpeech.IOnUtteranceCompletedListener
#pragma warning restore CS0618
@@ -80,7 +80,7 @@ Task Initialize()
}
catch (Exception e)
{
- tcsInitialize.SetException(e);
+ tcsInitialize.TrySetException(e);
}
return tcsInitialize.Task;
@@ -206,7 +206,7 @@ public async Task> GetLocalesAsync()
.Select(g => g.First());
}
- private bool IsLocaleAvailable(JavaLocale l)
+ bool IsLocaleAvailable(JavaLocale l)
{
try
{
diff --git a/Xamarin.Essentials/TextToSpeech/TextToSpeech.ios.tvos.watchos.cs b/Xamarin.Essentials/TextToSpeech/TextToSpeech.ios.tvos.watchos.cs
index 92977aae2..f7f555eb6 100644
--- a/Xamarin.Essentials/TextToSpeech/TextToSpeech.ios.tvos.watchos.cs
+++ b/Xamarin.Essentials/TextToSpeech/TextToSpeech.ios.tvos.watchos.cs
@@ -69,7 +69,7 @@ internal static async Task SpeakUtterance(AVSpeechUtterance speechUtterance, Can
void TryCancel()
{
- speechSynthesizer.Value?.StopSpeaking(AVSpeechBoundary.Word);
+ speechSynthesizer.Value?.StopSpeaking(AVSpeechBoundary.Immediate);
tcsUtterance?.TrySetResult(true);
}
diff --git a/Xamarin.Essentials/TextToSpeech/TextToSpeech.macos.cs b/Xamarin.Essentials/TextToSpeech/TextToSpeech.macos.cs
new file mode 100644
index 000000000..5da458253
--- /dev/null
+++ b/Xamarin.Essentials/TextToSpeech/TextToSpeech.macos.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AppKit;
+
+namespace Xamarin.Essentials
+{
+ public static partial class TextToSpeech
+ {
+ static readonly Lazy speechSynthesizer = new Lazy(() =>
+ new NSSpeechSynthesizer { Delegate = new SpeechSynthesizerDelegate() });
+
+ internal static Task> PlatformGetLocalesAsync() =>
+ Task.FromResult(NSSpeechSynthesizer.AvailableVoices
+ .Select(v => new Locale(v, null, null, null)));
+
+ internal static async Task PlatformSpeakAsync(string text, SpeechOptions options, CancellationToken cancelToken = default)
+ {
+ var ss = speechSynthesizer.Value;
+ var ssd = (SpeechSynthesizerDelegate)ss.Delegate;
+
+ var tcs = new TaskCompletionSource();
+ try
+ {
+ if (options != null)
+ {
+ if (options.Volume.HasValue)
+ ss.Volume = NormalizeVolume(options.Volume);
+
+ if (options.Locale != null)
+ ss.Voice = options.Locale.Language;
+ }
+
+ ssd.FinishedSpeaking += OnFinishedSpeaking;
+ ssd.EncounteredError += OnEncounteredError;
+
+ ss.StartSpeakingString(text);
+
+ using (cancelToken.Register(TryCancel))
+ {
+ await tcs.Task;
+ }
+ }
+ finally
+ {
+ ssd.FinishedSpeaking -= OnFinishedSpeaking;
+ ssd.EncounteredError -= OnEncounteredError;
+ }
+
+ void TryCancel()
+ {
+ ss.StopSpeaking(NSSpeechBoundary.hWord);
+ tcs.TrySetResult(true);
+ }
+
+ void OnFinishedSpeaking(bool completed)
+ {
+ tcs.TrySetResult(completed);
+ }
+
+ void OnEncounteredError(string errorMessage)
+ {
+ // TODO: a real exception type here
+ tcs.TrySetException(new Exception(errorMessage));
+ }
+ }
+
+ static float NormalizeVolume(float? volume)
+ {
+ var v = volume ?? 1.0f;
+ if (v > 1.0f)
+ v = 1.0f;
+ else if (v < 0.0f)
+ v = 0.0f;
+ return v;
+ }
+
+ class SpeechSynthesizerDelegate : NSSpeechSynthesizerDelegate
+ {
+ public event Action FinishedSpeaking;
+
+ public event Action EncounteredError;
+
+ public override void DidEncounterError(NSSpeechSynthesizer sender, nuint characterIndex, string theString, string message) =>
+ EncounteredError?.Invoke(message);
+
+ public override void DidFinishSpeaking(NSSpeechSynthesizer sender, bool finishedSpeaking) =>
+ FinishedSpeaking?.Invoke(finishedSpeaking);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Types/Contact.shared.cs b/Xamarin.Essentials/Types/Contact.shared.cs
new file mode 100644
index 000000000..74ec34758
--- /dev/null
+++ b/Xamarin.Essentials/Types/Contact.shared.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Xamarin.Essentials
+{
+ public class Contact
+ {
+ public string Name { get; }
+
+ public ContactType ContactType { get; }
+
+ public IReadOnlyList Numbers { get; }
+
+ public IReadOnlyList Emails { get; }
+
+ internal Contact(
+ string name,
+ List numbers,
+ List email,
+ ContactType contactType)
+ {
+ Name = name;
+ Emails = email;
+ Numbers = numbers;
+ ContactType = contactType;
+ }
+ }
+
+ public class ContactEmail
+ {
+ public string EmailAddress { get; }
+
+ public ContactType ContactType { get; }
+
+ internal ContactEmail(string email, ContactType contactType)
+ {
+ EmailAddress = email;
+ ContactType = contactType;
+ }
+ }
+
+ public class ContactPhone
+ {
+ public string PhoneNumber { get; }
+
+ public ContactType ContactType { get; }
+
+ internal ContactPhone(string phoneNumber, ContactType contactType)
+ {
+ PhoneNumber = phoneNumber;
+ ContactType = contactType;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Types/DevicePlatform.shared.cs b/Xamarin.Essentials/Types/DevicePlatform.shared.cs
index bef820d6d..50d52a399 100644
--- a/Xamarin.Essentials/Types/DevicePlatform.shared.cs
+++ b/Xamarin.Essentials/Types/DevicePlatform.shared.cs
@@ -10,6 +10,8 @@ namespace Xamarin.Essentials
public static DevicePlatform iOS { get; } = new DevicePlatform(nameof(iOS));
+ public static DevicePlatform macOS { get; } = new DevicePlatform(nameof(macOS));
+
public static DevicePlatform tvOS { get; } = new DevicePlatform(nameof(tvOS));
public static DevicePlatform Tizen { get; } = new DevicePlatform(nameof(Tizen));
diff --git a/Xamarin.Essentials/Types/ExperimentalFeatures.shared.cs b/Xamarin.Essentials/Types/ExperimentalFeatures.shared.cs
index 0b5c974d6..31b9fead3 100644
--- a/Xamarin.Essentials/Types/ExperimentalFeatures.shared.cs
+++ b/Xamarin.Essentials/Types/ExperimentalFeatures.shared.cs
@@ -18,6 +18,7 @@ public static class ExperimentalFeatures
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("ExperimentalFeatures.EmailAttachments is obsolete as of version 1.3.0 and no longer required to use the feature.")]
public const string EmailAttachments = "EmailAttachments_Experimental";
+ public const string MediaPicker = "MediaPicker_Experimental";
static HashSet