diff --git a/Billing.Shared/App.cs b/Billing.Shared/App.cs index d5ff537..dc9cd94 100644 --- a/Billing.Shared/App.cs +++ b/Billing.Shared/App.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Billing.Languages; @@ -14,15 +13,19 @@ namespace Billing { public class App : Application { + private const string SaveLocationKey = nameof(SaveLocationKey); + public static AppTheme CurrentTheme { get; private set; } public static PlatformCulture CurrentCulture { get; private set; } public static List Bills => bills ??= new List(); public static List Accounts => accounts ??= new List(); public static List Categories => categories ??= new List(); + public static bool SaveLocation => saveLocation; private static List bills; private static List accounts; private static List categories; + private static bool saveLocation; private string initialUrl; @@ -48,15 +51,6 @@ namespace Billing } } - public static async Task InitializeData() - { - var instance = await StoreHelper.Instance; - await Task.WhenAll( - Task.Run(async () => accounts = await instance.GetListAsync()), - Task.Run(async () => categories = await instance.GetListAsync()), - Task.Run(async () => bills = await instance.GetListAsync())); - } - protected override void OnResume() { SetTheme(AppInfo.RequestedTheme); @@ -93,6 +87,21 @@ namespace Billing Resources = instance; } + public static void SetSaveLocation(bool flag) + { + saveLocation = flag; + Preferences.Set(SaveLocationKey, flag); + } + + public static async Task InitializeData() + { + var instance = await StoreHelper.Instance; + await Task.WhenAll( + Task.Run(async () => accounts = await instance.GetListAsync()), + Task.Run(async () => categories = await instance.GetListAsync()), + Task.Run(async () => bills = await instance.GetListAsync())); + } + public static async Task OpenUrl(string url) { if (string.IsNullOrEmpty(url)) @@ -101,12 +110,7 @@ namespace Billing } if (File.Exists(url)) { - var status = await Permissions.CheckStatusAsync(); - if (status != PermissionStatus.Disabled && - status != PermissionStatus.Granted) - { - status = await Permissions.RequestAsync(); - } + var status = await Helper.CheckAndRequestPermissionAsync(); if (status != PermissionStatus.Granted) { return false; diff --git a/Billing.Shared/Helper.cs b/Billing.Shared/Helper.cs index c698ef5..69223a3 100644 --- a/Billing.Shared/Helper.cs +++ b/Billing.Shared/Helper.cs @@ -136,6 +136,17 @@ namespace Billing } public delegate void PropertyValueChanged(TOwner obj, TResult old, TResult @new); + + public static async Task CheckAndRequestPermissionAsync() where T : Permissions.BasePermission, new() + { + var status = await Permissions.CheckStatusAsync(); + if (status != PermissionStatus.Disabled && + status != PermissionStatus.Granted) + { + status = await Permissions.RequestAsync(); + } + return status; + } } public class AsyncLazy diff --git a/Billing.Shared/Languages/en.xml b/Billing.Shared/Languages/en.xml index a604265..dd0f4e9 100644 --- a/Billing.Shared/Languages/en.xml +++ b/Billing.Shared/Languages/en.xml @@ -101,6 +101,7 @@ Feature Category Management Detail + Save Location Add Category Are you sure you want to delete the category: {0}? Select Category diff --git a/Billing.Shared/Languages/zh-CN.xml b/Billing.Shared/Languages/zh-CN.xml index 79f9426..e25f4b4 100644 --- a/Billing.Shared/Languages/zh-CN.xml +++ b/Billing.Shared/Languages/zh-CN.xml @@ -101,6 +101,7 @@ 功能 分类管理 详细 + 保存位置 新建分类 是否确认删除该分类:{0}? 选择类别 diff --git a/Billing.Shared/Models/Bill.cs b/Billing.Shared/Models/Bill.cs index bd11615..e04b279 100644 --- a/Billing.Shared/Models/Bill.cs +++ b/Billing.Shared/Models/Bill.cs @@ -14,5 +14,10 @@ namespace Billing.Models public string Store { get; set; } public DateTime CreateTime { get; set; } public string Note { get; set; } + public double? Latitude { get; set; } + public double? Longitude { get; set; } + public double? Altitude { get; set; } + public double? Accuracy { get; set; } + public bool? IsGps { get; set; } } } \ No newline at end of file diff --git a/Billing.Shared/Views/AddBillPage.xaml.cs b/Billing.Shared/Views/AddBillPage.xaml.cs index d93a088..480ace3 100644 --- a/Billing.Shared/Views/AddBillPage.xaml.cs +++ b/Billing.Shared/Views/AddBillPage.xaml.cs @@ -1,7 +1,8 @@ using System; using System.Globalization; using System.Linq; -using Billing.Languages; +using System.Threading; +using System.Threading.Tasks; using Billing.Models; using Billing.Store; using Billing.UI; @@ -13,6 +14,7 @@ namespace Billing.Views { public partial class AddBillPage : BillingPage { + private static readonly BindableProperty CheckBillProperty = Helper.Create(nameof(CheckBill), defaultValue: new Command(() => { }, () => false)); private static readonly BindableProperty AmountProperty = Helper.Create(nameof(Amount)); private static readonly BindableProperty NameProperty = Helper.Create(nameof(Name)); private static readonly BindableProperty CategoryProperty = Helper.Create(nameof(Category)); @@ -22,6 +24,7 @@ namespace Billing.Views private static readonly BindableProperty CreatedTimeProperty = Helper.Create(nameof(CreatedTime)); private static readonly BindableProperty NoteProperty = Helper.Create(nameof(Note)); + public Command CheckBill => (Command)GetValue(CheckBillProperty); public string Amount { get => (string)GetValue(AmountProperty); @@ -55,7 +58,6 @@ namespace Billing.Views set => SetValue(NoteProperty, value); } - public Command CheckBill { get; } public Command SelectCategory { get; } public Command SelectWallet { get; } @@ -65,11 +67,12 @@ namespace Billing.Views private readonly DateTime createDate; private bool categoryChanged; + private CancellationTokenSource tokenSource; + private Location location; public AddBillPage(DateTime date) { createDate = date; - CheckBill = new Command(OnCheckBill); SelectCategory = new Command(OnSelectCategory); SelectWallet = new Command(OnSelectWallet); InitializeComponent(); @@ -81,7 +84,6 @@ namespace Billing.Views public AddBillPage(Bill bill) { this.bill = bill; - CheckBill = new Command(OnCheckBill); SelectCategory = new Command(OnSelectCategory); SelectWallet = new Command(OnSelectWallet); InitializeComponent(); @@ -90,6 +92,15 @@ namespace Billing.Views Initial(); } + protected override void OnDisappearing() + { + if (tokenSource != null && !tokenSource.IsCancellationRequested) + { + tokenSource.Cancel(); + } + base.OnDisappearing(); + } + private void Initial() { if (bill != null) @@ -116,6 +127,41 @@ namespace Billing.Views protected override void OnLoaded() { editorAmount.SetFocus(); + + if (App.SaveLocation) + { + _ = GetCurrentLocation(); + } + else + { + SetValue(CheckBillProperty, new Command(OnCheckBill)); + } + } + + private async Task GetCurrentLocation() + { + try + { + var request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10)); + tokenSource = new CancellationTokenSource(); + var status = await Helper.CheckAndRequestPermissionAsync(); + if (status != PermissionStatus.Granted) + { + return; + } + location = await Geolocation.GetLocationAsync(request, tokenSource.Token); + } + catch (FeatureNotSupportedException) { } + catch (FeatureNotEnabledException) { } + catch (PermissionException) { } + catch (Exception ex) + { + Helper.Error("location.get", ex); + } + finally + { + SetValue(CheckBillProperty, new Command(OnCheckBill)); + } } private async void OnCheckBill() @@ -148,26 +194,27 @@ namespace Billing.Views { name = category.Name; } - if (bill != null) - { - bill.Amount = amount; - bill.Name = name; - bill.CategoryId = category.Id; - bill.WalletId = wallet.Id; - bill.CreateTime = CreatedDate.Date.Add(CreatedTime); - bill.Store = Store; - bill.Note = Note; + Bill b = bill; + if (b == null) + { + b = new Bill(); + } + b.Amount = amount; + b.Name = name; + b.CategoryId = category.Id; + b.WalletId = wallet.Id; + b.CreateTime = CreatedDate.Date.Add(CreatedTime); + b.Store = Store; + b.Note = Note; + if (location != null) + { + b.Latitude = location.Latitude; + b.Longitude = location.Longitude; + b.Altitude = location.Altitude; + b.Accuracy = location.Accuracy; + b.IsGps = location.IsFromMockProvider; } - BillChecked?.Invoke(this, bill ?? new Bill - { - Amount = amount, - Name = name, - CategoryId = category.Id, - WalletId = wallet.Id, - CreateTime = CreatedDate.Date.Add(CreatedTime), - Store = Store, - Note = Note - }); + BillChecked?.Invoke(this, b); category.LastAccountId = wallet.Id; category.LastUsed = DateTime.Now; diff --git a/Billing.Shared/Views/RankPage.xaml.cs b/Billing.Shared/Views/RankPage.xaml.cs index 552f6c2..b7957d6 100644 --- a/Billing.Shared/Views/RankPage.xaml.cs +++ b/Billing.Shared/Views/RankPage.xaml.cs @@ -189,13 +189,17 @@ namespace Billing.Views LeftCommand = new Command(OnLeftCommand); RightCommand = new Command(OnRightCommand); FilterCommand = new Command(OnFilterCommand); - EditBilling = new Command(OnEditBilling); - - var style = SKFontManager.Default.GetFontStyles("PingFang SC"); + EditBilling = new Command(OnEditBilling); + +#if __IOS__ + var style = SKFontManager.Default.GetFontStyles("PingFang SC"); if (style != null) { font = style.CreateTypeface(SKFontStyle.Normal); } + else +#endif + font = SKFontManager.Default.MatchCharacter(0x4e00); DateTypes = new List { diff --git a/Billing.Shared/Views/SettingPage.xaml b/Billing.Shared/Views/SettingPage.xaml index da7753e..4aa525e 100644 --- a/Billing.Shared/Views/SettingPage.xaml +++ b/Billing.Shared/Views/SettingPage.xaml @@ -24,6 +24,8 @@ + (nameof(Version)); + private static readonly BindableProperty SaveLocationProperty = Helper.Create(nameof(SaveLocation)); private static readonly BindableProperty PrimaryColorProperty = Helper.Create(nameof(PrimaryColor)); private static readonly BindableProperty ManyRecordsProperty = Helper.Create(nameof(ManyRecords)); public string Version => (string)GetValue(VersionProperty); + public bool SaveLocation + { + get => (bool)GetValue(SaveLocationProperty); + set => SetValue(SaveLocationProperty, value); + } public string PrimaryColor { get => (string)GetValue(PrimaryColorProperty); @@ -35,16 +41,14 @@ namespace Billing.Views ShareLogsCommand = new Command(OnShareLogsCommand); InitializeComponent(); - var main = AppInfo.VersionString; - var build = AppInfo.BuildString; - SetValue(VersionProperty, $"{main} ({build})"); + SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})"); } protected override async void OnAppearing() { base.OnAppearing(); - //SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})"); + SaveLocation = App.SaveLocation; var colorString = Preferences.Get(Definition.PrimaryColorKey, Helper.DEFAULT_COLOR); PrimaryColor = Helper.WrapColorString(colorString); @@ -56,8 +60,8 @@ namespace Billing.Views { base.OnDisappearing(); + App.SetSaveLocation(SaveLocation); Preferences.Set(Definition.PrimaryColorKey, PrimaryColor); - //Light.Instance.RefreshColor(Color.FromHex(color)); } protected override async void OnRefresh() diff --git a/Billing/Billing.Android/MainActivity.cs b/Billing/Billing.Android/MainActivity.cs index 51d68ff..edb5074 100644 --- a/Billing/Billing.Android/MainActivity.cs +++ b/Billing/Billing.Android/MainActivity.cs @@ -4,6 +4,9 @@ using Android.Content.PM; using Android.Runtime; using Android.OS; using Android.Net; +using System.Collections.Generic; +using Android.Provider; +using Android.Database; namespace Billing.Droid { @@ -23,11 +26,7 @@ namespace Billing.Droid string url; if (Intent.ActionView.Equals(Intent.Action) && Intent.Data is Uri uri) { - url = uri.Path; - if (url != null && url.StartsWith("/root")) - { - url = url[5..]; - } + url = GetFilePath(BaseContext, uri); } else { @@ -44,5 +43,74 @@ namespace Billing.Droid base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } + + private string GetFilePath(Context context, Uri uri) + { + if (DocumentsContract.IsDocumentUri(context, uri)) + { + Uri contentUri; + string[] split; + switch (uri.Authority) + { + case "com.android.externalstorage.documents": + split = DocumentsContract.GetDocumentId(uri).Split(':'); + if (split[0] == "primary") + { + var external = ExternalCacheDir.Path; + external = external[..external.IndexOf("/Android/")]; + return external + "/" + split[1]; + } + break; + + case "com.android.providers.downloads.documents": + contentUri = ContentUris.WithAppendedId( + Uri.Parse("content://downloads/public_downloads"), + long.Parse(DocumentsContract.GetDocumentId(uri))); + return GetDataColumn(context, contentUri, null, null); + + case "com.android.providers.media.documents": + split = DocumentsContract.GetDocumentId(uri).Split(':'); + contentUri = split[0] switch + { + "image" => MediaStore.Images.Media.ExternalContentUri, + "video" => MediaStore.Video.Media.ExternalContentUri, + "audio" => MediaStore.Audio.Media.ExternalContentUri, + _ => null + }; + return GetDataColumn(context, contentUri, "_id=?", new[] { split[1] }); + } + } + else if (uri.Scheme == "content") + { + return GetDataColumn(context, uri, null, null); + } + else if (uri.Scheme == "file") + { + return uri.Path; + } + return null; + } + + private string GetDataColumn(Context context, Uri uri, string selection, string[] selectionArgs) + { + ICursor cursor = null; + try + { + cursor = context.ContentResolver.Query(uri, new[] { "_data" }, selection, selectionArgs, null); + if (cursor != null && cursor.MoveToFirst()) + { + var index = cursor.GetColumnIndexOrThrow("_data"); + return cursor.GetString(index); + } + } + finally + { + if (cursor != null) + { + cursor.Close(); + } + } + return null; + } } } \ No newline at end of file diff --git a/Billing/Billing.Android/Properties/AndroidManifest.xml b/Billing/Billing.Android/Properties/AndroidManifest.xml index 555f42d..a984595 100644 --- a/Billing/Billing.Android/Properties/AndroidManifest.xml +++ b/Billing/Billing.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/Billing/Billing.Android/Properties/AssemblyInfo.cs b/Billing/Billing.Android/Properties/AssemblyInfo.cs index 8c2ea37..e756626 100644 --- a/Billing/Billing.Android/Properties/AssemblyInfo.cs +++ b/Billing/Billing.Android/Properties/AssemblyInfo.cs @@ -30,3 +30,8 @@ using Android.App; [assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)] [assembly: UsesPermission(Android.Manifest.Permission.ManageExternalStorage)] [assembly: UsesPermission(Android.Manifest.Permission.Vibrate)] +[assembly: UsesPermission(Android.Manifest.Permission.AccessCoarseLocation)] +[assembly: UsesPermission(Android.Manifest.Permission.AccessFineLocation)] +[assembly: UsesFeature("android.hardware.location", Required = false)] +[assembly: UsesFeature("android.hardware.location.gps", Required = false)] +[assembly: UsesFeature("android.hardware.location.network", Required = false)] diff --git a/Billing/Billing.Android/SplashActivity.cs b/Billing/Billing.Android/SplashActivity.cs index 286ffe2..354a998 100644 --- a/Billing/Billing.Android/SplashActivity.cs +++ b/Billing/Billing.Android/SplashActivity.cs @@ -15,9 +15,9 @@ namespace Billing.Droid [IntentFilter( new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, - DataScheme = "file", + //DataScheme = "file", DataMimeType = "*/*", - DataPathPattern = ".*\\\\.db3")] + DataPathPattern = ".*\\.db3")] public class SplashActivity : AppCompatActivity { public override void OnCreate(Bundle savedInstanceState, PersistableBundle persistentState) diff --git a/Billing/Billing.iOS/Base.lproj/InfoPlist.strings b/Billing/Billing.iOS/Base.lproj/InfoPlist.strings index 18ab93e..19f66b9 100644 --- a/Billing/Billing.iOS/Base.lproj/InfoPlist.strings +++ b/Billing/Billing.iOS/Base.lproj/InfoPlist.strings @@ -1 +1,2 @@ -"CFBundleDisplayName" = "Billing"; \ No newline at end of file +"CFBundleDisplayName" = "Billing"; +"NSLocationWhenInUseUsageDescription" = "When "Save Location" is checked, the Billing app needs to access the location."; \ No newline at end of file diff --git a/Billing/Billing.iOS/Info.plist b/Billing/Billing.iOS/Info.plist index 5b8fa19..dce218b 100644 --- a/Billing/Billing.iOS/Info.plist +++ b/Billing/Billing.iOS/Info.plist @@ -80,9 +80,9 @@ CFBundleVersion - 15 + 16 CFBundleShortVersionString - 1.1.316 + 1.2.317 LSApplicationQueriesSchemes mailto @@ -91,5 +91,7 @@ LSSupportsOpeningDocumentsInPlace + NSLocationWhenInUseUsageDescription + When "Save Location" is checked, the Billing app needs to access the location. diff --git a/Billing/Billing.iOS/zh-Hans.lproj/InfoPlist.strings b/Billing/Billing.iOS/zh-Hans.lproj/InfoPlist.strings index 4149251..e009fa3 100644 --- a/Billing/Billing.iOS/zh-Hans.lproj/InfoPlist.strings +++ b/Billing/Billing.iOS/zh-Hans.lproj/InfoPlist.strings @@ -1 +1,2 @@ -"CFBundleDisplayName" = "记账本"; \ No newline at end of file +"CFBundleDisplayName" = "记账本"; +"NSLocationWhenInUseUsageDescription" = "当选中“保存位置”时,记账本需要访问位置信息。"; \ No newline at end of file