Skip to content

Commit

Permalink
Use FileDescriptor from MediaStore for random file access (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShortDevelopment authored Feb 9, 2024
1 parent 668d93b commit 887382e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 177 deletions.
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

0 comments on commit 887382e

Please sign in to comment.