300 lines
7.3 KiB
C#
300 lines
7.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Android.Content;
|
|
using Android.Database;
|
|
using Android.OS;
|
|
using Android.Provider;
|
|
using KeePassLib.Serialization;
|
|
|
|
namespace keepass2android.Io
|
|
{
|
|
/** FileStorage to work with content URIs
|
|
* Supports both "old" system where data is available only temporarily as
|
|
* well as the SAF system. Assumes that persistable permissions are "taken" by
|
|
* the activity which receives OnActivityResult from the system file picker.*/
|
|
public class AndroidContentStorage: IFileStorage
|
|
{
|
|
private readonly Context _ctx;
|
|
|
|
public AndroidContentStorage(Context ctx)
|
|
{
|
|
_ctx = ctx;
|
|
}
|
|
|
|
public IEnumerable<string> SupportedProtocols
|
|
{
|
|
get { yield return "content"; }
|
|
}
|
|
|
|
public void Delete(IOConnectionInfo ioc)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public Stream OpenFileForRead(IOConnectionInfo ioc)
|
|
{
|
|
return _ctx.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(ioc.Path));
|
|
}
|
|
|
|
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
|
|
{
|
|
return new AndroidContentWriteTransaction(ioc.Path, _ctx);
|
|
}
|
|
|
|
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
public bool RequiresCredentials(IOConnectionInfo ioc)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public FileDescription GetFileDescription(IOConnectionInfo ioc)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc)
|
|
{
|
|
|
|
}
|
|
|
|
public bool RequiresSetup(IOConnectionInfo ioConnection)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public string IocToPath(IOConnectionInfo ioc)
|
|
{
|
|
return ioc.Path;
|
|
}
|
|
|
|
public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId)
|
|
{
|
|
Intent intent = new Intent();
|
|
activity.IocToIntent(intent, new IOConnectionInfo { Path = protocolId + "://" });
|
|
activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileChooserPrepared, intent);
|
|
}
|
|
|
|
public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode,
|
|
bool alwaysReturnSuccess)
|
|
{
|
|
Intent intent = new Intent();
|
|
activity.IocToIntent(intent, ioc);
|
|
activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent);
|
|
}
|
|
|
|
public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void OnResume(IFileStorageSetupActivity activity)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void OnStart(IFileStorageSetupActivity activity)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public string GetDisplayName(IOConnectionInfo ioc)
|
|
{
|
|
string displayName = null;
|
|
if (TryGetDisplayName(ioc, ref displayName))
|
|
return "content://" + displayName; //make sure we return the protocol in the display name for consistency, also expected e.g. by CreateDatabaseActivity
|
|
return ioc.Path;
|
|
}
|
|
|
|
private bool TryGetDisplayName(IOConnectionInfo ioc, ref string displayName)
|
|
{
|
|
var uri = Android.Net.Uri.Parse(ioc.Path);
|
|
try
|
|
{
|
|
var cursor = _ctx.ContentResolver.Query(uri, null, null, null, null, null);
|
|
try
|
|
{
|
|
|
|
if (cursor != null && cursor.MoveToFirst())
|
|
{
|
|
displayName = cursor.GetString(cursor.GetColumnIndex(OpenableColumns.DisplayName));
|
|
if (!string.IsNullOrEmpty(displayName))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (cursor != null)
|
|
cursor.Close();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.Log(e.ToString());
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
public string CreateFilePath(string parent, string newFilename)
|
|
{
|
|
if (!parent.EndsWith("/"))
|
|
parent += "/";
|
|
return parent + newFilename;
|
|
}
|
|
|
|
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
|
|
{
|
|
//TODO: required for OTP Aux file retrieval
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
private static bool IsKitKatOrLater
|
|
{
|
|
get { return (int)Build.VERSION.SdkInt >= 19; }
|
|
}
|
|
|
|
public bool IsPermanentLocation(IOConnectionInfo ioc)
|
|
{
|
|
//on pre-Kitkat devices, content:// is always temporary:
|
|
if (!IsKitKatOrLater)
|
|
return false;
|
|
|
|
//try to get a persisted permission for the file
|
|
return _ctx.ContentResolver.PersistedUriPermissions.Any(p => p.Uri.ToString().Equals(ioc.Path));
|
|
}
|
|
|
|
|
|
public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut<UiStringKey> reason = null)
|
|
{
|
|
ICursor cursor = null;
|
|
try
|
|
{
|
|
//on pre-Kitkat devices, we can't write content:// files
|
|
if (!IsKitKatOrLater)
|
|
{
|
|
Kp2aLog.Log("File is read-only because we're not on KitKat or later.");
|
|
if (reason != null)
|
|
reason.Result = UiStringKey.ReadOnlyReason_PreKitKat;
|
|
return true;
|
|
}
|
|
|
|
|
|
//KitKat or later...
|
|
var uri = Android.Net.Uri.Parse(ioc.Path);
|
|
cursor = _ctx.ContentResolver.Query(uri, null, null, null, null, null);
|
|
|
|
if (cursor != null && cursor.MoveToFirst())
|
|
{
|
|
int column = cursor.GetColumnIndex(DocumentsContract.Document.ColumnFlags);
|
|
if (column < 0)
|
|
return false; //seems like this is not supported. See below for reasoning to return false.
|
|
int flags = cursor.GetInt(column);
|
|
Kp2aLog.Log("File flags: " + flags);
|
|
if ((flags & (long) DocumentContractFlags.SupportsWrite) == 0)
|
|
{
|
|
if (reason != null)
|
|
reason.Result = UiStringKey.ReadOnlyReason_ReadOnlyFlag;
|
|
return true;
|
|
}
|
|
else return false;
|
|
}
|
|
else throw new Exception("couldn't move to first result element: " + (cursor == null) + uri.ToString());
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(e);
|
|
//better return false here. We don't really know what happened (as this is unexpected).
|
|
//let the user try to write the file. If it fails they will get an exception string.
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
if (cursor != null)
|
|
cursor.Close();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class AndroidContentWriteTransaction : IWriteTransaction
|
|
{
|
|
private readonly string _path;
|
|
private readonly Context _ctx;
|
|
private MemoryStream _memoryStream;
|
|
|
|
public AndroidContentWriteTransaction(string path, Context ctx)
|
|
{
|
|
_path = path;
|
|
_ctx = ctx;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_memoryStream.Dispose();
|
|
}
|
|
|
|
public Stream OpenFile()
|
|
{
|
|
_memoryStream = new MemoryStream();
|
|
return _memoryStream;
|
|
}
|
|
|
|
public void CommitWrite()
|
|
{
|
|
using (Stream outputStream = _ctx.ContentResolver.OpenOutputStream(Android.Net.Uri.Parse(_path)))
|
|
{
|
|
byte[] data = _memoryStream.ToArray();
|
|
outputStream.Write(data, 0, data.Length);
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
} |