Files
Sokoban/Assets/Plugins/SimpleFileBrowser/Scripts/FileBrowserHelpers.cs
2023-06-16 17:08:56 +03:00

543 lines
15 KiB
C#

using System.IO;
using UnityEngine;
namespace SimpleFileBrowser
{
public struct FileSystemEntry
{
public readonly string Path;
public readonly string Name;
public readonly string Extension;
public readonly FileAttributes Attributes;
public bool IsDirectory { get { return ( Attributes & FileAttributes.Directory ) == FileAttributes.Directory; } }
public FileSystemEntry( string path, string name, string extension, bool isDirectory )
{
Path = path;
Name = name;
Extension = extension;
Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal;
}
public FileSystemEntry( FileSystemInfo fileInfo, string extension )
{
Path = fileInfo.FullName;
Name = fileInfo.Name;
Extension = extension;
Attributes = fileInfo.Attributes;
}
}
public static class FileBrowserHelpers
{
#if !UNITY_EDITOR && UNITY_ANDROID
private static AndroidJavaClass m_ajc = null;
public static AndroidJavaClass AJC
{
get
{
if( m_ajc == null )
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.FileBrowser" );
return m_ajc;
}
}
private static AndroidJavaObject m_context = null;
public static AndroidJavaObject Context
{
get
{
if( m_context == null )
{
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
{
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
}
}
return m_context;
}
}
private static string m_temporaryFilePath = null;
private static string TemporaryFilePath
{
get
{
if( m_temporaryFilePath == null )
{
m_temporaryFilePath = Path.Combine( Application.temporaryCachePath, "tmpFile" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_temporaryFilePath;
}
}
// On Android 10+, filesystem can be accessed via Storage Access Framework only
private static bool? m_shouldUseSAF = null;
public static bool ShouldUseSAF
{
get
{
if( m_shouldUseSAF == null )
m_shouldUseSAF = AJC.CallStatic<bool>( "CheckSAF" );
return m_shouldUseSAF.Value;
}
}
public static bool ShouldUseSAFForPath( string path ) // true: path should be managed with AJC (native helper class for Storage Access Framework), false: path should be managed with System.IO
{
return ShouldUseSAF && ( string.IsNullOrEmpty( path ) || path[0] != '/' );
}
#endif
public static bool FileExists( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<bool>( "SAFEntryExists", Context, path, false );
#endif
return File.Exists( path );
}
public static bool DirectoryExists( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<bool>( "SAFEntryExists", Context, path, true );
else if( ShouldUseSAF ) // Directory.Exists returns true even for inaccessible directories on Android 10+, we need to check if the directory is accessible
{
try
{
Directory.GetFiles( path, "testtesttest" );
return true;
}
catch
{
return false;
}
}
#endif
return Directory.Exists( path );
}
public static bool IsDirectory( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<bool>( "SAFEntryDirectory", Context, path );
#endif
if( Directory.Exists( path ) )
return true;
if( File.Exists( path ) )
return false;
string extension = Path.GetExtension( path );
return extension == null || extension.Length <= 1; // extension includes '.'
}
public static bool IsPathDescendantOfAnother( string path, string parentFolderPath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<bool>( "IsSAFEntryChildOfAnother", Context, path, parentFolderPath );
#endif
path = Path.GetFullPath( path ).Replace( '\\', '/' );
parentFolderPath = Path.GetFullPath( parentFolderPath ).Replace( '\\', '/' );
if( path == parentFolderPath )
return false;
if( parentFolderPath[parentFolderPath.Length - 1] != '/' )
parentFolderPath += "/";
return path != parentFolderPath && path.StartsWith( parentFolderPath, System.StringComparison.OrdinalIgnoreCase );
}
public static string GetDirectoryName( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<string>( "GetParentDirectory", Context, path );
#endif
return Path.GetDirectoryName( path );
}
public static FileSystemEntry[] GetEntriesInDirectory( string path, bool extractOnlyLastSuffixFromExtensions )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
{
string resultRaw = AJC.CallStatic<string>( "OpenSAFFolder", Context, path );
int separatorIndex = resultRaw.IndexOf( "<>" );
if( separatorIndex <= 0 )
{
Debug.LogError( "Entry count does not exist" );
return null;
}
int entryCount = 0;
for( int i = 0; i < separatorIndex; i++ )
{
char ch = resultRaw[i];
if( ch < '0' && ch > '9' )
{
Debug.LogError( "Couldn't parse entry count" );
return null;
}
entryCount = entryCount * 10 + ( ch - '0' );
}
if( entryCount <= 0 )
return null;
FileSystemEntry[] result = new FileSystemEntry[entryCount];
for( int i = 0; i < entryCount; i++ )
{
separatorIndex += 2;
if( separatorIndex >= resultRaw.Length )
{
Debug.LogError( "Couldn't fetch directory attribute" );
return null;
}
bool isDirectory = resultRaw[separatorIndex] == 'd';
separatorIndex++;
int nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
if( nextSeparatorIndex <= 0 )
{
Debug.LogError( "Entry name is empty" );
return null;
}
string entryName = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
separatorIndex = nextSeparatorIndex + 2;
nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
if( nextSeparatorIndex <= 0 )
{
Debug.LogError( "Entry rawUri is empty" );
return null;
}
string rawUri = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
separatorIndex = nextSeparatorIndex;
result[i] = new FileSystemEntry( rawUri, entryName, isDirectory ? null : FileBrowser.GetExtensionFromFilename( entryName, extractOnlyLastSuffixFromExtensions ), isDirectory );
}
return result;
}
#endif
try
{
FileSystemInfo[] items = new DirectoryInfo( path ).GetFileSystemInfos();
FileSystemEntry[] result = new FileSystemEntry[items.Length];
int index = 0;
for( int i = 0; i < items.Length; i++ )
{
try
{
result[index] = new FileSystemEntry( items[i], FileBrowser.GetExtensionFromFilename( items[i].Name, extractOnlyLastSuffixFromExtensions ) );
index++;
}
catch( System.Exception e )
{
Debug.LogException( e );
}
}
if( result.Length != index )
System.Array.Resize( ref result, index );
return result;
}
catch( System.UnauthorizedAccessException ) { }
catch( System.Exception e )
{
Debug.LogException( e );
}
return null;
}
public static string CreateFileInDirectory( string directoryPath, string filename )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( directoryPath ) )
return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, false, filename );
#endif
string path = Path.Combine( directoryPath, filename );
using( File.Create( path ) ) { }
return path;
}
public static string CreateFolderInDirectory( string directoryPath, string folderName )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( directoryPath ) )
return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, true, folderName );
#endif
string path = Path.Combine( directoryPath, folderName );
Directory.CreateDirectory( path );
return path;
}
public static void WriteBytesToFile( string targetPath, byte[] bytes )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( targetPath ) )
{
File.WriteAllBytes( TemporaryFilePath, bytes );
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
File.Delete( TemporaryFilePath );
return;
}
#endif
File.WriteAllBytes( targetPath, bytes );
}
public static void WriteTextToFile( string targetPath, string text )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( targetPath ) )
{
File.WriteAllText( TemporaryFilePath, text );
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
File.Delete( TemporaryFilePath );
return;
}
#endif
File.WriteAllText( targetPath, text );
}
public static void AppendBytesToFile( string targetPath, byte[] bytes )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( targetPath ) )
{
File.WriteAllBytes( TemporaryFilePath, bytes );
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
File.Delete( TemporaryFilePath );
return;
}
#endif
using( var stream = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
{
stream.Write( bytes, 0, bytes.Length );
}
}
public static void AppendTextToFile( string targetPath, string text )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( targetPath ) )
{
File.WriteAllText( TemporaryFilePath, text );
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
File.Delete( TemporaryFilePath );
return;
}
#endif
File.AppendAllText( targetPath, text );
}
private static void AppendFileToFile( string targetPath, string sourceFileToAppend )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( targetPath ) )
{
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, sourceFileToAppend, true );
return;
}
#endif
using( Stream input = File.OpenRead( sourceFileToAppend ) )
using( Stream output = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
{
byte[] buffer = new byte[4096];
int bytesRead;
while( ( bytesRead = input.Read( buffer, 0, buffer.Length ) ) > 0 )
output.Write( buffer, 0, bytesRead );
}
}
public static byte[] ReadBytesFromFile( string sourcePath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( sourcePath ) )
{
AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
byte[] result = File.ReadAllBytes( TemporaryFilePath );
File.Delete( TemporaryFilePath );
return result;
}
#endif
return File.ReadAllBytes( sourcePath );
}
public static string ReadTextFromFile( string sourcePath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( sourcePath ) )
{
AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
string result = File.ReadAllText( TemporaryFilePath );
File.Delete( TemporaryFilePath );
return result;
}
#endif
return File.ReadAllText( sourcePath );
}
public static void CopyFile( string sourcePath, string destinationPath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw file paths are handled on the native-side
{
AJC.CallStatic( "CopyFile", Context, sourcePath, destinationPath, false );
return;
}
#endif
File.Copy( sourcePath, destinationPath, true );
}
public static void CopyDirectory( string sourcePath, string destinationPath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw directory paths are handled on the native-side
{
AJC.CallStatic( "CopyDirectory", Context, sourcePath, destinationPath, false );
return;
}
#endif
CopyDirectoryRecursively( new DirectoryInfo( sourcePath ), destinationPath );
}
private static void CopyDirectoryRecursively( DirectoryInfo sourceDirectory, string destinationPath )
{
Directory.CreateDirectory( destinationPath );
FileInfo[] files = sourceDirectory.GetFiles();
for( int i = 0; i < files.Length; i++ )
files[i].CopyTo( Path.Combine( destinationPath, files[i].Name ), true );
DirectoryInfo[] subDirectories = sourceDirectory.GetDirectories();
for( int i = 0; i < subDirectories.Length; i++ )
CopyDirectoryRecursively( subDirectories[i], Path.Combine( destinationPath, subDirectories[i].Name ) );
}
public static void MoveFile( string sourcePath, string destinationPath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw file paths are handled on the native-side
{
AJC.CallStatic( "CopyFile", Context, sourcePath, destinationPath, true );
return;
}
#endif
File.Move( sourcePath, destinationPath );
}
public static void MoveDirectory( string sourcePath, string destinationPath )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw directory paths are handled on the native-side
{
AJC.CallStatic( "CopyDirectory", Context, sourcePath, destinationPath, true );
return;
}
#endif
Directory.Move( sourcePath, destinationPath );
}
public static string RenameFile( string path, string newName )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
#endif
string newPath = Path.Combine( Path.GetDirectoryName( path ), newName );
File.Move( path, newPath );
return newPath;
}
public static string RenameDirectory( string path, string newName )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
#endif
string newPath = Path.Combine( new DirectoryInfo( path ).Parent.FullName, newName );
Directory.Move( path, newPath );
return newPath;
}
public static void DeleteFile( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
{
AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
return;
}
#endif
File.Delete( path );
}
public static void DeleteDirectory( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
{
AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
return;
}
#endif
Directory.Delete( path, true );
}
public static string GetFilename( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<string>( "SAFEntryName", Context, path );
#endif
return Path.GetFileName( path );
}
public static long GetFilesize( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
if( ShouldUseSAFForPath( path ) )
return AJC.CallStatic<long>( "SAFEntrySize", Context, path );
#endif
return new FileInfo( path ).Length;
}
public static System.DateTime GetLastModifiedDate( string path )
{
#if !UNITY_EDITOR && UNITY_ANDROID
// Credit: https://stackoverflow.com/a/28504416/2373034
if( ShouldUseSAFForPath( path ) )
return new System.DateTime( 1970, 1, 1, 0, 0, 0 ).AddMilliseconds( AJC.CallStatic<long>( "SAFEntryLastModified", Context, path ) );
#endif
return new FileInfo( path ).LastWriteTime;
}
}
}