mirror of
https://github.com/bitwarden/mobile
synced 2025-12-22 03:03:46 +00:00
EC-395 Apple Watch MVP (#2228)
* [EC-426] Add watchOS PoC app (#2054) * EC-426 Added watchOS app, configured iOS.csproj to bundle the output of XCode build into the Xamarin iOS app and added some custom logic to use WCSession to communicate between the iOS and the watchOS apps * EC-426 Removed Info.plist from iOS.Core project given that it's not needed * [EC-426] Added new encrypted watch app profiles * EC-426 added configuration for building watchApp and bundle it up on the iOS one * EC-426 Fix build for watchOS * EC-426 Fix build for watchOS applied shell bash * EC-426 Fix build for watchOS echo * EC-426 Fix build for watchOS simplify * EC-426 Fix build for watchOS added workspace path * EC-426 Changed code sign identity of watchOS project to Apple Distribution * EC-426 added manual code sign style and specified the provisioning profile for the targets on the watch xcode project * EC-426 updated path to watchOS on release on iOS.csproj and disabled android and f-.droid * EC-426 fix build * EC-426 fix path and check listing of directory of watchOS output just in case * EC-426 Fix Apple Watch build to list the folder recursively just in case we need to change the path for the watch bundle * EC-426 TEMP Change texts on input on login and lock to show that the app is for the Watch PoC testing * EC-426 Fix WatchApp build path * EC-426 Added WatchOS AppIcons * EC-426 added gitignore for XCode project removed files supposed to be ignored * EC-426 Cleaned the code a bit to avoid misbehavior * EC-426 Code cleanup Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com> * [EC-585] Added data, encryption and some helpers and structure to the Watch app (#2164) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * [EC-614] Apple Watch MVP Cipher list UI (#2175) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * [EC-615] Apple Watch MVP Cipher details UI (#2192) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * EC-615 Added cipher details UI to watch and also implemented logic and helpers to generate the TOTPs * EC-615 Added value transformer to login uris on the cipher entity * EC-617 Added state view on watch app and some state helpers and wired it on the CipherListView. Also added some images (#2195) * [EC-581] Implement Apple Watch MVP Sync (#2206) * EC-581 Implemented sync iPhone -> watchOS, fix some issues with the watch database and sync flows for login/locks/multiple accounts * EC-581 Added watch sync on unlocking and need setup state when no user is synced and the session is not active * EC-581 Removed unused method * EC-581 Fix format * EC-759 Added avatar row on cipher list header to display avatar icon and email (#2213) * [EC-786] Apple Watch MVP Sync fixes (#2214) * EC-786 Commented things that are not going to be included on the MVP and fixed issue on the dictionary sent on the applicationContext to have a changing key based on time * EC-786 Commented need unlock state * EC-579 Added logic for Connect To Watch on iOS settings and moved it to the correct place. Also improved the synchronization and watch session activation logic (#2218) * EC-616 Added search header for ciphers and polished the code (#2226) Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com>
This commit is contained in:
48
src/iOS.Core/Services/WatchDeviceService.cs
Normal file
48
src/iOS.Core/Services/WatchDeviceService.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Services;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
using WatchConnectivity;
|
||||
|
||||
namespace Bit.iOS.Core.Services
|
||||
{
|
||||
public class WatchDeviceService : BaseWatchDeviceService
|
||||
{
|
||||
public WatchDeviceService(ICipherService cipherService,
|
||||
IEnvironmentService environmentService,
|
||||
IStateService stateService,
|
||||
IVaultTimeoutService vaultTimeoutService)
|
||||
: base(cipherService, environmentService, stateService, vaultTimeoutService)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsConnected => WCSessionManager.SharedManager.IsSessionActivated;
|
||||
|
||||
protected override bool CanSendData => WCSessionManager.SharedManager.IsValidSession;
|
||||
|
||||
protected override bool IsSupported => WCSession.IsSupported;
|
||||
|
||||
protected override Task SendDataToWatchAsync(WatchDTO watchDto)
|
||||
{
|
||||
var serializedData = JsonConvert.SerializeObject(watchDto);
|
||||
|
||||
// Add time to the key to make it change on every message sent so it's delivered faster.
|
||||
// If we use the same key then the OS may defer the delivery of the message because of
|
||||
// resources, reachability and other stuff
|
||||
WCSessionManager.SharedManager.SendBackgroundHighPriorityMessage(new Dictionary<string, object>
|
||||
{
|
||||
[$"watchDto-{DateTime.UtcNow.ToLongTimeString()}"] = serializedData
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void ConnectToWatch()
|
||||
{
|
||||
WCSessionManager.SharedManager.StartSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/iOS.Core/Utilities/DictionaryExtensions.cs
Normal file
26
src/iOS.Core/Utilities/DictionaryExtensions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Foundation;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static NSDictionary<NSString, NSObject> ToNSDictionary(this Dictionary<string, object> dict)
|
||||
{
|
||||
return dict.ToNSDictionary(k => new NSString(k), v => (NSObject)new NSString(JsonConvert.SerializeObject(v)));
|
||||
}
|
||||
|
||||
public static NSDictionary<KTo,VTo> ToNSDictionary<KFrom,VFrom,KTo,VTo>(this Dictionary<KFrom, VFrom> dict, Func<KFrom, KTo> keyConverter, Func<VFrom, VTo> valueConverter)
|
||||
where KTo : NSObject
|
||||
where VTo : NSObject
|
||||
{
|
||||
var NSValues = dict.Values.Select(x => valueConverter(x)).ToArray();
|
||||
var NSKeys = dict.Keys.Select(x => keyConverter(x)).ToArray();
|
||||
return NSDictionary<KTo, VTo>.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
185
src/iOS.Core/Utilities/WCSessionManager.cs
Normal file
185
src/iOS.Core/Utilities/WCSessionManager.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Foundation;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace WatchConnectivity
|
||||
{
|
||||
public sealed class WCSessionManager : WCSessionDelegate
|
||||
{
|
||||
// Setup is converted from https://www.natashatherobot.com/watchconnectivity-say-hello-to-wcsession/
|
||||
// with some extra bits
|
||||
private static readonly WCSessionManager sharedManager = new WCSessionManager();
|
||||
private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null;
|
||||
|
||||
public static string Device = "Phone";
|
||||
|
||||
public event WCSessionReceiveDataHandler ApplicationContextUpdated;
|
||||
public event WCSessionReceiveDataHandler MessagedReceived;
|
||||
public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary<string, object> applicationContext);
|
||||
|
||||
|
||||
private WCSession validSession
|
||||
{
|
||||
get
|
||||
{
|
||||
Console.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n");
|
||||
Console.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n");
|
||||
return (session.Paired && session.WatchAppInstalled) ? session : null;
|
||||
}
|
||||
}
|
||||
|
||||
private WCSession validReachableSession
|
||||
{
|
||||
get
|
||||
{
|
||||
return session.Reachable ? validSession : null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValidSession => validSession != null;
|
||||
|
||||
public bool IsSessionReachable => session.Reachable;
|
||||
|
||||
public bool IsSessionActivated => validSession?.ActivationState == WCSessionActivationState.Activated;
|
||||
|
||||
private WCSessionManager() : base() { }
|
||||
|
||||
public static WCSessionManager SharedManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return sharedManager;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartSession()
|
||||
{
|
||||
if (session != null)
|
||||
{
|
||||
session.Delegate = this;
|
||||
session.ActivateSession();
|
||||
Console.WriteLine($"Started Watch Connectivity Session on {Device}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void SessionReachabilityDidChange(WCSession session)
|
||||
{
|
||||
Console.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')} from {Device}");
|
||||
// handle session reachability change
|
||||
if (session.Reachable)
|
||||
{
|
||||
// great! continue on with Interactive Messaging
|
||||
}
|
||||
else
|
||||
{
|
||||
// 😥 prompt the user to unlock their iOS device
|
||||
}
|
||||
}
|
||||
|
||||
#region Application Context Methods
|
||||
|
||||
public void SendBackgroundHighPriorityMessage(Dictionary<string, object> applicationContext)
|
||||
{
|
||||
// Application context doesnt need the watch to be reachable, it will be received when opened
|
||||
if (validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var sendSuccessfully = validSession.UpdateApplicationContext(applicationContext.ToNSDictionary(), out var error);
|
||||
if (sendSuccessfully)
|
||||
{
|
||||
Console.WriteLine($"Sent App Context from {Device} \nPayLoad: {applicationContext.ToNSDictionary().ToString()} \n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Error Updating Application Context: {error.LocalizedDescription}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Exception Updating Application Context: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
WCSessionUserInfoTransfer _transf;
|
||||
public void SendBackgroundFifoHighPriorityMessage(Dictionary<string, object> message)
|
||||
{
|
||||
if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_transf?.Cancel();
|
||||
|
||||
Console.WriteLine("Started transferring user info");
|
||||
|
||||
_transf = session.TransferUserInfo(message.ToNSDictionary());
|
||||
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_transf.Transferring)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
Console.WriteLine("Finished transferring user info");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Error transferring user info " + ex);
|
||||
}
|
||||
});
|
||||
|
||||
//session.SendMessage(dic,
|
||||
// (dd) =>
|
||||
// {
|
||||
// Console.WriteLine(dd?.ToString());
|
||||
// },
|
||||
// error =>
|
||||
// {
|
||||
// Console.WriteLine(error?.ToString());
|
||||
// }
|
||||
//);
|
||||
}
|
||||
|
||||
public override void DidReceiveApplicationContext(WCSession session, NSDictionary<NSString, NSObject> applicationContext)
|
||||
{
|
||||
Console.WriteLine($"Receiving Message on {Device}");
|
||||
if (ApplicationContextUpdated != null)
|
||||
{
|
||||
var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray();
|
||||
var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray();
|
||||
var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
ApplicationContextUpdated(session, dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void DidReceiveMessage(WCSession session, NSDictionary<NSString, NSObject> message)
|
||||
{
|
||||
Console.WriteLine($"Receiving Message on {Device}");
|
||||
|
||||
var keys = message.Keys.Select(k => k.ToString()).ToArray();
|
||||
var values = message.Values.Select(v => v?.ToString() as object).ToArray();
|
||||
var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
MessagedReceived?.Invoke(session, dictionary);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,12 @@ namespace Bit.iOS.Core.Utilities
|
||||
clearCipherCacheKey,
|
||||
Bit.Core.Constants.iOSAllClearCipherCacheKeys);
|
||||
InitLogger();
|
||||
|
||||
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>()));
|
||||
|
||||
Bootstrap();
|
||||
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
@@ -226,7 +232,8 @@ namespace Bit.iOS.Core.Utilities
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||
ServiceContainer.Resolve<ILogger>("logger"),
|
||||
ServiceContainer.Resolve<IMessagingService>("messagingService"));
|
||||
ServiceContainer.Resolve<IMessagingService>("messagingService"),
|
||||
ServiceContainer.Resolve<IWatchDeviceService>());
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
|
||||
if (postBootstrapFunc != null)
|
||||
|
||||
@@ -204,9 +204,12 @@
|
||||
<Compile Include="Renderers\CollectionView\CollectionException.cs" />
|
||||
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewDelegator.cs" />
|
||||
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
||||
<Compile Include="Utilities\WCSessionManager.cs" />
|
||||
<Compile Include="Services\FileService.cs" />
|
||||
<Compile Include="Utilities\UIViewControllerExtensions.cs" />
|
||||
<Compile Include="Services\AutofillHandler.cs" />
|
||||
<Compile Include="Utilities\DictionaryExtensions.cs" />
|
||||
<Compile Include="Services\WatchDeviceService.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\App\App.csproj">
|
||||
|
||||
Reference in New Issue
Block a user