From 13bb42cd8a038831651722a6f54ca87ab433b278 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 21 Mar 2018 06:55:52 +0200 Subject: [PATCH] GH-26: Add SMS API (#91) * Adding SMS API * Added tests * Added docs * Made some changes to the way SMS works * Fix the tests --- Caboodle.Tests/Sms_Tests.cs | 14 ++++ Caboodle/Platform/Platform.android.cs | 7 ++ Caboodle/Platform/Platform.ios.cs | 33 ++++++++ Caboodle/Shared/Exceptions.shared.cs | 17 ++++ Caboodle/Sms/Sms.android.cs | 66 +++++++++++++++ Caboodle/Sms/Sms.ios.cs | 38 +++++++++ Caboodle/Sms/Sms.netstandard.cs | 10 +++ Caboodle/Sms/Sms.shared.cs | 27 +++++++ Caboodle/Sms/Sms.uwp.cs | 27 +++++++ .../Caboodle.DeviceTests.Shared.projitems | 1 + .../Caboodle.DeviceTests.Shared/Sms_Tests.cs | 34 ++++++++ .../Caboodle.DeviceTests.Shared/Utils.cs | 4 + Samples/Caboodle.Samples/App.xaml | 7 +- .../Converters/NegativeConverter.cs | 25 ++++++ Samples/Caboodle.Samples/View/BasePage.cs | 11 ++- Samples/Caboodle.Samples/View/SMSPage.xaml | 29 +++++++ Samples/Caboodle.Samples/View/SMSPage.xaml.cs | 10 +++ .../ViewModel/BaseViewModel.cs | 12 ++- .../ViewModel/HomeViewModel.cs | 7 +- .../ViewModel/SmsViewModel.cs | 50 ++++++++++++ docs/en/Microsoft.Caboodle/Sms.xml | 54 +++++++++++++ docs/en/Microsoft.Caboodle/SmsMessage.xml | 81 +++++++++++++++++++ docs/en/index.xml | 2 + 23 files changed, 558 insertions(+), 8 deletions(-) create mode 100644 Caboodle.Tests/Sms_Tests.cs create mode 100644 Caboodle/Sms/Sms.android.cs create mode 100644 Caboodle/Sms/Sms.ios.cs create mode 100644 Caboodle/Sms/Sms.netstandard.cs create mode 100644 Caboodle/Sms/Sms.shared.cs create mode 100644 Caboodle/Sms/Sms.uwp.cs create mode 100644 DeviceTests/Caboodle.DeviceTests.Shared/Sms_Tests.cs create mode 100644 Samples/Caboodle.Samples/Converters/NegativeConverter.cs create mode 100644 Samples/Caboodle.Samples/View/SMSPage.xaml create mode 100644 Samples/Caboodle.Samples/View/SMSPage.xaml.cs create mode 100644 Samples/Caboodle.Samples/ViewModel/SmsViewModel.cs create mode 100644 docs/en/Microsoft.Caboodle/Sms.xml create mode 100644 docs/en/Microsoft.Caboodle/SmsMessage.xml diff --git a/Caboodle.Tests/Sms_Tests.cs b/Caboodle.Tests/Sms_Tests.cs new file mode 100644 index 000000000..04f3391b4 --- /dev/null +++ b/Caboodle.Tests/Sms_Tests.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Caboodle.Tests +{ + public class Sms_Tests + { + [Fact] + public Task Sms_Fail_On_NetStandard() + { + return Assert.ThrowsAsync(() => Sms.ComposeAsync()); + } + } +} diff --git a/Caboodle/Platform/Platform.android.cs b/Caboodle/Platform/Platform.android.cs index f2b57c1d0..225390819 100644 --- a/Caboodle/Platform/Platform.android.cs +++ b/Caboodle/Platform/Platform.android.cs @@ -34,6 +34,13 @@ internal static bool HasPermissionInManifest(string permission) return requestedPermissions?.Any(r => r.Equals(permission, StringComparison.InvariantCultureIgnoreCase)) ?? false; } + internal static bool IsIntentSupported(Intent intent) + { + var manager = CurrentContext.PackageManager; + var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly); + return activities.Any(); + } + public static void BeginInvokeOnMainThread(Action action) { if (handler?.Looper != Looper.MainLooper) diff --git a/Caboodle/Platform/Platform.ios.cs b/Caboodle/Platform/Platform.ios.cs index 239cd4e45..09948cef9 100644 --- a/Caboodle/Platform/Platform.ios.cs +++ b/Caboodle/Platform/Platform.ios.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using Foundation; +using UIKit; namespace Microsoft.Caboodle { @@ -15,5 +17,36 @@ public static void BeginInvokeOnMainThread(Action action) NSRunLoop.Main.BeginInvokeOnMainThread(action.Invoke); } + + internal static UIViewController GetCurrentViewController(bool throwIfNull = true) + { + UIViewController viewController = null; + + var window = UIApplication.SharedApplication.KeyWindow; + + if (window.WindowLevel == UIWindowLevel.Normal) + viewController = window.RootViewController; + + if (viewController == null) + { + window = UIApplication.SharedApplication + .Windows + .OrderByDescending(w => w.WindowLevel) + .FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal); + + if (window == null) + throw new InvalidOperationException("Could not find current view controller."); + else + viewController = window.RootViewController; + } + + while (viewController.PresentedViewController != null) + viewController = viewController.PresentedViewController; + + if (throwIfNull && viewController == null) + throw new InvalidOperationException("Could not find current view controller."); + + return viewController; + } } } diff --git a/Caboodle/Shared/Exceptions.shared.cs b/Caboodle/Shared/Exceptions.shared.cs index a8af8436c..0445337b1 100644 --- a/Caboodle/Shared/Exceptions.shared.cs +++ b/Caboodle/Shared/Exceptions.shared.cs @@ -17,4 +17,21 @@ public PermissionException(string permission) { } } + + public class FeatureNotSupportedException : NotSupportedException + { + public FeatureNotSupportedException() + { + } + + public FeatureNotSupportedException(string message) + : base(message) + { + } + + public FeatureNotSupportedException(string message, Exception innerException) + : base(message, innerException) + { + } + } } diff --git a/Caboodle/Sms/Sms.android.cs b/Caboodle/Sms/Sms.android.cs new file mode 100644 index 000000000..ac30a9f7b --- /dev/null +++ b/Caboodle/Sms/Sms.android.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Android.Content; +using Android.OS; +using Android.Provider; + +using AndroidUri = Android.Net.Uri; + +namespace Microsoft.Caboodle +{ + public static partial class Sms + { + static bool IsComposeSupported + => Platform.IsIntentSupported(CreateIntent("0000000000")); + + public static Task ComposeAsync(SmsMessage message) + { + if (!IsComposeSupported) + throw new FeatureNotSupportedException(); + + var intent = CreateIntent(message); + Platform.CurrentContext.StartActivity(intent); + + return Task.FromResult(true); + } + + static Intent CreateIntent(SmsMessage message) + => CreateIntent(message?.Recipient, message?.Body); + + static Intent CreateIntent(string recipient, string body = null) + { + Intent intent; + if (!string.IsNullOrWhiteSpace(recipient)) + { + var uri = AndroidUri.Parse("smsto:" + recipient); + intent = new Intent(Intent.ActionSendto, uri); + + if (!string.IsNullOrWhiteSpace(body)) + intent.PutExtra("sms_body", body); + } + else + { + var pm = Platform.CurrentContext.PackageManager; + var packageName = Telephony.Sms.GetDefaultSmsPackage(Platform.CurrentContext); + intent = pm.GetLaunchIntentForPackage(packageName); + } + + return intent; + } + + public static string GetDefaultSmsPackage(Context context) + { + if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat) + { + return Telephony.Sms.GetDefaultSmsPackage(context); + } + else + { + var defApp = Settings.Secure.GetString(context.ContentResolver, "sms_default_application"); + var pm = context.ApplicationContext.PackageManager; + var intent = pm.GetLaunchIntentForPackage(defApp); + var mInfo = pm.ResolveActivity(intent, 0); + return mInfo.ActivityInfo.PackageName; + } + } + } +} diff --git a/Caboodle/Sms/Sms.ios.cs b/Caboodle/Sms/Sms.ios.cs new file mode 100644 index 000000000..f50f72e3f --- /dev/null +++ b/Caboodle/Sms/Sms.ios.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using MessageUI; + +namespace Microsoft.Caboodle +{ + public static partial class Sms + { + static bool IsComposeSupported + => MFMessageComposeViewController.CanSendText; + + public static Task ComposeAsync(SmsMessage message) + { + if (!IsComposeSupported) + throw new FeatureNotSupportedException(); + + // do this first so we can throw as early as possible + var controller = Platform.GetCurrentViewController(); + + // create the controller + var messageController = new MFMessageComposeViewController(); + if (!string.IsNullOrWhiteSpace(message?.Body)) + messageController.Body = message.Body; + if (!string.IsNullOrWhiteSpace(message?.Recipient)) + messageController.Recipients = new[] { message.Recipient }; + + // show the controller + var tcs = new TaskCompletionSource(); + messageController.Finished += (sender, e) => + { + messageController.DismissViewController(true, null); + tcs.SetResult(e.Result == MessageComposeResult.Sent); + }; + controller.PresentViewController(messageController, true, null); + + return tcs.Task; + } + } +} diff --git a/Caboodle/Sms/Sms.netstandard.cs b/Caboodle/Sms/Sms.netstandard.cs new file mode 100644 index 000000000..361ab75ce --- /dev/null +++ b/Caboodle/Sms/Sms.netstandard.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Microsoft.Caboodle +{ + public static partial class Sms + { + public static Task ComposeAsync(SmsMessage message) + => throw new NotImplementedInReferenceAssemblyException(); + } +} diff --git a/Caboodle/Sms/Sms.shared.cs b/Caboodle/Sms/Sms.shared.cs new file mode 100644 index 000000000..16bc64d5d --- /dev/null +++ b/Caboodle/Sms/Sms.shared.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; + +namespace Microsoft.Caboodle +{ + public static partial class Sms + { + public static Task ComposeAsync() + => ComposeAsync(null); + } + + public class SmsMessage + { + public SmsMessage() + { + } + + public SmsMessage(string body, string recipient) + { + Body = body; + Recipient = recipient; + } + + public string Body { get; set; } + + public string Recipient { get; set; } + } +} diff --git a/Caboodle/Sms/Sms.uwp.cs b/Caboodle/Sms/Sms.uwp.cs new file mode 100644 index 000000000..d50cf0339 --- /dev/null +++ b/Caboodle/Sms/Sms.uwp.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Windows.ApplicationModel.Chat; +using Windows.Foundation.Metadata; + +namespace Microsoft.Caboodle +{ + public static partial class Sms + { + static bool IsComposeSupported + => ApiInformation.IsTypePresent("Windows.ApplicationModel.Chat.ChatMessageManager"); + + public static Task ComposeAsync(SmsMessage message) + { + if (!IsComposeSupported) + throw new FeatureNotSupportedException(); + + var chat = new ChatMessage(); + if (!string.IsNullOrWhiteSpace(message?.Body)) + chat.Body = message.Body; + if (!string.IsNullOrWhiteSpace(message?.Recipient)) + chat.Recipients.Add(message.Recipient); + + return ChatMessageManager.ShowComposeSmsMessageAsync(chat).AsTask(); + } + } +} diff --git a/DeviceTests/Caboodle.DeviceTests.Shared/Caboodle.DeviceTests.Shared.projitems b/DeviceTests/Caboodle.DeviceTests.Shared/Caboodle.DeviceTests.Shared.projitems index 2e7cb21b7..21bc5ad31 100644 --- a/DeviceTests/Caboodle.DeviceTests.Shared/Caboodle.DeviceTests.Shared.projitems +++ b/DeviceTests/Caboodle.DeviceTests.Shared/Caboodle.DeviceTests.Shared.projitems @@ -15,6 +15,7 @@ + diff --git a/DeviceTests/Caboodle.DeviceTests.Shared/Sms_Tests.cs b/DeviceTests/Caboodle.DeviceTests.Shared/Sms_Tests.cs new file mode 100644 index 000000000..d6df5073f --- /dev/null +++ b/DeviceTests/Caboodle.DeviceTests.Shared/Sms_Tests.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Microsoft.Caboodle; +using Xunit; + +namespace Caboodle.DeviceTests +{ + public class Sms_Tests + { + [Fact] + public Task Sms_ComposeAsync_Does_Not_Throw() + { + return Utils.OnMainThread(() => + { + if (Utils.IsiOSSimulator) + return Assert.ThrowsAsync(() => Sms.ComposeAsync()); + else + return Sms.ComposeAsync(); + }); + } + + [Fact] + public Task Sms_ComposeAsync_Does_Not_Throw_When_Empty() + { + var message = new SmsMessage(); + return Utils.OnMainThread(() => + { + if (Utils.IsiOSSimulator) + return Assert.ThrowsAsync(() => Sms.ComposeAsync(message)); + else + return Sms.ComposeAsync(message); + }); + } + } +} diff --git a/DeviceTests/Caboodle.DeviceTests.Shared/Utils.cs b/DeviceTests/Caboodle.DeviceTests.Shared/Utils.cs index a0a72f80e..4a82bfb59 100644 --- a/DeviceTests/Caboodle.DeviceTests.Shared/Utils.cs +++ b/DeviceTests/Caboodle.DeviceTests.Shared/Utils.cs @@ -1,10 +1,14 @@ using System; using System.Threading.Tasks; +using Microsoft.Caboodle; namespace Caboodle.DeviceTests { public class Utils { + public static bool IsiOSSimulator + => DeviceInfo.DeviceType == DeviceType.Virtual && DeviceInfo.Platform == DeviceInfo.Platforms.iOS; + #if WINDOWS_UWP public static async Task OnMainThread(Windows.UI.Core.DispatchedHandler action) { diff --git a/Samples/Caboodle.Samples/App.xaml b/Samples/Caboodle.Samples/App.xaml index 691778846..9d02945ec 100644 --- a/Samples/Caboodle.Samples/App.xaml +++ b/Samples/Caboodle.Samples/App.xaml @@ -1,10 +1,11 @@  - - - + + + \ No newline at end of file diff --git a/Samples/Caboodle.Samples/Converters/NegativeConverter.cs b/Samples/Caboodle.Samples/Converters/NegativeConverter.cs new file mode 100644 index 000000000..7e1271432 --- /dev/null +++ b/Samples/Caboodle.Samples/Converters/NegativeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using Xamarin.Forms; + +namespace Caboodle.Samples.Converters +{ + public class NegativeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool v) + return !v; + else + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool v) + return !v; + else + return true; + } + } +} diff --git a/Samples/Caboodle.Samples/View/BasePage.cs b/Samples/Caboodle.Samples/View/BasePage.cs index b03aaa28e..cdb906cb7 100644 --- a/Samples/Caboodle.Samples/View/BasePage.cs +++ b/Samples/Caboodle.Samples/View/BasePage.cs @@ -1,4 +1,6 @@ -using Caboodle.Samples.ViewModel; +using System; +using System.Threading.Tasks; +using Caboodle.Samples.ViewModel; using Xamarin.Forms; namespace Caboodle.Samples.View @@ -15,6 +17,7 @@ protected override void OnAppearing() if (BindingContext is BaseViewModel vm) { + vm.DoDisplayAlert += OnDisplayAlert; vm.OnAppearing(); } } @@ -24,9 +27,15 @@ protected override void OnDisappearing() if (BindingContext is BaseViewModel vm) { vm.OnDisappearing(); + vm.DoDisplayAlert -= OnDisplayAlert; } base.OnDisappearing(); } + + Task OnDisplayAlert(string message) + { + return DisplayAlert(Title, message, "OK"); + } } } diff --git a/Samples/Caboodle.Samples/View/SMSPage.xaml b/Samples/Caboodle.Samples/View/SMSPage.xaml new file mode 100644 index 000000000..d552c8dc9 --- /dev/null +++ b/Samples/Caboodle.Samples/View/SMSPage.xaml @@ -0,0 +1,29 @@ + + + + + + + +