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