Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use FileDescriptor from MediaStore for random file access #130

Merged
merged 6 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 46 additions & 16 deletions Nearby Sharing Windows/FileUtils.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Android.Content;
using Android.Provider;
using Microsoft.Win32.SafeHandles;
using ShortDev.Microsoft.ConnectedDevices.NearShare;
using Environment = Android.OS.Environment;

namespace Nearby_Sharing_Windows;

Expand All @@ -11,9 +11,7 @@ public static CdpFileProvider CreateNearShareFileFromContentUri(this ContentReso
{
var fileName = contentResolver.QueryContentName(contentUri);

using var fd = contentResolver.OpenAssetFileDescriptor(contentUri, "r") ?? throw new IOException("Could not open file");
var stream = fd.CreateInputStream() ?? throw new IOException("Could not open input stream");

var stream = contentResolver.OpenInputStream(contentUri) ?? throw new IOException("Could not open input stream");
return CdpFileProvider.FromStream(fileName, stream);
}

Expand All @@ -24,9 +22,51 @@ public static string QueryContentName(this ContentResolver resolver, AndroidUri
return returnCursor.GetString(0) ?? throw new IOException("Could not query content name");
}

public static Stream CreateDownloadFile(this Activity activity, string fileName)
public static (AndroidUri uri, FileStream stream) CreateMediaStoreStream(this ContentResolver resolver, string fileName)
{
ContentValues contentValues = new();
contentValues.Put(MediaStore.IMediaColumns.DisplayName, fileName);

FileStream stream;
AndroidUri mediaUri;
if (!OperatingSystem.IsAndroidVersionAtLeast(29))
{
stream = CreateDownloadFile(fileName);

contentValues.Put(MediaStore.IMediaColumns.Data, stream.Name);
mediaUri = resolver.Insert(contentValues);
}
else
{
contentValues.Put(MediaStore.IMediaColumns.RelativePath, "Download/Nearby Sharing/");
mediaUri = resolver.Insert(contentValues);

stream = resolver.OpenFileStream(mediaUri);
}

return (mediaUri, stream);
}

public static FileStream OpenFileStream(this ContentResolver resolver, AndroidUri mediaUri)
{
var downloadDir = activity.GetDownloadDirectory().FullName;
using var fileDescriptor = resolver.OpenFileDescriptor(mediaUri, "rwt") ?? throw new InvalidOperationException("Could not open file descriptor");

SafeFileHandle handle = new(fileDescriptor.DetachFd(), ownsHandle: true);
return new(handle, FileAccess.ReadWrite);
}

static AndroidUri Insert(this ContentResolver resolver, ContentValues contentValues)
{
return resolver.Insert(
MediaStore.Files.GetContentUri("external") ?? throw new InvalidOperationException("Could not get external content uri"),
contentValues
) ?? throw new InvalidOperationException("Could not insert into MediaStore");
}

static FileStream CreateDownloadFile(string fileName)
{
var downloadDir = AndroidEnvironment.GetExternalStoragePublicDirectory(AndroidEnvironment.DirectoryDownloads)?.AbsolutePath
?? throw new NullReferenceException("Could not get download directory");

string filePath = Path.Combine(downloadDir, fileName);
if (!File.Exists(filePath))
Expand All @@ -41,16 +81,6 @@ public static Stream CreateDownloadFile(this Activity activity, string fileName)
return File.Create(filePath);
}

public static DirectoryInfo GetDownloadDirectory(this Activity activity)
{
var publicDownloadDir = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDownloads)?.AbsolutePath;
DirectoryInfo downloadDir = new(publicDownloadDir ?? Path.Combine(activity.GetExternalMediaDirs()?.FirstOrDefault()?.AbsolutePath ?? "/sdcard/", "Download"));
if (!downloadDir.Exists)
downloadDir.Create();

return downloadDir;
}

public static string GetLogFilePattern(this Activity activity)
{
DirectoryInfo downloadDir = new(Path.Combine(activity.GetExternalMediaDirs()?.FirstOrDefault()?.AbsolutePath ?? "/sdcard/", "logs"));
Expand Down
1 change: 1 addition & 0 deletions Nearby Sharing Windows/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
global using AndroidUri = Android.Net.Uri;
global using AndroidEnvironment = Android.OS.Environment;
global using ManifestPermission = Android.Manifest.Permission;
168 changes: 69 additions & 99 deletions Nearby Sharing Windows/ReceiveActivity.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Android.Bluetooth;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
Expand All @@ -17,8 +18,8 @@
using ShortDev.Microsoft.ConnectedDevices.Platforms.Bluetooth;
using ShortDev.Microsoft.ConnectedDevices.Platforms.Network;
using ShortDev.Microsoft.ConnectedDevices.Transports;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.NetworkInformation;
using SystemDebug = System.Diagnostics.Debug;

Expand All @@ -27,11 +28,8 @@ namespace Nearby_Sharing_Windows;
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", ConfigurationChanges = UIHelper.ConfigChangesFlags)]
public sealed class ReceiveActivity : AppCompatActivity
{
BluetoothAdapter? _btAdapter;

[AllowNull] AdapterDescriptor<TransferToken> adapterDescriptor;
[AllowNull] RecyclerView notificationsRecyclerView;
readonly List<TransferToken> _notifications = new();
RecyclerView notificationsRecyclerView = null!;
readonly ObservableCollection<TransferToken> _notifications = [];

PhysicalAddress? btAddress = null;

Expand All @@ -43,7 +41,7 @@ protected override void OnCreate(Bundle? savedInstanceState)

if (ReceiveSetupActivity.IsSetupRequired(this) || !ReceiveSetupActivity.TryGetBtAddress(this, out btAddress) || btAddress == null)
{
StartActivity(new Android.Content.Intent(this, typeof(ReceiveSetupActivity)));
StartActivity(new Intent(this, typeof(ReceiveSetupActivity)));

Finish();
return;
Expand All @@ -55,11 +53,15 @@ protected override void OnCreate(Bundle? savedInstanceState)

notificationsRecyclerView = FindViewById<RecyclerView>(Resource.Id.notificationsRecyclerView)!;
notificationsRecyclerView.SetLayoutManager(new LinearLayoutManager(this));
notificationsRecyclerView.SetAdapter(
new AdapterDescriptor<TransferToken>(
Resource.Layout.item_transfer_notification,
OnInflateNotification
).CreateRecyclerViewAdapter(_notifications)
);

FindViewById<Button>(Resource.Id.openFAQButton)!.Click += (s, e) => UIHelper.OpenFAQ(this);

adapterDescriptor = new(Resource.Layout.item_transfer_notification, OnInflateNotification);

_loggerFactory = ConnectedDevicesPlatform.CreateLoggerFactory(this.GetLogFilePattern());
_logger = _loggerFactory.CreateLogger<ReceiveActivity>();

Expand All @@ -72,14 +74,14 @@ void OnInflateNotification(View view, TransferToken transfer)
var openButton = view.FindViewById<Button>(Resource.Id.openButton)!;
var fileNameTextView = view.FindViewById<TextView>(Resource.Id.fileNameTextView)!;
var detailsTextView = view.FindViewById<TextView>(Resource.Id.detailsTextView)!;
var loadingProgressIndicator = view.FindViewById<CircularProgressIndicator>(Resource.Id.loadingProgressIndicator)!;

view.FindViewById<Button>(Resource.Id.cancelButton)!.Click += (s, e) =>
{
_notifications.Remove(transfer);
UpdateUI();

if (transfer is FileTransferToken fileTransfer)
fileTransfer.Cancel();

_notifications.Remove(transfer);
};

if (transfer is UriTransferToken uriTranfer)
Expand All @@ -99,77 +101,65 @@ void OnInflateNotification(View view, TransferToken transfer)
throw new UnreachableException();

fileNameTextView.Text = string.Join(", ", fileTransfer.Files.Select(x => x.Name));
detailsTextView.Text = $"{fileTransfer.DeviceName} • {FileTransferToken.FormatFileSize(fileTransfer.TotalBytesToSend)}";

var loadingProgressIndicator = view.FindViewById<CircularProgressIndicator>(Resource.Id.loadingProgressIndicator)!;
void OnCompleted()
{
acceptButton.Visibility = ViewStates.Gone;
loadingProgressIndicator.Visibility = ViewStates.Gone;
detailsTextView.Text = $"{fileTransfer.DeviceName} • {FileTransferToken.FormatFileSize(fileTransfer.TotalBytes)}";

openButton.Visibility = ViewStates.Visible;
openButton.Click += (_, _) =>
{
this.ViewDownloads();
acceptButton.Click += OnAccept;

// ToDo: View single file
// if (fileTransfer.Files.Count == 1)
};
}
fileTransfer.Progress += progress => RunOnUiThread(() => OnProgress(progress));
loadingProgressIndicator.Indeterminate = true;

if (fileTransfer.IsTransferComplete)
openButton.Click += (_, _) =>
{
OnCompleted();
return;
}
this.ViewDownloads();

// ToDo: View single file
// if (fileTransfer.Files.Count == 1)
};

void OnAccept()
UpdateUI();

void OnAccept(object? sender, EventArgs e)
{
if (!fileTransfer.IsAccepted)
try
{
try
{
var streams = fileTransfer.Select(file => this.CreateDownloadFile(file.Name)).ToArray();
fileTransfer.Accept(streams);
}
catch (Exception ex)
{
new MaterialAlertDialogBuilder(this)
.SetTitle(ex.GetType().Name)!
.SetMessage(ex.Message)!
.Show();
var streams = fileTransfer.Select(file => ContentResolver!.CreateMediaStoreStream(file.Name).stream).ToArray();
fileTransfer.Accept(streams);

return;
}
fileTransfer.Finished += () =>
{
// ToDo: Delete failed transfers
};
}
catch (Exception ex)
{
new MaterialAlertDialogBuilder(this)
.SetTitle(ex.GetType().Name)!
.SetMessage(ex.Message)!
.Show();
}

acceptButton.Visibility = ViewStates.Gone;
loadingProgressIndicator.Visibility = ViewStates.Visible;
UpdateUI();
}

loadingProgressIndicator.Progress = 0;
void OnProgress(NearShareProgress progress, bool animate)
{
loadingProgressIndicator.Indeterminate = false;
void OnProgress(NearShareProgress progress)
{
loadingProgressIndicator.Indeterminate = false;

int progressInt = progress.TotalBytesToSend == 0 ? 0 : Math.Min((int)(progress.BytesSent * 100 / progress.TotalBytesToSend), 100);
if (OperatingSystem.IsAndroidVersionAtLeast(24))
loadingProgressIndicator.SetProgress(progressInt, animate);
else
loadingProgressIndicator.Progress = progressInt;
int progressInt = progress.TotalBytes == 0 ? 0 : Math.Min((int)(progress.TransferedBytes * 100 / progress.TotalBytes), 100);
if (OperatingSystem.IsAndroidVersionAtLeast(24))
loadingProgressIndicator.SetProgress(progressInt, animate: true);
else
loadingProgressIndicator.Progress = progressInt;

if (fileTransfer.IsTransferComplete)
OnCompleted();
}
fileTransfer.Progress += progress => RunOnUiThread(() => OnProgress(progress, animate: true));
loadingProgressIndicator.Indeterminate = true;
UpdateUI();
}
if (fileTransfer.IsAccepted)

void UpdateUI()
{
OnAccept();
return;
acceptButton.Visibility = !fileTransfer.IsTransferComplete && !fileTransfer.IsAccepted ? ViewStates.Visible : ViewStates.Gone;
loadingProgressIndicator.Visibility = !fileTransfer.IsTransferComplete && fileTransfer.IsAccepted ? ViewStates.Visible : ViewStates.Gone;
openButton.Visibility = fileTransfer.IsTransferComplete ? ViewStates.Visible : ViewStates.Gone;
}

acceptButton.Click += (s, e) => OnAccept();
}

CancellationTokenSource? _cancellationTokenSource;
Expand All @@ -183,9 +173,9 @@ void InitializeCDP()
_cancellationTokenSource = new();

var service = (BluetoothManager)GetSystemService(BluetoothService)!;
_btAdapter = service.Adapter!;
var btAdapter = service.Adapter!;

var deviceName = SettingsFragment.GetDeviceName(this, _btAdapter);
var deviceName = SettingsFragment.GetDeviceName(this, btAdapter);

SystemDebug.Assert(_cdp == null);

Expand All @@ -198,7 +188,7 @@ void InitializeCDP()
DeviceCertificate = ConnectedDevicesPlatform.CreateDeviceCertificate(CdpEncryptionParams.Default)
}, _loggerFactory);

IBluetoothHandler bluetoothHandler = new AndroidBluetoothHandler(_btAdapter, btAddress);
IBluetoothHandler bluetoothHandler = new AndroidBluetoothHandler(btAdapter, btAddress);
_cdp.AddTransport<BluetoothTransport>(new(bluetoothHandler));

INetworkHandler networkHandler = new AndroidNetworkHandler(this);
Expand All @@ -208,26 +198,23 @@ void InitializeCDP()
_cdp.Advertise(_cancellationTokenSource.Token);

NearShareReceiver.Register(_cdp);
NearShareReceiver.ReceivedUri += OnReceivedUri;
NearShareReceiver.FileTransfer += OnFileTransfer;
NearShareReceiver.ReceivedUri += OnTransfer;
NearShareReceiver.FileTransfer += OnTransfer;

FindViewById<TextView>(Resource.Id.deviceInfoTextView)!.Text = this.Localize(
Resource.String.visible_as_template,
$"\"{deviceName}\".\n" +
$"Address: {btAddress.ToStringFormatted()}\n" +
$"IP-Address: {networkHandler.TryGetLocalIp()?.ToString() ?? "null"}"
$"""
"{deviceName}"
Address: {btAddress.ToStringFormatted()}
IP-Address: {networkHandler.TryGetLocalIp()?.ToString() ?? "null"}
"""
);
}

public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
{
_logger.RequestPermissionResult(requestCode, permissions, grantResults);

if (grantResults.Contains(Permission.Denied))
{
Toast.MakeText(this, this.Localize(Resource.String.receive_missing_permissions), ToastLength.Long)!.Show();
}

InitializeCDP();
}

Expand All @@ -245,25 +232,8 @@ public override void Finish()
base.Finish();
}

void UpdateUI()
{
RunOnUiThread(() =>
{
notificationsRecyclerView.SetAdapter(adapterDescriptor.CreateRecyclerViewAdapter(_notifications));
});
}

public void OnReceivedUri(UriTransferToken transfer)
{
_notifications.Add(transfer);
UpdateUI();
}

public void OnFileTransfer(FileTransferToken transfer)
{
_notifications.Add(transfer);
UpdateUI();
}
void OnTransfer(TransferToken transfer)
=> RunOnUiThread(() => _notifications.Add(transfer));
}

static class Extensions
Expand Down
8 changes: 4 additions & 4 deletions Nearby Sharing Windows/SendActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,14 @@ private async void SendData(CdpDevice remoteSystem)
{
#endif
progressIndicator.Indeterminate = false;
progressIndicator.Max = (int)args.TotalBytesToSend;
progressIndicator.SetProgressCompat((int)args.BytesSent, animated: true);
progressIndicator.Max = (int)args.TotalBytes;
progressIndicator.SetProgressCompat((int)args.TransferedBytes, animated: true);

if (args.TotalFilesToSend != 0 && args.TotalBytesToSend != 0)
if (args.TotalFiles != 0 && args.TotalBytes != 0)
{
StatusTextView.Text = this.Localize(
Resource.String.sending_template,
args.TotalFilesToSend
args.TotalFiles
);
}
#if !DEBUG
Expand Down
Loading
Loading