511 lines
15 KiB
C#
511 lines
15 KiB
C#
using Android.Content;
|
|
using Tesses.VirtualFilesystem;
|
|
using Android.App;
|
|
using Android.Content.PM;
|
|
using AndroidX.DocumentFile.Provider;
|
|
using Tesses.VirtualFilesystem.Extensions;
|
|
namespace Tesses.VirtualFilesystem.Filesystems;
|
|
|
|
|
|
public class SAFFileSystem : SyncFileSystem
|
|
{
|
|
public const int RequestCode = 1447775022;
|
|
public const string SharedPreferencesFile = "tesess_vfs";
|
|
public static void RequestDirectory(Activity app,bool canwrite=true,bool presistant=false,int reqCode=RequestCode)
|
|
{
|
|
Intent intent = new Intent(Intent.ActionOpenDocumentTree);
|
|
intent.AddFlags(ActivityFlags.GrantReadUriPermission|(canwrite? ActivityFlags.GrantWriteUriPermission : 0)|(presistant?ActivityFlags.GrantPersistableUriPermission:0));
|
|
app.StartActivityForResult(intent,reqCode);
|
|
}
|
|
|
|
public static SAFFileSystem? GetSAFFromResponse(Context? app,Intent? intent)
|
|
{
|
|
if(app == null || intent == null) return null;
|
|
var uri=intent.Data;
|
|
if(uri != null)
|
|
{
|
|
return new SAFFileSystem(uri,app);
|
|
}
|
|
return null;
|
|
}
|
|
public static SAFFileSystem? GetSAFFromResponsePresistant(Context? app,Intent? intent,string key)
|
|
{
|
|
if(app == null || intent == null) return null;
|
|
var uri=intent.Data;
|
|
if(uri != null)
|
|
{
|
|
var r = app?.ContentResolver;
|
|
if(r == null) return null;
|
|
var rw = intent.Flags & (ActivityFlags.GrantReadUriPermission | ActivityFlags.GrantWriteUriPermission);
|
|
r.TakePersistableUriPermission(uri,rw);
|
|
var app2=app?.CreatePackageContext(app.PackageName,0);
|
|
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
|
|
|
|
sharedPrefs?.Edit()?.PutString(key,uri.ToString())?.Apply();
|
|
|
|
|
|
return new SAFFileSystem(uri,app);
|
|
}
|
|
return null;
|
|
}
|
|
public static void Revoke(Context? app,string key)
|
|
{
|
|
var app2=app?.CreatePackageContext(app.PackageName,0);
|
|
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
|
|
var res=sharedPrefs?.GetString(key,null);
|
|
var r = app?.ContentResolver;
|
|
if(r == null) return;
|
|
|
|
if(string.IsNullOrWhiteSpace(res)) return;
|
|
var uri = Android.Net.Uri.Parse(res);
|
|
if(uri == null) return;
|
|
r.ReleasePersistableUriPermission(uri,ActivityFlags.GrantReadUriPermission);
|
|
sharedPrefs?.Edit()?.Remove(key)?.Apply();
|
|
}
|
|
|
|
public static SAFFileSystem? GetSAFFromSharedStorage(Context? app,string key)
|
|
{
|
|
if(app == null) return null;
|
|
var app2=app?.CreatePackageContext(app.PackageName,0);
|
|
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
|
|
var res=sharedPrefs?.GetString(key,null);
|
|
|
|
if(string.IsNullOrWhiteSpace(res)) return null;
|
|
var uri = Android.Net.Uri.Parse(res);
|
|
if(uri == null) return null;
|
|
|
|
return new SAFFileSystem(uri,app);
|
|
}
|
|
|
|
|
|
public SAFFileSystem(global::Android.Net.Uri uri,Context? ctx)
|
|
{
|
|
Uri = uri;
|
|
Context = ctx;
|
|
|
|
}
|
|
public global::Android.Net.Uri Uri {get;set;}
|
|
public Context? Context {get;set;}
|
|
|
|
|
|
public override void CreateDirectory(UnixPath directory)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return;
|
|
|
|
foreach(var item in directory.Parts)
|
|
{
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
dir = dir2.CreateDirectory(item);
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir = dir2;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public override void DeleteDirectory(UnixPath path)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return;
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
return;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
dir.Delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public override void DeleteFile(UnixPath path)
|
|
{
|
|
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return;
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
return;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir=dir2;
|
|
}
|
|
else if(dir2.IsFile)
|
|
{
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
dir.Delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public override bool DirectoryExists(UnixPath path)
|
|
{
|
|
if(path.IsRoot) return true;
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return false;
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
return false;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override IEnumerable<UnixPath> EnumerateFileSystemEntries(UnixPath path)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) yield break;
|
|
|
|
if(path.IsRoot)
|
|
{
|
|
foreach(var _item in dir.ListFiles())
|
|
{
|
|
yield return path / _item.Name;
|
|
}
|
|
yield break;
|
|
}
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
yield break;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
{
|
|
foreach(var _item in dir2.ListFiles())
|
|
{
|
|
yield return path / _item.Name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public override bool FileExists(UnixPath path)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return false;
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
return false;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir=dir2;
|
|
}
|
|
else if(dir2.IsFile)
|
|
{
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override DateTime GetCreationTime(UnixPath path)
|
|
{
|
|
return DateTime.Now;
|
|
}
|
|
|
|
public override DateTime GetLastAccessTime(UnixPath path)
|
|
{
|
|
return DateTime.Now;
|
|
}
|
|
|
|
public override DateTime GetLastWriteTime(UnixPath path)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return DateTime.Now;
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(dir2.Exists())
|
|
{
|
|
|
|
|
|
dir = dir2;
|
|
if(i==path.Parts.Length -1)
|
|
return DateTimeOffset.FromUnixTimeSeconds(dir2.LastModified()).DateTime;
|
|
}
|
|
else return DateTime.Now;
|
|
}
|
|
}
|
|
|
|
}
|
|
return DateTime.Now;
|
|
}
|
|
|
|
public override void MoveDirectory(UnixPath src, UnixPath dest)
|
|
{
|
|
foreach(var item in this.EnumerateDirectories(src))
|
|
{
|
|
MoveDirectory(item,dest / item.Name);
|
|
}
|
|
foreach(var item in this.EnumerateFiles(src))
|
|
{
|
|
MoveFile(item,dest / item.Name);
|
|
}
|
|
DeleteDirectory(src);
|
|
}
|
|
|
|
public override void MoveFile(UnixPath src, UnixPath dest)
|
|
{
|
|
if(src.Parent == dest.Parent)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) return;
|
|
|
|
for(int i = 0;i<src.Parts.Length;i++)
|
|
{
|
|
var item = src.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
return;
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir=dir2;
|
|
}
|
|
else if(dir2.IsFile)
|
|
{
|
|
dir = dir2;
|
|
if(i==src.Parts.Length -1)
|
|
dir.RenameTo(dest.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
this.CopyFile(src,dest);
|
|
DeleteFile(src);
|
|
}
|
|
}
|
|
|
|
public override Stream Open(UnixPath path, FileMode mode, FileAccess access, FileShare share)
|
|
{
|
|
if(Context != null && Uri != null)
|
|
{
|
|
|
|
var dir = DocumentFile.FromTreeUri(Context,Uri);
|
|
if(dir == null) throw new FileNotFoundException(path.ToString());
|
|
|
|
for(int i = 0;i<path.Parts.Length;i++)
|
|
{
|
|
var item = path.Parts[i];
|
|
var dir2= dir?.FindFile(item);
|
|
if(dir2 != null)
|
|
{
|
|
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
|
|
{
|
|
throw new FileNotFoundException(path.ToString());
|
|
}
|
|
else if(dir2.IsDirectory)
|
|
{
|
|
dir = dir2;
|
|
|
|
}
|
|
|
|
if(i==path.Parts.Length -1)
|
|
{
|
|
if(dir2?.IsFile ?? false)
|
|
{
|
|
if(mode == FileMode.CreateNew) throw new IOException("File already exists");
|
|
return _OpenFile(dir2.Uri,mode,access,share);
|
|
|
|
}
|
|
else
|
|
{
|
|
var (mimeType,displayName)=GetDisplayNameAndMime(item);
|
|
var f=dir?.CreateFile(mimeType,displayName);
|
|
if(mode == FileMode.Open || mode == FileMode.Truncate || mode == FileMode.Append)
|
|
throw new IOException("File does not exist");
|
|
if(f != null)
|
|
return _OpenFile(f.Uri,mode,access,share);
|
|
throw new IOException("Could not open file");
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
throw new FileNotFoundException(path.ToString());
|
|
}
|
|
|
|
private (string mimeType, string displayName) GetDisplayNameAndMime(string item)
|
|
{
|
|
var mime=HeyRed.Mime.MimeTypesMap.GetMimeType(item);
|
|
if(mime == "application/octet-stream")
|
|
{
|
|
return ("unknown/unknown",item);
|
|
}
|
|
else
|
|
{
|
|
return (mime,Path.GetFileNameWithoutExtension(item));
|
|
}
|
|
}
|
|
|
|
private Stream _OpenFile(global::Android.Net.Uri uri, FileMode mode, FileAccess access, FileShare share)
|
|
{
|
|
|
|
//FileMode.Append -> "a"
|
|
//FileMode.Create -> "w"
|
|
//FileMode.CreateNew -> "w"
|
|
//FileMode.Open -> "r or w"
|
|
//FileMode.OpenOrCreate -> "r or w"
|
|
//FileMode.Truncate -> "t"
|
|
//"r", "w", "wt", "wa", "rw" or "rwt"
|
|
|
|
Stream? strm = null;
|
|
|
|
if(access == FileAccess.Read)
|
|
strm = Context?.ContentResolver?.OpenInputStream(uri) ?? null;
|
|
|
|
if(access == FileAccess.Write)
|
|
if(mode == FileMode.Truncate)
|
|
strm = Context?.ContentResolver?.OpenOutputStream(uri,"wt");
|
|
else if(mode == FileMode.Append)
|
|
strm = Context?.ContentResolver?.OpenOutputStream(uri,"wa");
|
|
else
|
|
strm = Context?.ContentResolver?.OpenOutputStream(uri,"w");
|
|
|
|
if(access == FileAccess.ReadWrite)
|
|
if(mode == FileMode.Truncate)
|
|
strm = Context?.ContentResolver?.OpenOutputStream(uri,"rwt");
|
|
else
|
|
strm = Context?.ContentResolver?.OpenOutputStream(uri,"rw");
|
|
|
|
|
|
|
|
|
|
|
|
if(strm == null) throw new IOException("Failed to open stream");
|
|
return strm;
|
|
|
|
}
|
|
|
|
public override void SetCreationTime(UnixPath path, DateTime time)
|
|
{
|
|
|
|
}
|
|
|
|
public override void SetLastAccessTime(UnixPath path, DateTime time)
|
|
{
|
|
|
|
}
|
|
|
|
public override void SetLastWriteTime(UnixPath path, DateTime time)
|
|
{
|
|
|
|
}
|
|
}
|