From c6ff3c52130cb5d97861f5f6a0076edb48dd4b97 Mon Sep 17 00:00:00 2001 From: buddygr Date: Fri, 8 Sep 2023 11:19:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EDocumentsUtils=20fix=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 201 ------- build.gradle | 7 +- .../java/com/quseit/base/QBaseActivity.java | 2 +- src/main/java/com/quseit/base/QBaseApp.java | 3 +- .../com/quseit/base/QBaseUpdateService.java | 4 +- .../java/com/quseit/common/CrashHandler.java | 23 +- .../com/quseit/service/DownloaderBase.java | 6 +- .../java/com/quseit/util/DocumentsUtils.java | 510 ++++++++++++++++++ src/main/java/com/quseit/util/FileHelper.java | 8 +- src/main/java/com/quseit/util/FileUtils.java | 7 +- src/main/java/com/quseit/util/NAction.java | 2 +- src/main/java/com/quseit/util/NUtil.java | 4 +- src/main/java/com/quseit/util/Utils.java | 6 +- 13 files changed, 546 insertions(+), 237 deletions(-) delete mode 100644 LICENSE create mode 100644 src/main/java/com/quseit/util/DocumentsUtils.java diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 29f81d8..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/build.gradle b/build.gradle index 924d459..f2b59a5 100644 --- a/build.gradle +++ b/build.gradle @@ -22,9 +22,9 @@ android { } } repositories { - flatDir { + /*flatDir { dirs 'libs' - } + }*/ } useLibrary 'org.apache.http.legacy' @@ -36,8 +36,7 @@ dependencies { //compile 'com.loopj.android:android-async-http:1.4.8' api project(':qftplib') - api files('libs/android-async-http-1.4.8.aar') - //api 'com.loopj.android:android-async-http:1.4.8' + api 'com.loopj.android:android-async-http:1.4.8' debugApi rootProject.ext.leakcanaryDebug releaseApi rootProject.ext.leakcanaryRelease diff --git a/src/main/java/com/quseit/base/QBaseActivity.java b/src/main/java/com/quseit/base/QBaseActivity.java index 6df9d19..a5c087f 100644 --- a/src/main/java/com/quseit/base/QBaseActivity.java +++ b/src/main/java/com/quseit/base/QBaseActivity.java @@ -203,7 +203,7 @@ public abstract class QBaseActivity extends Activity { pq.deleteAllStat_0_Log(); // } - int localVersion = NUtil.getVersinoCode(context); + int localVersion = NUtil.getVersionCode(context); try { List ks = Utils.copyIterator(result.keys()); diff --git a/src/main/java/com/quseit/base/QBaseApp.java b/src/main/java/com/quseit/base/QBaseApp.java index b1fb785..7da4508 100644 --- a/src/main/java/com/quseit/base/QBaseApp.java +++ b/src/main/java/com/quseit/base/QBaseApp.java @@ -2,7 +2,6 @@ package com.quseit.base; import android.app.Activity; import android.content.Context; -import android.os.Environment; import com.loopj.android.http.AsyncHttpClient; import com.quseit.config.BASE_CONF; @@ -51,7 +50,7 @@ public class QBaseApp { public String getOrCreateRoot(Context context, String subDir) { // String path = Environment.getExternalStorageDirectory().getAbsolutePath()+ (root==null?BASE_CONF.DEFAULT_ROOT:root) // FileUtils.createTestDir(context,"test"); - String path = FileUtils.getQyPath(context) + (root == null ? BASE_CONF.DEFAULT_ROOT : root) + String path = FileUtils.getQyPath() + (root == null ? BASE_CONF.DEFAULT_ROOT : root) + (subDir != null ? ("/" + subDir) : ""); diff --git a/src/main/java/com/quseit/base/QBaseUpdateService.java b/src/main/java/com/quseit/base/QBaseUpdateService.java index aae1478..ec7a510 100644 --- a/src/main/java/com/quseit/base/QBaseUpdateService.java +++ b/src/main/java/com/quseit/base/QBaseUpdateService.java @@ -94,10 +94,10 @@ public abstract class QBaseUpdateService extends Service { Intent updateIntent = new Intent(); updateIntent.setClassName(this.getPackageName(), this.getPackageName() + ".MIndexAct"); - updatePendingIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, updateIntent, 0); + updatePendingIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE); } else { Intent updateIntent = x; - updatePendingIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, updateIntent, 0); + updatePendingIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE); } Notification updateNotification = NAction.getNotification(getApplicationContext(), getString(R.string.up_soft_update), "%0", updatePendingIntent, R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT); diff --git a/src/main/java/com/quseit/common/CrashHandler.java b/src/main/java/com/quseit/common/CrashHandler.java index 1265830..8030234 100644 --- a/src/main/java/com/quseit/common/CrashHandler.java +++ b/src/main/java/com/quseit/common/CrashHandler.java @@ -33,6 +33,7 @@ public class CrashHandler implements UncaughtExceptionHandler { private Context mContext; private Thread.UncaughtExceptionHandler mDefaultHandler; private Map infos = new HashMap(); + public static String errlog; public static CrashHandler getInstance() { if (INSTANCE == null) { @@ -48,7 +49,7 @@ public class CrashHandler implements UncaughtExceptionHandler { if (!NAction.getExtP(context, "conf_enable_crash_log").equals("0")) { AppLog appDB = new AppLog(context); - String ver = String.valueOf(NUtil.getVersinoCode(context)); + String ver = String.valueOf(NUtil.getVersionCode(context)); if (!appDB.ifLogExists(ver, data)) { Log.d(TAG, "WriteSettings no exits:"+data); appDB.insertNewLog(name, ver, NAction.getUserNoId(context), data); @@ -57,10 +58,12 @@ public class CrashHandler implements UncaughtExceptionHandler { } // File log = new File(Environment.getExternalStorageDirectory()+"/"+NAction.getCode(context)+"_last_err.log"); - File log = new File(FileUtils.getQyPath(context) +"/"+NAction.getCode(context)+"_last_err.log"); - if (log.exists()) { - log.delete(); - } + File log = new File(errlog); + if (!log.getAbsoluteFile().getParentFile().exists()) { + log.getAbsoluteFile().getParentFile().mkdirs(); + } else if (log.exists()) { // clear log + log.delete(); + } byte[] datas = data.getBytes(); FileOutputStream outStream; try { @@ -114,10 +117,11 @@ public class CrashHandler implements UncaughtExceptionHandler { @Override public void run() { try { - Looper.prepare(); -// Toast.makeText(mContext, MessageFormat.format(mContext.getString(R.string.err_caught), Environment.getExternalStorageDirectory()+"/"+NAction.getCode(mContext)+"_last_err.log"), Toast.LENGTH_LONG).show(); - Toast.makeText(mContext, MessageFormat.format(mContext.getString(R.string.err_caught), mContext.getApplicationContext().getExternalFilesDir(null).getAbsolutePath() +"/"+ NAction.getCode(mContext)+"_last_err.log"), Toast.LENGTH_LONG).show(); - Looper.loop(); + Looper.prepare(); + Toast.makeText(mContext, MessageFormat.format(mContext.getString(R.string.err_caught), + //Environment.getExternalStorageDirectory()+"/"+NAction.getCode(mContext)+"_last_err.log" + errlog), Toast.LENGTH_LONG).show(); + Looper.loop(); } catch (InflateException e) { Log.e(TAG, "error : ", e); } @@ -224,5 +228,6 @@ public class CrashHandler implements UncaughtExceptionHandler { mContext = ctx; mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); + errlog = ctx.getExternalFilesDir("log")+"/qpython_last_error.log"; } } diff --git a/src/main/java/com/quseit/service/DownloaderBase.java b/src/main/java/com/quseit/service/DownloaderBase.java index 5c4660f..0834965 100644 --- a/src/main/java/com/quseit/service/DownloaderBase.java +++ b/src/main/java/com/quseit/service/DownloaderBase.java @@ -292,7 +292,7 @@ public abstract class DownloaderBase extends Service { updateIntent = new Intent(this, getMan()); updatePendingIntent = PendingIntent.getActivity(this, - NOTIFICATION_ID, updateIntent, 0); + NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE); Notification downloadNotification = NAction.getNotification(getApplicationContext(), mTitle + "(" + mArtist + ")", "0%", updatePendingIntent, R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT); @@ -500,7 +500,7 @@ public abstract class DownloaderBase extends Service { updateIntent = new Intent(this, getMan()); updatePendingIntent = PendingIntent.getActivity(this, - NOTIFICATION_ID, updateIntent, 0); + NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE); Notification downloadNotification = NAction.getNotification(getApplicationContext(), mTitle + "(" + mArtist + ")", getString(R.string.up_soft_download), updatePendingIntent, R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT); @@ -635,7 +635,7 @@ public abstract class DownloaderBase extends Service { if (mCompletedSize * 100 / fileLenght % 2 == 0) { updatePendingIntent = PendingIntent.getActivity(DownloaderBase.this, - NOTIFICATION_ID, updateIntent, 0); + NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE); Notification downloadNotification = NAction.getNotification(getApplicationContext(), mTitle + "(" + mArtist + ")", mCompletedSize * 100 / fileLenght + "%", updatePendingIntent, R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT); diff --git a/src/main/java/com/quseit/util/DocumentsUtils.java b/src/main/java/com/quseit/util/DocumentsUtils.java new file mode 100644 index 0000000..815d724 --- /dev/null +++ b/src/main/java/com/quseit/util/DocumentsUtils.java @@ -0,0 +1,510 @@ +package com.quseit.util; +//by 乘着船 at 2021-2023 + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class DocumentsUtils { + + private static final String TAG = DocumentsUtils.class.getSimpleName(); + + //public static final int OPEN_DOCUMENT_TREE_CODE = 8000; + public static final int MAX_BUFFER_SIZE = 5242880;//max buffer size 5MB + + public static final String SDCARD = Environment.getExternalStorageDirectory().getPath(); + public static final String SDCARD_PATH = SDCARD + "/"; + public static final String CONTENT_PRF = "content://"; + public static final String[] SDCARD_CONTENT = new String[]{ + CONTENT_PRF + "com.android.externalstorage.documents/tree/primary%3A", "/document/primary%3A"}; + public static final String ANDROID_PATH = SDCARD_PATH + "Android/"; + public static final String[] ANDROID_CONTENT = new String[]{ + SDCARD_CONTENT[0] + "Android%2F", SDCARD_CONTENT[1] + "Android%2F"}; + + public static final int ANDROID_SAVE_INTENT = + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION ; + public static final int ANDROID_OPEN_INTENT = + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | + ANDROID_SAVE_INTENT ; + + public static List sExtSdCardPaths = new ArrayList<>(); + + //private static String requestRootPath = null; + + private DocumentsUtils() { + + } + + public static void cleanCache() { + sExtSdCardPaths.clear(); + } + + /** + * Get a list of external SD card paths. (Kitkat or higher.) + * + * @return A list of external SD card paths. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String[] getExtSdCardPaths(Context context) { + if (sExtSdCardPaths.size() > 0) { + return sExtSdCardPaths.toArray(new String[0]); + } + for (File file : context.getExternalFilesDirs("external")) { + if (file != null && !file.equals(context.getExternalFilesDir("external"))) { + int index = file.getAbsolutePath().lastIndexOf("/Android/data"); + if (index < 0) { + Log.d(TAG, "Unexpected external file dir: " + file.getAbsolutePath()); + } else { + String path = file.getAbsolutePath().substring(0, index); + try { + path = new File(path).getCanonicalPath(); + } catch (IOException e) { + // Keep non-canonical path. + } + sExtSdCardPaths.add(path); + } + } + } + if (sExtSdCardPaths.isEmpty()) sExtSdCardPaths.add(SDCARD); + return sExtSdCardPaths.toArray(new String[0]); + } + + /** + * Determine the main folder of the external SD card containing the given file. + * + * @param file the file. + * @return The main folder of the external SD card containing this file, if the file is on an SD + * card. Otherwise, + * null is returned. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String getExtSdCardFolder(final File file, Context context) { + String[] extSdPaths = getExtSdCardPaths(context); + try { + for (String extSdPath : extSdPaths) { + if (file.getCanonicalPath().startsWith(extSdPath)) { + return extSdPath; + } + } + } catch (IOException e) { + return null; + } + return null; + } + + /** + * Determine if a file is on external sd card. (Kitkat or higher.) + * + * @param file The file. + * @return true if on external sd card. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static boolean isOnExtSdCard(final File file, Context c) { + return getExtSdCardFolder(file, c) != null; + } + + /** + * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). + * If the file is not + * existing, it is created. + * + * @param file The file. + * @param isDirectory true/false/null + * true/false - flag indicating if the file should be a directory , + * if file not exist, it will be create . + * null - do not know the file is a directory , + * if file not exist, it will not be create . + * @return The DocumentFile + */ + public static DocumentFile getDocumentFile( + final File file, final Boolean isDirectory, Context context) { + + String baseFolder = getExtSdCardFolder(file, context); + boolean originalDirectory = false; + if (baseFolder == null) { + return null; + } + + String relativePath = null; + try { + String fullPath = file.getCanonicalPath(); + if (!baseFolder.equals(fullPath)) { + relativePath = fullPath.substring(baseFolder.length() + 1); + } else { + originalDirectory = true; + } + } catch (IOException e) { + return null; + } catch (Exception f) { + originalDirectory = true; + //continue + } + String as = PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder, + null); + + Uri treeUri = null; + if (as != null) treeUri = Uri.parse(as); + if (treeUri == null) { + return null; + } + + // start with root of SD card and then parse through document tree. + DocumentFile document = DocumentFile.fromTreeUri(context, treeUri); + if (originalDirectory) return document; + String[] parts = relativePath.split("/"); + for (int i = 0; i < parts.length; i++) { + DocumentFile nextDocument = document.findFile(parts[i]); + + if (nextDocument == null) { + if(isDirectory == null) return null; + if ((i < parts.length - 1) || isDirectory) { + nextDocument = document.createDirectory(parts[i]); + } else { + nextDocument = document.createFile("image", parts[i]); + } + } + document = nextDocument; + } + + return document; + } + + public static boolean mkdirs(Context context, File dir) { + boolean res = dir.mkdirs(); + if (!res) { + if (isOnExtSdCard(dir, context)) { + DocumentFile documentFile = getDocumentFile(dir, true, context); + res = documentFile != null && documentFile.canWrite(); + } + } + return res; + } + + private static boolean FileDelete(File file) { + if (file.isFile()) return file.delete(); + File[] subFiles = file.listFiles(); + if (subFiles==null || subFiles.length==0) + return file.delete(); + boolean ret = true; + for(File subFile:subFiles){ + if (subFile.isDirectory()) ret = FileDelete(subFile) && ret; + else ret = subFile.delete() && ret; + } + return ret && file.delete(); + } + + public static boolean delete(Context context, File file) { + boolean ret = FileDelete(file); + if (ret) return ret; + //if (isOnExtSdCard(file, context)) { + DocumentFile f = getDocumentFile(file, false, context); + if (f != null) { + ret = f.delete(); + } + //} + return ret; + } + + public static boolean canWrite(File file) { + boolean res = file.exists() && file.canWrite(); + + if (!res && !file.exists()) { + try { + if (!file.isDirectory()) { + res = file.createNewFile() && file.delete(); + } else { + res = file.mkdirs() && file.delete(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return res; + } + + /*public static boolean canWrite(Context context, File file) { + boolean res = canWrite(file); + + if (!res && isOnExtSdCard(file, context)) { + DocumentFile documentFile = getDocumentFile(file, true, context); + res = documentFile != null && documentFile.canWrite(); + } + return res; + }*/ + + private static boolean renameToCross(Context context,File src,File dest) throws Exception { + copy(context,src,dest); + boolean exist = dest.exists(); + if (!exist) { + DocumentFile Dest = getDocumentFile(dest,null,context); + if(Dest!=null && Dest.exists()) + exist = true; + } + delete(context,src); + return exist; + } + + public static boolean renameTo(Context context, File src, File dest) throws Exception { + boolean res = src.renameTo(dest); + if (res) return true; + + if (isOnExtSdCard(dest, context)) { + DocumentFile srcDoc; + if (isOnExtSdCard(src, context)) { + srcDoc = getDocumentFile(src, false, context); + } else { + srcDoc = DocumentFile.fromFile(src); + } + DocumentFile destDoc = getDocumentFile(dest.getParentFile(), true, context); + + if (srcDoc != null && destDoc != null) { + if(!srcDoc.exists()) + throw new FileNotFoundException(src.toString()); + if (Objects.equals(src.getParent(), dest.getParent())) { + DocumentFile DestDoc = getDocumentFile(dest, null, context); + if(DestDoc!=null && DestDoc.exists()){ + if(!DestDoc.delete()) + return false; + } + res = srcDoc.renameTo(dest.getName()); + }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + DocumentFile srcParentDoc = srcDoc.getParentFile(); + if (srcParentDoc!=null) { + res = DocumentsContract.moveDocument(context.getContentResolver(), + srcDoc.getUri(), + srcParentDoc.getUri(), + destDoc.getUri()) != null; + }*/ else return renameToCross(context, src, dest); + //} + } else { + if ((src.exists() || srcDoc!=null) && (dest.canWrite() || destDoc!=null)) + return renameToCross(context, src, dest); + else return false; + } + } else return renameToCross(context, src, dest); + + return res; + } + + public static InputStream getInputStream(Context context, File destFile) { + InputStream in = null; + try { + if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { + DocumentFile file = getDocumentFile(destFile, false, context); + if (file != null && file.canWrite()) { + in = context.getContentResolver().openInputStream(file.getUri()); + } + } else { + in = new FileInputStream(destFile); + + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return in; + } + + public static OutputStream getOutputStream(Context context, File destFile) { + OutputStream out = null; + try { + if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { + DocumentFile file = getDocumentFile(destFile, false, context); + if (file != null && file.canWrite()) + out = context.getContentResolver().openOutputStream(file.getUri(),"wt"); + } else { + out = new FileOutputStream(destFile); + + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return out; + } + + public static void saveTreeUri(Context context, String rootPath, Uri uri) { + DocumentFile file = DocumentFile.fromTreeUri(context, uri); + if (file != null && file.canWrite()) { + SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); + perf.edit().putString(rootPath, uri.toString()).apply(); + } else { + Log.e(TAG, "no write permission: " + rootPath); + } + } + + /*public static boolean checkWritableRootPath(Context context, String rootPath) { + File root = new File(rootPath); + if (!root.canWrite()) { + + if (isOnExtSdCard(root, context)) { + DocumentFile documentFile = getDocumentFile(root, true, context); + return documentFile == null || !documentFile.canWrite(); + } else { + SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); + + String documentUri = perf.getString(rootPath, ""); + + if (documentUri == null || documentUri.isEmpty()) { + return true; + } else { + DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri)); + return !(file != null && file.canWrite()); + } + } + } + return false; + }*/ + + private static void copyFile ( + Context context, File srcFile, File destFile) throws Exception { + InputStream fis=getInputStream(context,srcFile); + if (fis==null) fis=new FileInputStream(srcFile); + OutputStream fos=getOutputStream(context,destFile); + if (fos==null) fos=new FileOutputStream(destFile); + int len = fis.available(); + if (len>MAX_BUFFER_SIZE) len=MAX_BUFFER_SIZE; + byte[] flush =new byte [len]; + while((len=fis.read(flush))>0) { + fos.write(flush,0,len); + } + fos.flush(); + fis.close(); + fos.close(); + } + + private static void copyTree ( + Context context,File srcFolder,File destFolder) + throws Exception{ + DocumentFile DestFolder=getDocumentFile(destFolder,true,context); + if (DestFolder==null) destFolder.mkdirs(); + DocumentFile SrcFolder=getDocumentFile(srcFolder,true,context); + String name; + File srcSub,destSub; + if (SrcFolder != null) { + DocumentFile[] SrcSubs = SrcFolder.listFiles(); + for(DocumentFile SrcSub:SrcSubs) { + name = SrcSub.getName(); + srcSub=new File(srcFolder.getAbsolutePath(), name); + destSub=new File(destFolder.getAbsolutePath(), name); + if (SrcSub.isDirectory()){ + copyTree(context,srcSub,destSub); + } else { + copyFile(context,srcSub,destSub); + } + } + } else { + File[] SrcSubs = srcFolder.listFiles(); + for(File SrcSub:SrcSubs) { + name = SrcSub.getName(); + srcSub=new File(srcFolder.getAbsolutePath(), name); + destSub=new File(destFolder.getAbsolutePath(), name); + if (SrcSub.isDirectory()){ + copyTree(context,srcSub,destSub); + } else { + copyFile(context,srcSub,destSub); + } + } + } + } + + public static void copy ( + Context context, File src, File dest) + throws Exception{ + if (isDirectory(context,src)) + copyTree(context,src,dest); + else copyFile(context,src,dest); + } + + public static String[] listFiles ( + Context context,File folder) + throws Exception{ + DocumentFile Folder=getDocumentFile(folder,true,context); + if (Folder == null) { + File[] Subs = folder.listFiles(); + if (Subs==null) return null; + String[] subs=new String[Subs.length]; + for (int i=0;i