Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

GH-26: Add SMS API #91

Merged
merged 8 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Caboodle/Platform/Platform.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions Caboodle/Platform/Platform.ios.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Linq;
using Foundation;
using UIKit;

namespace Microsoft.Caboodle
{
Expand All @@ -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;
}
}
}
47 changes: 47 additions & 0 deletions Caboodle/Sms/Sms.android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Android.Content;

using Uri = Android.Net.Uri;

namespace Microsoft.Caboodle
{
public static partial class Sms
{
public static bool IsComposeSupported
=> Platform.IsIntentSupported(CreateIntent("0000000000"));

public static Task ComposeAsync(SmsMessage message)
{
var intent = CreateIntent(message);
Platform.CurrentContext.StartActivity(intent);

return Task.FromResult(true);
}

private static Intent CreateIntent(SmsMessage message)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
message.Validate();

return CreateIntent(message.Recipient, message.Body);
}

private static Intent CreateIntent(string recipient, string body = null)
{
Uri uri;
if (!string.IsNullOrWhiteSpace(recipient))
uri = Uri.Parse("smsto:" + recipient);
else
uri = Uri.Parse("smsto:");

var intent = new Intent(Intent.ActionSendto, uri);

if (!string.IsNullOrWhiteSpace(body))
intent.PutExtra("sms_body", body);

return intent;
}
}
}
38 changes: 38 additions & 0 deletions Caboodle/Sms/Sms.ios.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Threading.Tasks;
using MessageUI;

namespace Microsoft.Caboodle
{
public static partial class Sms
{
public static bool IsComposeSupported
=> MFMessageComposeViewController.CanSendText;

public static Task ComposeAsync(SmsMessage message)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
message.Validate();

// do this first so we can throw as early as possible
var controller = Platform.GetCurrentViewController();

// show the controller
var tcs = new TaskCompletionSource<bool>();
var messageController = new MFMessageComposeViewController
{
Body = message.Body,
Recipients = new[] { message.Recipient }
};
messageController.Finished += (sender, e) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this get triggered no matter what? even if it is canceled? Is it better ot fire and forget?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be the case. The docs do say we have to explicitly hide the controller in this event and we are provided a result (success, canceled, failed)

{
messageController.DismissViewController(true, null);
tcs.SetResult(e.Result == MessageComposeResult.Sent);
};
controller.PresentViewController(messageController, true, null);

return tcs.Task;
}
}
}
13 changes: 13 additions & 0 deletions Caboodle/Sms/Sms.netstandard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Threading.Tasks;

namespace Microsoft.Caboodle
{
public static partial class Sms
{
public static bool IsComposeSupported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still need to decide if a bool and this naming is what we want to end up with.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. We may want to have some support level thing: Unsupported, Supported, SupportedWithBackgroundSend or something along those lines. This is more a feature that works when there is a SIM card of some sort.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that probably each class should be small enough that a single Enum should represent what it needs...

So we could introduce "SmsBackground" for instance and that is a new API with a new Enum. I think that is almost better to be honest with you.

=> throw new NotImplementedInReferenceAssemblyException();

public static Task ComposeAsync(SmsMessage message)
=> throw new NotImplementedInReferenceAssemblyException();
}
}
34 changes: 34 additions & 0 deletions Caboodle/Sms/Sms.shared.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace Microsoft.Caboodle
{
public static partial class Sms
{
}

public class SmsMessage
{
public SmsMessage()
{
}

public SmsMessage(string body, string recipient)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be one with just the body? also what is a recipient? phone number?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could have overloads for each combo, or we could just let the devs use the properties.
The recipient... each platform just takes a string, so we could leave it as is and hope for the best :)

{
Body = body;
Recipient = recipient;
}

public string Body { get; set; }

public string Recipient { get; set; }

internal void Validate()
{
if (string.IsNullOrWhiteSpace(Body))
throw new ArgumentException("SMS body must not be empty.", nameof(Body));

if (string.IsNullOrWhiteSpace(Recipient))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be empty. I think that is fine.

throw new ArgumentException("SMS recipient must not be empty.", nameof(Recipient));
}
}
}
31 changes: 31 additions & 0 deletions Caboodle/Sms/Sms.uwp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.Chat;
using Windows.Foundation.Metadata;

namespace Microsoft.Caboodle
{
public static partial class Sms
{
public static bool IsComposeSupported
=> ApiInformation.IsTypePresent("Windows.ApplicationModel.Chat.ChatMessageManager");

public static Task ComposeAsync(SmsMessage message)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
message.Validate();

var chat = new ChatMessage
{
Body = message.Body,
Recipients =
{
message.Recipient
}
};

return ChatMessageManager.ShowComposeSmsMessageAsync(chat).AsTask();
}
}
}
7 changes: 4 additions & 3 deletions Samples/Caboodle.Samples/App.xaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:Caboodle.Samples.Converters"
x:Class="Caboodle.Samples.App">
<Application.Resources>

<!-- Application resource dictionary -->

<ResourceDictionary>
<converters:NegativeConverter x:Key="NegativeConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
25 changes: 25 additions & 0 deletions Samples/Caboodle.Samples/Converters/NegativeConverter.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
38 changes: 38 additions & 0 deletions Samples/Caboodle.Samples/View/SMSPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<views:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Caboodle.Samples.View"
xmlns:viewmodels="clr-namespace:Caboodle.Samples.ViewModel"
x:Class="Caboodle.Samples.View.SMSPage"
Title="SMS">
<ContentPage.BindingContext>
<viewmodels:SmsViewModel />
</ContentPage.BindingContext>

<StackLayout>
<Label Text="Easily send SMS messages." FontAttributes="Bold" Margin="12" />

<ScrollView>
<StackLayout Padding="12,0,12,12" Spacing="6">
<Label Text="Recipent:" />
<Entry Text="{Binding Recipient}" Keyboard="Telephone" />
<Label Text="Message Text:" />
<Entry Text="{Binding MessageText}" />

<Button Text="Send SMS"
Command="{Binding SendSmsCommand}"
IsEnabled="{Binding CanSend}" />
<Label Text="Please make sure you enter a recipient and some text for the message."
TextColor="Red"
FontAttributes="Italic"
IsVisible="{Binding IsValid, Converter={StaticResource NegativeConverter}}" />
<Label Text="Sending an SMS is not supported on this platform."
FontAttributes="Italic"
IsVisible="{Binding IsSupported, Converter={StaticResource NegativeConverter}}" />

<ActivityIndicator IsVisible="{Binding IsBusy}" IsRunning="{Binding IsBusy}" />
</StackLayout>
</ScrollView>
</StackLayout>

</views:BasePage>
10 changes: 10 additions & 0 deletions Samples/Caboodle.Samples/View/SMSPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Caboodle.Samples.View
{
public partial class SMSPage : BasePage
{
public SMSPage()
{
InitializeComponent();
}
}
}
7 changes: 4 additions & 3 deletions Samples/Caboodle.Samples/ViewModel/HomeViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ public HomeViewModel()
Items = new ObservableCollection<SampleItem>
{
new SampleItem("Battery", typeof(BatteryPage), "Easily detect battery level, source, and state."),
new SampleItem("Geocoding", typeof(GeocodingPage), "Easily geocode and reverse geocoding."),
new SampleItem("Preferences", typeof(PreferencesPage), "Quickly and easily add persistent preferences."),
new SampleItem("Device Info", typeof(DeviceInfoPage), "Find out about the device with ease."),
new SampleItem("Clipboard", typeof(ClipboardPage), "Quickly and easily use clipboard"),
new SampleItem("Device Info", typeof(DeviceInfoPage), "Find out about the device with ease."),
new SampleItem("File System", typeof(FileSystemPage), "Easily save files to app data."),
new SampleItem("Geocoding", typeof(GeocodingPage), "Easily geocode and reverse geocoding."),
new SampleItem("Preferences", typeof(PreferencesPage), "Quickly and easily add persistent preferences."),
new SampleItem("SMS", typeof(SMSPage), "Easily send SMS messages."),
};
}

Expand Down
56 changes: 56 additions & 0 deletions Samples/Caboodle.Samples/ViewModel/SmsViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Windows.Input;
using Microsoft.Caboodle;
using Xamarin.Forms;

namespace Caboodle.Samples.ViewModel
{
public class SmsViewModel : BaseViewModel
{
string recipient;
string messageText;

public SmsViewModel()
{
SendSmsCommand = new Command(OnSendSms);
}

public string Recipient
{
get => recipient;
set => base.SetProperty(ref recipient, value, onChanged: OnChange);
}

public string MessageText
{
get => messageText;
set => SetProperty(ref messageText, value, onChanged: OnChange);
}

public ICommand SendSmsCommand { get; }

public bool CanSend => IsSupported && IsValid;

public bool IsSupported => Sms.IsComposeSupported;

public bool IsValid => !string.IsNullOrWhiteSpace(MessageText) && !string.IsNullOrWhiteSpace(Recipient);

private void OnChange()
{
OnPropertyChanged(nameof(CanSend));
OnPropertyChanged(nameof(IsSupported));
OnPropertyChanged(nameof(IsValid));
}

async void OnSendSms()
{
if (IsBusy)
return;
IsBusy = true;

var message = new SmsMessage(MessageText, Recipient);
await Sms.ComposeAsync(message);

IsBusy = false;
}
}
}