keepass2android/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs

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);
}
}
}
}