FileBrowserHelpers.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. using System.IO;
  2. using UnityEngine;
  3. namespace SimpleFileBrowser
  4. {
  5. public struct FileSystemEntry
  6. {
  7. public readonly string Path;
  8. public readonly string Name;
  9. public readonly string Extension;
  10. public readonly FileAttributes Attributes;
  11. public bool IsDirectory { get { return ( Attributes & FileAttributes.Directory ) == FileAttributes.Directory; } }
  12. public FileSystemEntry( string path, string name, bool isDirectory )
  13. {
  14. Path = path;
  15. Name = name;
  16. Extension = isDirectory ? null : System.IO.Path.GetExtension( name );
  17. Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal;
  18. }
  19. public FileSystemEntry( FileSystemInfo fileInfo )
  20. {
  21. Path = fileInfo.FullName;
  22. Name = fileInfo.Name;
  23. Extension = fileInfo.Extension;
  24. Attributes = fileInfo.Attributes;
  25. }
  26. }
  27. public static class FileBrowserHelpers
  28. {
  29. #if !UNITY_EDITOR && UNITY_ANDROID
  30. private static AndroidJavaClass m_ajc = null;
  31. public static AndroidJavaClass AJC
  32. {
  33. get
  34. {
  35. if( m_ajc == null )
  36. m_ajc = new AndroidJavaClass( "com.yasirkula.unity.FileBrowser" );
  37. return m_ajc;
  38. }
  39. }
  40. private static AndroidJavaObject m_context = null;
  41. public static AndroidJavaObject Context
  42. {
  43. get
  44. {
  45. if( m_context == null )
  46. {
  47. using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
  48. {
  49. m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
  50. }
  51. }
  52. return m_context;
  53. }
  54. }
  55. private static string m_temporaryFilePath = null;
  56. private static string TemporaryFilePath
  57. {
  58. get
  59. {
  60. if( m_temporaryFilePath == null )
  61. {
  62. m_temporaryFilePath = Path.Combine( Application.temporaryCachePath, "tmpFile" );
  63. Directory.CreateDirectory( Application.temporaryCachePath );
  64. }
  65. return m_temporaryFilePath;
  66. }
  67. }
  68. // On Android 10+, filesystem can be accessed via Storage Access Framework only
  69. private static bool? m_shouldUseSAF = null;
  70. public static bool ShouldUseSAF
  71. {
  72. get
  73. {
  74. if( m_shouldUseSAF == null )
  75. m_shouldUseSAF = AJC.CallStatic<bool>( "CheckSAF" );
  76. return m_shouldUseSAF.Value;
  77. }
  78. }
  79. #endif
  80. public static bool FileExists( string path )
  81. {
  82. #if !UNITY_EDITOR && UNITY_ANDROID
  83. if( ShouldUseSAF )
  84. return AJC.CallStatic<bool>( "SAFEntryExists", Context, path );
  85. #endif
  86. return File.Exists( path );
  87. }
  88. public static bool DirectoryExists( string path )
  89. {
  90. #if !UNITY_EDITOR && UNITY_ANDROID
  91. if( ShouldUseSAF )
  92. return AJC.CallStatic<bool>( "SAFEntryExists", Context, path );
  93. #endif
  94. return Directory.Exists( path );
  95. }
  96. public static bool IsDirectory( string path )
  97. {
  98. #if !UNITY_EDITOR && UNITY_ANDROID
  99. if( ShouldUseSAF )
  100. return AJC.CallStatic<bool>( "SAFEntryDirectory", Context, path );
  101. #endif
  102. if( Directory.Exists( path ) )
  103. return true;
  104. if( File.Exists( path ) )
  105. return false;
  106. string extension = Path.GetExtension( path );
  107. return extension == null || extension.Length <= 1; // extension includes '.'
  108. }
  109. public static FileSystemEntry[] GetEntriesInDirectory( string path )
  110. {
  111. #if !UNITY_EDITOR && UNITY_ANDROID
  112. if( ShouldUseSAF )
  113. {
  114. string resultRaw = AJC.CallStatic<string>( "OpenSAFFolder", Context, path );
  115. int separatorIndex = resultRaw.IndexOf( "<>" );
  116. if( separatorIndex <= 0 )
  117. {
  118. Debug.LogError( "Entry count does not exist" );
  119. return null;
  120. }
  121. int entryCount = 0;
  122. for( int i = 0; i < separatorIndex; i++ )
  123. {
  124. char ch = resultRaw[i];
  125. if( ch < '0' && ch > '9' )
  126. {
  127. Debug.LogError( "Couldn't parse entry count" );
  128. return null;
  129. }
  130. entryCount = entryCount * 10 + ( ch - '0' );
  131. }
  132. if( entryCount <= 0 )
  133. return null;
  134. FileSystemEntry[] result = new FileSystemEntry[entryCount];
  135. for( int i = 0; i < entryCount; i++ )
  136. {
  137. separatorIndex += 2;
  138. if( separatorIndex >= resultRaw.Length )
  139. {
  140. Debug.LogError( "Couldn't fetch directory attribute" );
  141. return null;
  142. }
  143. bool isDirectory = resultRaw[separatorIndex] == 'd';
  144. separatorIndex++;
  145. int nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
  146. if( nextSeparatorIndex <= 0 )
  147. {
  148. Debug.LogError( "Entry name is empty" );
  149. return null;
  150. }
  151. string entryName = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
  152. separatorIndex = nextSeparatorIndex + 2;
  153. nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
  154. if( nextSeparatorIndex <= 0 )
  155. {
  156. Debug.LogError( "Entry rawUri is empty" );
  157. return null;
  158. }
  159. string rawUri = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
  160. separatorIndex = nextSeparatorIndex;
  161. result[i] = new FileSystemEntry( rawUri, entryName, isDirectory );
  162. }
  163. return result;
  164. }
  165. #endif
  166. try
  167. {
  168. FileSystemInfo[] items = new DirectoryInfo( path ).GetFileSystemInfos();
  169. FileSystemEntry[] result = new FileSystemEntry[items.Length];
  170. for( int i = 0; i < items.Length; i++ )
  171. result[i] = new FileSystemEntry( items[i] );
  172. return result;
  173. }
  174. catch( System.Exception e )
  175. {
  176. Debug.LogException( e );
  177. return null;
  178. }
  179. }
  180. public static string CreateFileInDirectory( string directoryPath, string filename )
  181. {
  182. #if !UNITY_EDITOR && UNITY_ANDROID
  183. if( ShouldUseSAF )
  184. return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, false, filename );
  185. #endif
  186. string path = Path.Combine( directoryPath, filename );
  187. using( File.Create( path ) ) { }
  188. return path;
  189. }
  190. public static string CreateFolderInDirectory( string directoryPath, string folderName )
  191. {
  192. #if !UNITY_EDITOR && UNITY_ANDROID
  193. if( ShouldUseSAF )
  194. return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, true, folderName );
  195. #endif
  196. string path = Path.Combine( directoryPath, folderName );
  197. Directory.CreateDirectory( path );
  198. return path;
  199. }
  200. public static void WriteBytesToFile( string targetPath, byte[] bytes )
  201. {
  202. #if !UNITY_EDITOR && UNITY_ANDROID
  203. if( ShouldUseSAF )
  204. {
  205. File.WriteAllBytes( TemporaryFilePath, bytes );
  206. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
  207. File.Delete( TemporaryFilePath );
  208. return;
  209. }
  210. #endif
  211. File.WriteAllBytes( targetPath, bytes );
  212. }
  213. public static void WriteTextToFile( string targetPath, string text )
  214. {
  215. #if !UNITY_EDITOR && UNITY_ANDROID
  216. if( ShouldUseSAF )
  217. {
  218. File.WriteAllText( TemporaryFilePath, text );
  219. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
  220. File.Delete( TemporaryFilePath );
  221. return;
  222. }
  223. #endif
  224. File.WriteAllText( targetPath, text );
  225. }
  226. public static void WriteCopyToFile( string targetPath, string sourceFile )
  227. {
  228. #if !UNITY_EDITOR && UNITY_ANDROID
  229. if( ShouldUseSAF )
  230. {
  231. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, sourceFile, false );
  232. return;
  233. }
  234. #endif
  235. File.Copy( sourceFile, targetPath, true );
  236. }
  237. public static void AppendBytesToFile( string targetPath, byte[] bytes )
  238. {
  239. #if !UNITY_EDITOR && UNITY_ANDROID
  240. if( ShouldUseSAF )
  241. {
  242. File.WriteAllBytes( TemporaryFilePath, bytes );
  243. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
  244. File.Delete( TemporaryFilePath );
  245. return;
  246. }
  247. #endif
  248. using( var stream = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
  249. {
  250. stream.Write( bytes, 0, bytes.Length );
  251. }
  252. }
  253. public static void AppendTextToFile( string targetPath, string text )
  254. {
  255. #if !UNITY_EDITOR && UNITY_ANDROID
  256. if( ShouldUseSAF )
  257. {
  258. File.WriteAllText( TemporaryFilePath, text );
  259. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
  260. File.Delete( TemporaryFilePath );
  261. return;
  262. }
  263. #endif
  264. File.AppendAllText( targetPath, text );
  265. }
  266. public static void AppendCopyToFile( string targetPath, string sourceFile )
  267. {
  268. #if !UNITY_EDITOR && UNITY_ANDROID
  269. if( ShouldUseSAF )
  270. {
  271. AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, sourceFile, true );
  272. return;
  273. }
  274. #endif
  275. using( Stream input = File.OpenRead( sourceFile ) )
  276. using( Stream output = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
  277. {
  278. byte[] buffer = new byte[4096];
  279. int bytesRead;
  280. while( ( bytesRead = input.Read( buffer, 0, buffer.Length ) ) > 0 )
  281. output.Write( buffer, 0, bytesRead );
  282. }
  283. }
  284. public static byte[] ReadBytesFromFile( string sourcePath )
  285. {
  286. #if !UNITY_EDITOR && UNITY_ANDROID
  287. if( ShouldUseSAF )
  288. {
  289. AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
  290. byte[] result = File.ReadAllBytes( TemporaryFilePath );
  291. File.Delete( TemporaryFilePath );
  292. return result;
  293. }
  294. #endif
  295. return File.ReadAllBytes( sourcePath );
  296. }
  297. public static string ReadTextFromFile( string sourcePath )
  298. {
  299. #if !UNITY_EDITOR && UNITY_ANDROID
  300. if( ShouldUseSAF )
  301. {
  302. AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
  303. string result = File.ReadAllText( TemporaryFilePath );
  304. File.Delete( TemporaryFilePath );
  305. return result;
  306. }
  307. #endif
  308. return File.ReadAllText( sourcePath );
  309. }
  310. public static void ReadCopyFromFile( string sourcePath, string destinationFile )
  311. {
  312. #if !UNITY_EDITOR && UNITY_ANDROID
  313. if( ShouldUseSAF )
  314. {
  315. AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, destinationFile );
  316. return;
  317. }
  318. #endif
  319. File.Copy( sourcePath, destinationFile, true );
  320. }
  321. public static string RenameFile( string path, string newName )
  322. {
  323. #if !UNITY_EDITOR && UNITY_ANDROID
  324. if( ShouldUseSAF )
  325. return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
  326. #endif
  327. string newPath = Path.Combine( Path.GetDirectoryName( path ), newName );
  328. File.Move( path, newPath );
  329. return newPath;
  330. }
  331. public static string RenameDirectory( string path, string newName )
  332. {
  333. #if !UNITY_EDITOR && UNITY_ANDROID
  334. if( ShouldUseSAF )
  335. return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
  336. #endif
  337. string newPath = Path.Combine( new DirectoryInfo( path ).Parent.FullName, newName );
  338. Directory.Move( path, newPath );
  339. return newPath;
  340. }
  341. public static void DeleteFile( string path )
  342. {
  343. #if !UNITY_EDITOR && UNITY_ANDROID
  344. if( ShouldUseSAF )
  345. {
  346. AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
  347. return;
  348. }
  349. #endif
  350. File.Delete( path );
  351. }
  352. public static void DeleteDirectory( string path )
  353. {
  354. #if !UNITY_EDITOR && UNITY_ANDROID
  355. if( ShouldUseSAF )
  356. {
  357. AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
  358. return;
  359. }
  360. #endif
  361. Directory.Delete( path, true );
  362. }
  363. public static string GetFilename( string path )
  364. {
  365. #if !UNITY_EDITOR && UNITY_ANDROID
  366. if( ShouldUseSAF )
  367. return AJC.CallStatic<string>( "SAFEntryName", Context, path );
  368. #endif
  369. return Path.GetFileName( path );
  370. }
  371. public static long GetFilesize( string path )
  372. {
  373. #if !UNITY_EDITOR && UNITY_ANDROID
  374. if( ShouldUseSAF )
  375. return AJC.CallStatic<long>( "SAFEntrySize", Context, path );
  376. #endif
  377. return new FileInfo( path ).Length;
  378. }
  379. public static System.DateTime GetLastModifiedDate( string path )
  380. {
  381. #if !UNITY_EDITOR && UNITY_ANDROID
  382. // Credit: https://stackoverflow.com/a/28504416/2373034
  383. if( ShouldUseSAF )
  384. return new System.DateTime( 1970, 1, 1, 0, 0, 0 ).AddMilliseconds( AJC.CallStatic<long>( "SAFEntryLastModified", Context, path ) );
  385. #endif
  386. return new FileInfo( path ).LastWriteTime;
  387. }
  388. }
  389. }