diff --git a/src/SilentNotes.Android/CloudStorageRedirectActivity.cs b/src/SilentNotes.Android/CloudStorageRedirectActivity.cs index 7a17763c..e5ee6932 100644 --- a/src/SilentNotes.Android/CloudStorageRedirectActivity.cs +++ b/src/SilentNotes.Android/CloudStorageRedirectActivity.cs @@ -9,7 +9,6 @@ using Android.Content.PM; using Android.OS; using SilentNotes.Services; -using SilentNotes.Services.CloudStorageServices; using SilentNotes.StoryBoards.SynchronizationStory; namespace SilentNotes.Android @@ -21,27 +20,31 @@ namespace SilentNotes.Android /// /// The flags NoHistory and LaunchMode are essential, for the app to appear at the top again. /// - [Activity(Label = "CloudStorageRedirectActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTask)] + [Activity(Label = "CloudStorageRedirectActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTop)] [IntentFilter( new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, - DataScheme = "ch.martinstoeckli.silentnotes")] + DataScheme = "ch.martinstoeckli.silentnotes", + DataPath = "/oauth2redirect")] public class CloudStorageRedirectActivity : Activity { /// protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); - System.Uri redirectUri = new Uri(Intent.Data.ToString()); // Convert Android.Net.Uri to System.Uri - // Call the storage service - IStoryBoardService storyBoardService = Ioc.GetOrCreate(); - IOauth2CloudStorageService oauthStorageService = storyBoardService.ActiveStory?.LoadFromSession( - SynchronizationStorySessionKey.OauthCloudStorageService.ToInt()); - oauthStorageService?.HandleOauth2Redirect(redirectUri); + string redirectUrl = Intent.Data.ToString(); + IStoryBoardService storyBoardService = new StoryBoardService(); + if (storyBoardService.ActiveStory != null) + storyBoardService.ActiveStory.StoreToSession(SynchronizationStorySessionKey.OauthRedirectUrl.ToInt(), redirectUrl); - // Stop the activity, its job is already done. + // Stop the redirect activity, its job is already done. Finish(); + + // Clear the activity holding the custom tab and return to already running main activity. + Intent intent = new Intent(this, typeof(MainActivity)); + intent.SetFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop); + StartActivity(intent); } } } \ No newline at end of file diff --git a/src/SilentNotes.Android/MainActivity.cs b/src/SilentNotes.Android/MainActivity.cs index 1c3ecd3a..a767f212 100644 --- a/src/SilentNotes.Android/MainActivity.cs +++ b/src/SilentNotes.Android/MainActivity.cs @@ -16,6 +16,8 @@ using SilentNotes.Controllers; using SilentNotes.HtmlView; using SilentNotes.Services; +using SilentNotes.Services.CloudStorageServices; +using SilentNotes.StoryBoards.SynchronizationStory; namespace SilentNotes.Android { @@ -71,11 +73,21 @@ protected override void OnStart() Startup.InitializeApplication(this); INavigationService navigation = Ioc.GetOrCreate(); + IStoryBoardService storyBoardService = Ioc.GetOrCreate(); + if (IsStartedBySendIntent()) { // Another app sent content to SilentNotes navigation.Navigate(ControllerNames.Note, ControllerParameters.SendToSilentnotesText, GetSendIntentText()); } + else if (IsStartedByOAuthRedirectIndent(storyBoardService)) + { + IOauth2CloudStorageService oauthStorageService = storyBoardService.ActiveStory.LoadFromSession(SynchronizationStorySessionKey.OauthCloudStorageService.ToInt()); + string redirectUrl = storyBoardService.ActiveStory.LoadFromSession(SynchronizationStorySessionKey.OauthRedirectUrl.ToInt()); + storyBoardService.ActiveStory.RemoveFromSession(SynchronizationStorySessionKey.OauthRedirectUrl.ToInt()); + + oauthStorageService?.HandleOauth2Redirect(new Uri(redirectUrl)); + } else { // Normal startup @@ -189,6 +201,12 @@ private string GetSendIntentText() return Intent.GetStringExtra(Intent.ExtraText); } + private bool IsStartedByOAuthRedirectIndent(IStoryBoardService storyBoardService) + { + return (storyBoardService.ActiveStory != null) && + storyBoardService.ActiveStory.TryLoadFromSession(SynchronizationStorySessionKey.OauthRedirectUrl.ToInt(), out string _); + } + private class WebviewValueCallback : Java.Lang.Object, IValueCallback { private readonly TaskCompletionSource _taskCompletion; diff --git a/src/SilentNotes.Android/Properties/AndroidManifest.xml b/src/SilentNotes.Android/Properties/AndroidManifest.xml index 466d23d1..e0a83b90 100644 --- a/src/SilentNotes.Android/Properties/AndroidManifest.xml +++ b/src/SilentNotes.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/src/SilentNotes.Android/Services/NativeBrowserService.cs b/src/SilentNotes.Android/Services/NativeBrowserService.cs index 5c8052fd..a82392a4 100644 --- a/src/SilentNotes.Android/Services/NativeBrowserService.cs +++ b/src/SilentNotes.Android/Services/NativeBrowserService.cs @@ -5,6 +5,7 @@ using Android.Content; using Android.Net; +using Android.Support.CustomTabs; using SilentNotes.Services; namespace SilentNotes.Android.Services @@ -32,5 +33,14 @@ public void OpenWebsite(string url) Intent intent = new Intent(Intent.ActionView, webpage); _applicationContext.StartActivity(intent); } + + /// + public void OpenWebsiteInApp(string url) + { + // Use the Android custom tabs to display the webpage inside the app. + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + CustomTabsIntent customTabsIntent = builder.Build(); + customTabsIntent.LaunchUrl(_applicationContext, Uri.Parse(url)); + } } } \ No newline at end of file diff --git a/src/SilentNotes.Shared/Services/CloudStorageServices/DropboxCloudStorageService.cs b/src/SilentNotes.Shared/Services/CloudStorageServices/DropboxCloudStorageService.cs index 584e7cdb..d21785ca 100644 --- a/src/SilentNotes.Shared/Services/CloudStorageServices/DropboxCloudStorageService.cs +++ b/src/SilentNotes.Shared/Services/CloudStorageServices/DropboxCloudStorageService.cs @@ -118,7 +118,7 @@ public void ShowOauth2LoginPage() _oauthState = CryptoUtils.GenerateRandomBase62String(16, _randomSource); Uri oauthUrl = DropboxOAuth2Helper.GetAuthorizeUri( OAuthResponseType.Token, GetAppKey(), new Uri(_redirectUrl), _oauthState); - _nativeBrowserService.OpenWebsite(oauthUrl.AbsoluteUri); + _nativeBrowserService.OpenWebsiteInApp(oauthUrl.AbsoluteUri); } /// diff --git a/src/SilentNotes.Shared/Services/INativeBrowserService.cs b/src/SilentNotes.Shared/Services/INativeBrowserService.cs index e276fa63..dda36191 100644 --- a/src/SilentNotes.Shared/Services/INativeBrowserService.cs +++ b/src/SilentNotes.Shared/Services/INativeBrowserService.cs @@ -17,5 +17,16 @@ public interface INativeBrowserService /// /// Url to the website. void OpenWebsite(string url); + + /// + /// Opens a website using the external browser inside the app if possible. + /// + /// + /// OAuth2 authentication requires a login in an external browser, but we want to avoid + /// switching between app and browser. With Androids "custom tabs" we can start the browser + /// inside the app, thus the app remains active and the browser can be closed afterwards. + /// + /// Url to the website. + void OpenWebsiteInApp(string url); } } diff --git a/src/SilentNotes.Shared/Services/StoryBoardService.cs b/src/SilentNotes.Shared/Services/StoryBoardService.cs index dd96883b..3679cfe0 100644 --- a/src/SilentNotes.Shared/Services/StoryBoardService.cs +++ b/src/SilentNotes.Shared/Services/StoryBoardService.cs @@ -12,7 +12,17 @@ namespace SilentNotes.Services /// public class StoryBoardService : IStoryBoardService { + /// + /// We store the property in this static member, so it remains + /// accessible even if the Android activity changes and the Ioc is rebuilt with new services. + /// + private static IStoryBoard _activeStory; + /// - public IStoryBoard ActiveStory { get; set; } + public IStoryBoard ActiveStory + { + get { return _activeStory; } + set { _activeStory = value; } + } } } diff --git a/src/SilentNotes.Shared/StoryBoards/SynchronizationStory/SynchronizationStoryBoard.cs b/src/SilentNotes.Shared/StoryBoards/SynchronizationStory/SynchronizationStoryBoard.cs index 0cba1aa9..d42ab77c 100644 --- a/src/SilentNotes.Shared/StoryBoards/SynchronizationStory/SynchronizationStoryBoard.cs +++ b/src/SilentNotes.Shared/StoryBoards/SynchronizationStory/SynchronizationStoryBoard.cs @@ -44,6 +44,7 @@ public enum SynchronizationStorySessionKey UserEnteredTransferCode, CloudRepository, OauthCloudStorageService, + OauthRedirectUrl, } /// diff --git a/src/SilentNotes.UWP/Services/NativeBrowserService.cs b/src/SilentNotes.UWP/Services/NativeBrowserService.cs index 062df147..affe4049 100644 --- a/src/SilentNotes.UWP/Services/NativeBrowserService.cs +++ b/src/SilentNotes.UWP/Services/NativeBrowserService.cs @@ -20,6 +20,12 @@ public void OpenWebsite(string url) OpenWebsiteAsync(url); } + /// + public void OpenWebsiteInApp(string url) + { + OpenWebsite(url); + } + private async void OpenWebsiteAsync(string url) { await Launcher.LaunchUriAsync(new Uri(url)); diff --git a/src/Tests/SilentNotesTest/Services/CloudStorageServices/DropboxCloudStorageServiceTest.cs b/src/Tests/SilentNotesTest/Services/CloudStorageServices/DropboxCloudStorageServiceTest.cs index 2129c764..287ebe7c 100644 --- a/src/Tests/SilentNotesTest/Services/CloudStorageServices/DropboxCloudStorageServiceTest.cs +++ b/src/Tests/SilentNotesTest/Services/CloudStorageServices/DropboxCloudStorageServiceTest.cs @@ -26,7 +26,7 @@ public void ShowOauth2LoginPageOpensBrowser() service.ShowOauth2LoginPage(); nativeBrowserService.Verify( - x => x.OpenWebsite(It.Is( + x => x.OpenWebsiteInApp(It.Is( s => s.Contains("https://www.dropbox.com/oauth2/authorize") && s.Contains("client_id=2drl5n333") && s.Contains("redirect_uri=ch.martinstoeckli.silentnotes") && s.Contains("response_type=token"))), Times.Once); }