diff --git a/AuthenticatorPro.Droid.Shared/AuthenticatorPro.Droid.Shared.csproj b/AuthenticatorPro.Droid.Shared/AuthenticatorPro.Droid.Shared.csproj index 9ac84cc527..c77ebe75ab 100644 --- a/AuthenticatorPro.Droid.Shared/AuthenticatorPro.Droid.Shared.csproj +++ b/AuthenticatorPro.Droid.Shared/AuthenticatorPro.Droid.Shared.csproj @@ -50,6 +50,7 @@ + diff --git a/AuthenticatorPro.Droid.Shared/Source/Query/WearPreferences.cs b/AuthenticatorPro.Droid.Shared/Source/Query/WearPreferences.cs new file mode 100644 index 0000000000..18a7a160c9 --- /dev/null +++ b/AuthenticatorPro.Droid.Shared/Source/Query/WearPreferences.cs @@ -0,0 +1,12 @@ +namespace AuthenticatorPro.Droid.Shared.Query +{ + public class WearPreferences + { + public readonly string DefaultCategory; + + public WearPreferences(string defaultCategory) + { + DefaultCategory = defaultCategory; + } + } +} \ No newline at end of file diff --git a/AuthenticatorPro.Droid/Resources/values/wear.xml b/AuthenticatorPro.Droid/Resources/values/wear.xml index a7258cf2bb..e86c79221d 100644 --- a/AuthenticatorPro.Droid/Resources/values/wear.xml +++ b/AuthenticatorPro.Droid/Resources/values/wear.xml @@ -1,6 +1,6 @@  - protocol_v2 + protocol_v2.1 diff --git a/AuthenticatorPro.Droid/Source/Service/WearQueryService.cs b/AuthenticatorPro.Droid/Source/Service/WearQueryService.cs index f6187182ed..195649b814 100644 --- a/AuthenticatorPro.Droid/Source/Service/WearQueryService.cs +++ b/AuthenticatorPro.Droid/Source/Service/WearQueryService.cs @@ -27,6 +27,7 @@ internal class WearQueryService : WearableListenerService private const string ListCategoriesCapability = "list_categories"; private const string ListCustomIconsCapability = "list_custom_icons"; private const string GetCustomIconCapability = "get_custom_icon"; + private const string GetPreferencesCapability = "get_preferences"; private readonly Lazy _initTask; @@ -127,6 +128,17 @@ await WearableClass.GetMessageClient(this) .SendMessageAsync(nodeId, GetCustomIconCapability, data); } + private async Task GetPreferences(string nodeId) + { + var preferences = new PreferenceWrapper(this); + var settings = new WearPreferences(preferences.DefaultCategory); + var json = JsonConvert.SerializeObject(settings); + var data = Encoding.UTF8.GetBytes(json); + + await WearableClass.GetMessageClient(this) + .SendMessageAsync(nodeId, GetPreferencesCapability, data); + } + public override async void OnMessageReceived(IMessageEvent messageEvent) { await _initTask.Value; @@ -151,6 +163,10 @@ public override async void OnMessageReceived(IMessageEvent messageEvent) await GetCustomIcon(id, messageEvent.SourceNodeId); break; } + + case GetPreferencesCapability: + await GetPreferences(messageEvent.SourceNodeId); + break; } } } diff --git a/AuthenticatorPro.WearOS/AuthenticatorPro.WearOS.csproj b/AuthenticatorPro.WearOS/AuthenticatorPro.WearOS.csproj index a5de5dba64..b1f78cf040 100644 --- a/AuthenticatorPro.WearOS/AuthenticatorPro.WearOS.csproj +++ b/AuthenticatorPro.WearOS/AuthenticatorPro.WearOS.csproj @@ -1,4 +1,4 @@ - + Debug @@ -85,6 +85,7 @@ + @@ -124,6 +125,9 @@ 2.2.0.4 + + 1.1.1.6 + 1.1.0.1 diff --git a/AuthenticatorPro.WearOS/Source/Activity/MainActivity.cs b/AuthenticatorPro.WearOS/Source/Activity/MainActivity.cs index 63ae7a1b40..f4df97eb4e 100644 --- a/AuthenticatorPro.WearOS/Source/Activity/MainActivity.cs +++ b/AuthenticatorPro.WearOS/Source/Activity/MainActivity.cs @@ -19,6 +19,7 @@ using AuthenticatorPro.WearOS.Cache; using AuthenticatorPro.WearOS.Data; using AuthenticatorPro.WearOS.List; +using AuthenticatorPro.WearOS.Util; using Newtonsoft.Json; @@ -28,11 +29,12 @@ namespace AuthenticatorPro.WearOS.Activity internal class MainActivity : AppCompatActivity, MessageClient.IOnMessageReceivedListener { // Query Paths - private const string ProtocolVersion = "protocol_v2"; + private const string ProtocolVersion = "protocol_v2.1"; private const string ListAuthenticatorsCapability = "list_authenticators"; private const string ListCategoriesCapability = "list_categories"; private const string ListCustomIconsCapability = "list_custom_icons"; private const string GetCustomIconCapability = "get_custom_icon"; + private const string GetPreferencesCapability = "get_preferences"; private const string RefreshCapability = "refresh"; // Cache Names @@ -44,12 +46,14 @@ internal class MainActivity : AppCompatActivity, MessageClient.IOnMessageReceive private RelativeLayout _loadingLayout; private RelativeLayout _emptyLayout; private WearableRecyclerView _authList; + private WearableNavigationDrawerView _categoryList; // Data private AuthenticatorSource _authSource; private ListCache _authCache; private ListCache _categoryCache; private CustomIconCache _customIconCache; + private PreferenceWrapper _preferences; private AuthenticatorListAdapter _authListAdapter; private CategoryListAdapter _categoryListAdapter; @@ -61,11 +65,13 @@ internal class MainActivity : AppCompatActivity, MessageClient.IOnMessageReceive // Lifecycle Synchronisation private readonly SemaphoreSlim _onCreateLock; + private readonly SemaphoreSlim _refreshLock; public MainActivity() { - _onCreateLock = new SemaphoreSlim(1, 1); + _onCreateLock = new SemaphoreSlim(1, 1); + _refreshLock = new SemaphoreSlim(1, 1); } #region Activity Lifecycle @@ -76,6 +82,8 @@ protected override async void OnCreate(Bundle bundle) await _onCreateLock.WaitAsync(); SetContentView(Resource.Layout.activityMain); + _preferences = new PreferenceWrapper(this); + _authCache = new ListCache(AuthenticatorCacheName, this); _categoryCache = new ListCache(CategoryCacheName, this); _customIconCache = new CustomIconCache(this); @@ -103,12 +111,36 @@ protected override async void OnResume() } catch(ApiException) { - RunOnUiThread(UpdateViewState); + RunOnUiThread(CheckOfflineState); return; } - RunOnUiThread(UpdateViewState); + RunOnUiThread(CheckOfflineState); await Refresh(); + + await _refreshLock.WaitAsync(); + _refreshLock.Release(); + + var defaultCategory = _preferences.DefaultCategory; + + if(defaultCategory == null) + { + RunOnUiThread(CheckEmptyState); + return; + } + + _authSource.SetCategory(defaultCategory); + var position = _categoryCache.FindIndex(c => c.Id == defaultCategory) + 1; + + if(position < 0) + return; + + RunOnUiThread(delegate + { + _categoryList.SetCurrentItem(position, false); + _authListAdapter.NotifyDataSetChanged(); + CheckEmptyState(); + }); } protected override async void OnPause() @@ -144,17 +176,17 @@ private void InitViews() _authListAdapter.HasStableIds = true; _authList.SetAdapter(_authListAdapter); - var categoriesDrawer = FindViewById(Resource.Id.drawerCategories); + _categoryList = FindViewById(Resource.Id.drawerCategories); _categoryListAdapter = new CategoryListAdapter(this, _categoryCache); - categoriesDrawer.SetAdapter(_categoryListAdapter); - categoriesDrawer.ItemSelected += OnCategorySelected; + _categoryList.SetAdapter(_categoryListAdapter); + _categoryList.ItemSelected += OnCategorySelected; } private void OnCategorySelected(object sender, WearableNavigationDrawerView.ItemSelectedEventArgs e) { if(e.Pos > 0) { - var category = _categoryCache.Get(e.Pos - 1); + var category = _categoryCache[e.Pos - 1]; if(category == null) return; @@ -165,20 +197,27 @@ private void OnCategorySelected(object sender, WearableNavigationDrawerView.Item _authSource.SetCategory(null); _authListAdapter.NotifyDataSetChanged(); - UpdateViewState(); + CheckEmptyState(); + } + + private void CheckOfflineState() + { + if(_serverNode == null) + { + AnimUtil.FadeOutView(_loadingLayout, AnimUtil.LengthShort); + _offlineLayout.Visibility = ViewStates.Visible; + } + else + _offlineLayout.Visibility = ViewStates.Invisible; } - private void UpdateViewState() + private void CheckEmptyState() { if(_loadingLayout.Visibility == ViewStates.Visible) AnimUtil.FadeOutView(_loadingLayout, AnimUtil.LengthShort); _emptyLayout.Visibility = ViewStates.Gone; - _offlineLayout.Visibility = _serverNode == null - ? ViewStates.Visible - : ViewStates.Gone; - if(_authSource.GetView().Count == 0) _emptyLayout.Visibility = ViewStates.Visible; else @@ -263,18 +302,23 @@ private async Task Refresh() { if(_serverNode == null) return; + + await _refreshLock.WaitAsync(); Interlocked.Exchange(ref _responsesReceived, 0); - Interlocked.Exchange(ref _responsesRequired, 3); + Interlocked.Exchange(ref _responsesRequired, 4); - await WearableClass.GetMessageClient(this) - .SendMessageAsync(_serverNode.Id, ListAuthenticatorsCapability, new byte[] { }); - - await WearableClass.GetMessageClient(this) - .SendMessageAsync(_serverNode.Id, ListCategoriesCapability, new byte[] { }); + var client = WearableClass.GetMessageClient(this); - await WearableClass.GetMessageClient(this) - .SendMessageAsync(_serverNode.Id, ListCustomIconsCapability, new byte[] { }); + async Task MakeRequest(string capability) + { + await client.SendMessageAsync(_serverNode.Id, capability, new byte[] { }); + } + + await MakeRequest(ListAuthenticatorsCapability); + await MakeRequest(ListCategoriesCapability); + await MakeRequest(ListCustomIconsCapability); + await MakeRequest(GetPreferencesCapability); } private async Task OnAuthenticatorListReceived(byte[] data) @@ -330,6 +374,14 @@ private async Task OnCustomIconReceived(byte[] data) await _customIconCache.Add(icon.Id, icon.Data); } + + private void OnPreferencesReceived(byte[] data) + { + var json = Encoding.UTF8.GetString(data); + var prefs = JsonConvert.DeserializeObject(json); + + _preferences.DefaultCategory = prefs.DefaultCategory; + } public async void OnMessageReceived(IMessageEvent messageEvent) { @@ -350,6 +402,10 @@ public async void OnMessageReceived(IMessageEvent messageEvent) case GetCustomIconCapability: await OnCustomIconReceived(messageEvent.GetData()); break; + + case GetPreferencesCapability: + OnPreferencesReceived(messageEvent.GetData()); + break; case RefreshCapability: await Refresh(); @@ -362,7 +418,7 @@ public async void OnMessageReceived(IMessageEvent messageEvent) var required = Interlocked.CompareExchange(ref _responsesRequired, 0, 0); if(received == required) - RunOnUiThread(UpdateViewState); + _refreshLock.Release(); } #endregion } diff --git a/AuthenticatorPro.WearOS/Source/Cache/ListCache.cs b/AuthenticatorPro.WearOS/Source/Cache/ListCache.cs index d63416f424..617e7147f9 100644 --- a/AuthenticatorPro.WearOS/Source/Cache/ListCache.cs +++ b/AuthenticatorPro.WearOS/Source/Cache/ListCache.cs @@ -1,4 +1,6 @@ -using Android.Content; +using System; +using System.Collections; +using Android.Content; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,13 +11,13 @@ namespace AuthenticatorPro.WearOS.Cache { - internal class ListCache + internal class ListCache : IEnumerable { private readonly string _name; private readonly Context _context; private readonly SemaphoreSlim _flushLock; - private List _items; + public int Count => _items.Count; public ListCache(string name, Context context) @@ -48,7 +50,7 @@ public async Task Replace(List items) await Flush(); } - public bool Dirty(List items, IEqualityComparer comparer = null) + public bool Dirty(IEnumerable items, IEqualityComparer comparer = null) { return comparer != null ? !_items.SequenceEqual(items, comparer) @@ -70,14 +72,35 @@ private async Task Flush() } } - public T Get(int position) + public List GetItems() { - return _items.ElementAtOrDefault(position); + return _items; } - public List GetItems() + public bool Contains(T item) { - return _items; + return _items.Contains(item); + } + + private IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int FindIndex(Predicate predicate) + { + return _items.FindIndex(predicate); + } + + public T this[int index] + { + get => _items[index]; + set => _items[index] = value; } } } \ No newline at end of file diff --git a/AuthenticatorPro.WearOS/Source/List/CategoryListAdapter.cs b/AuthenticatorPro.WearOS/Source/List/CategoryListAdapter.cs index dda1d157a9..e3057f1d7e 100644 --- a/AuthenticatorPro.WearOS/Source/List/CategoryListAdapter.cs +++ b/AuthenticatorPro.WearOS/Source/List/CategoryListAdapter.cs @@ -29,7 +29,7 @@ public override ICharSequence GetItemTextFormatted(int pos) if(pos == 0) return new String(_context.GetString(Resource.String.categoryAll)); - var item = _cache.Get(pos - 1); + var item = _cache[pos - 1]; return item == null ? new String() diff --git a/AuthenticatorPro.WearOS/Source/Util/PreferenceWrapper.cs b/AuthenticatorPro.WearOS/Source/Util/PreferenceWrapper.cs new file mode 100644 index 0000000000..0ce3aac7d1 --- /dev/null +++ b/AuthenticatorPro.WearOS/Source/Util/PreferenceWrapper.cs @@ -0,0 +1,27 @@ +using Android.Content; +using AndroidX.Preference; + +namespace AuthenticatorPro.WearOS.Util +{ + internal class PreferenceWrapper + { + private const string DefaultCategoryKey = "defaultCategory"; + private const string DefaultCategoryDefault = null; + public string DefaultCategory + { + get => _preferences.GetString(DefaultCategoryKey, DefaultCategoryDefault); + set => SetPreference(DefaultCategoryKey, value); + } + + private readonly ISharedPreferences _preferences; + + public PreferenceWrapper(Context context) + { + _preferences = PreferenceManager.GetDefaultSharedPreferences(context); + } + private void SetPreference(string key, string value) + { + _preferences.Edit().PutString(key, value).Commit(); + } + } +} \ No newline at end of file