2019-02-13 20:43:30 +01:00
|
|
|
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
|
|
|
Gobbetti, Felix Konstantin Maurer, JohnnySun, Taavi Eomäe
|
2017-03-10 14:53:19 +01:00
|
|
|
|
|
|
|
This file is part of Gadgetbridge.
|
|
|
|
|
|
|
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Gadgetbridge is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
2015-07-08 23:03:34 +02:00
|
|
|
package nodomain.freeyourgadget.gadgetbridge.util;
|
|
|
|
|
2015-12-06 17:17:46 +01:00
|
|
|
import android.content.ContentResolver;
|
2015-10-29 00:46:52 +01:00
|
|
|
import android.content.Context;
|
2015-12-06 17:17:46 +01:00
|
|
|
import android.net.Uri;
|
2015-10-29 00:46:52 +01:00
|
|
|
import android.os.Environment;
|
|
|
|
|
2015-12-06 17:17:46 +01:00
|
|
|
import java.io.BufferedInputStream;
|
2018-01-04 15:13:06 +01:00
|
|
|
import java.io.BufferedOutputStream;
|
2015-12-07 18:15:26 +01:00
|
|
|
import java.io.BufferedReader;
|
2015-08-04 23:02:36 +02:00
|
|
|
import java.io.ByteArrayOutputStream;
|
2015-07-08 23:03:34 +02:00
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileInputStream;
|
2015-10-29 00:46:52 +01:00
|
|
|
import java.io.FileNotFoundException;
|
2015-07-08 23:03:34 +02:00
|
|
|
import java.io.FileOutputStream;
|
|
|
|
import java.io.IOException;
|
2015-08-04 23:02:36 +02:00
|
|
|
import java.io.InputStream;
|
2015-12-07 18:15:26 +01:00
|
|
|
import java.io.InputStreamReader;
|
2018-01-04 15:13:06 +01:00
|
|
|
import java.io.OutputStream;
|
2015-07-08 23:03:34 +02:00
|
|
|
import java.nio.channels.FileChannel;
|
2016-12-29 01:07:26 +01:00
|
|
|
import java.nio.charset.StandardCharsets;
|
2015-10-29 00:46:52 +01:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
2015-07-08 23:03:34 +02:00
|
|
|
|
2019-01-26 15:52:40 +01:00
|
|
|
import androidx.annotation.NonNull;
|
2015-07-08 23:03:34 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
2017-04-27 07:52:11 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
|
2015-07-08 23:03:34 +02:00
|
|
|
|
|
|
|
public class FileUtils {
|
2015-10-29 00:46:52 +01:00
|
|
|
// Don't use slf4j here -- would be a bootstrapping problem
|
|
|
|
private static final String TAG = "FileUtils";
|
|
|
|
|
2015-07-08 23:03:34 +02:00
|
|
|
/**
|
|
|
|
* Copies the the given sourceFile to destFile, overwriting it, in case it exists.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
2015-07-08 23:03:34 +02:00
|
|
|
* @param sourceFile
|
|
|
|
* @param destFile
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
|
|
|
public static void copyFile(File sourceFile, File destFile) throws IOException {
|
|
|
|
if (!sourceFile.exists()) {
|
|
|
|
throw new IOException("Does not exist: " + sourceFile.getAbsolutePath());
|
|
|
|
}
|
2017-04-09 01:09:23 +02:00
|
|
|
try (FileInputStream in = new FileInputStream(sourceFile); FileOutputStream out = new FileOutputStream(destFile)) {
|
|
|
|
copyFile(in, out);
|
|
|
|
}
|
2015-07-08 23:03:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private static void copyFile(FileInputStream sourceStream, FileOutputStream destStream) throws IOException {
|
|
|
|
try (FileChannel fromChannel = sourceStream.getChannel(); FileChannel toChannel = destStream.getChannel()) {
|
|
|
|
fromChannel.transferTo(0, fromChannel.size(), toChannel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-29 01:07:26 +01:00
|
|
|
/**
|
|
|
|
* Copies the contents of the given input stream to the destination file.
|
|
|
|
* @param inputStream the contents to write. Note: the caller has to close the input stream!
|
|
|
|
* @param destFile the file to write to
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
2016-03-08 11:41:20 +01:00
|
|
|
public static void copyStreamToFile(InputStream inputStream, File destFile) throws IOException {
|
2016-12-29 01:07:26 +01:00
|
|
|
try (FileOutputStream fout = new FileOutputStream(destFile)) {
|
|
|
|
byte[] buf = new byte[4096];
|
|
|
|
while (inputStream.available() > 0) {
|
|
|
|
int bytes = inputStream.read(buf);
|
|
|
|
fout.write(buf, 0, bytes);
|
|
|
|
}
|
2016-03-08 11:41:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-07 12:50:59 +01:00
|
|
|
/**
|
|
|
|
* Copies the contents of the given file to the destination output stream.
|
|
|
|
* @param src the file from which to read.
|
|
|
|
* @param dst the output stream that is written to. Note: the caller has to close the output stream!
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
2018-01-04 15:13:06 +01:00
|
|
|
public static void copyFileToStream(File src, OutputStream dst) throws IOException {
|
|
|
|
try (FileInputStream in = new FileInputStream(src)) {
|
|
|
|
byte[] buf = new byte[4096];
|
|
|
|
while(in.available() > 0) {
|
|
|
|
int bytes = in.read(buf);
|
|
|
|
dst.write(buf, 0, bytes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-06 17:17:46 +01:00
|
|
|
public static void copyURItoFile(Context ctx, Uri uri, File destFile) throws IOException {
|
2015-12-08 18:59:37 +01:00
|
|
|
if (uri.getPath().equals(destFile.getPath())) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-06 17:17:46 +01:00
|
|
|
ContentResolver cr = ctx.getContentResolver();
|
2016-12-29 01:07:26 +01:00
|
|
|
InputStream in = cr.openInputStream(uri);
|
|
|
|
if (in == null) {
|
|
|
|
throw new IOException("unable to open input stream: " + uri);
|
|
|
|
}
|
|
|
|
try (InputStream fin = new BufferedInputStream(in)) {
|
|
|
|
copyStreamToFile(fin, destFile);
|
|
|
|
fin.close();
|
2015-12-06 17:17:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-07 12:50:59 +01:00
|
|
|
/**
|
|
|
|
* Copies the content of a file to an uri,
|
|
|
|
* which for example was retrieved using the storage access framework.
|
|
|
|
* @param context the application context.
|
|
|
|
* @param src the file from which the content should be copied.
|
|
|
|
* @param dst the destination uri.
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
2018-01-04 15:13:06 +01:00
|
|
|
public static void copyFileToURI(Context context, File src, Uri dst) throws IOException {
|
|
|
|
OutputStream out = context.getContentResolver().openOutputStream(dst);
|
|
|
|
if (out == null) {
|
|
|
|
throw new IOException("Unable to open output stream for " + dst.toString());
|
|
|
|
}
|
|
|
|
try (OutputStream bufOut = new BufferedOutputStream(out)) {
|
|
|
|
copyFileToStream(src, bufOut);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-29 01:07:26 +01:00
|
|
|
/**
|
|
|
|
* Returns the textual contents of the given file. The contents is expected to be
|
|
|
|
* in UTF-8 encoding.
|
|
|
|
* @param file the file to read
|
|
|
|
* @return the file contents as a newline-delimited string
|
|
|
|
* @throws IOException
|
|
|
|
* @see #getStringFromFile(File, String)
|
|
|
|
*/
|
2015-12-07 18:15:26 +01:00
|
|
|
public static String getStringFromFile(File file) throws IOException {
|
2016-12-29 01:07:26 +01:00
|
|
|
return getStringFromFile(file, StandardCharsets.UTF_8.name());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the textual contents of the given file. The contents will be interpreted using the
|
|
|
|
* given encoding.
|
|
|
|
* @param file the file to read
|
|
|
|
* @return the file contents as a newline-delimited string
|
|
|
|
* @throws IOException
|
|
|
|
* @see #getStringFromFile(File)
|
|
|
|
*/
|
|
|
|
public static String getStringFromFile(File file, String encoding) throws IOException {
|
2015-12-07 18:15:26 +01:00
|
|
|
FileInputStream fin = new FileInputStream(file);
|
|
|
|
|
2016-12-29 01:07:26 +01:00
|
|
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(fin, encoding))) {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
String line;
|
|
|
|
while ((line = reader.readLine()) != null) {
|
|
|
|
sb.append(line).append("\n");
|
|
|
|
}
|
|
|
|
return sb.toString();
|
2015-12-07 18:15:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-08 23:03:34 +02:00
|
|
|
/**
|
2015-11-01 20:49:50 +01:00
|
|
|
* Returns the existing external storage dir. The directory is guaranteed to
|
|
|
|
* exist and to be writable.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
2015-07-08 23:03:34 +02:00
|
|
|
* @throws IOException when the directory is not available
|
|
|
|
*/
|
|
|
|
public static File getExternalFilesDir() throws IOException {
|
2015-10-29 00:46:52 +01:00
|
|
|
List<File> dirs = getWritableExternalFilesDirs();
|
|
|
|
for (File dir : dirs) {
|
|
|
|
if (canWriteTo(dir)) {
|
|
|
|
return dir;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new IOException("no writable external directory found");
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean canWriteTo(File dir) {
|
|
|
|
File file = new File(dir, "gbtest");
|
|
|
|
try {
|
|
|
|
FileOutputStream test = new FileOutputStream(file);
|
|
|
|
try {
|
|
|
|
test.close();
|
|
|
|
} catch (IOException e) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
file.delete();
|
|
|
|
return true;
|
|
|
|
} catch (FileNotFoundException e) {
|
2016-06-27 21:29:39 +02:00
|
|
|
GB.log("Cannot write to directory: " + dir.getAbsolutePath(), GB.INFO, e);
|
2015-10-29 00:46:52 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-01 20:49:50 +01:00
|
|
|
/**
|
|
|
|
* Returns a list of directories to write to. The list is sorted by priority,
|
|
|
|
* i.e. the first directory should be preferred, the last one is the least
|
|
|
|
* preferred one.
|
2015-12-06 17:17:46 +01:00
|
|
|
* <p/>
|
2015-11-01 20:49:50 +01:00
|
|
|
* Note that the directories may not exist, so it is not guaranteed that you
|
|
|
|
* can actually write to them. But when created, they *should* be writable.
|
2015-12-06 17:17:46 +01:00
|
|
|
*
|
2015-11-01 20:49:50 +01:00
|
|
|
* @return the list of writable directories
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
|
|
|
@NonNull
|
2015-10-29 00:46:52 +01:00
|
|
|
private static List<File> getWritableExternalFilesDirs() throws IOException {
|
|
|
|
Context context = GBApplication.getContext();
|
2016-08-24 22:54:32 +02:00
|
|
|
File[] dirs;
|
|
|
|
try {
|
|
|
|
dirs = context.getExternalFilesDirs(null);
|
2016-08-28 23:51:10 +02:00
|
|
|
} catch (NullPointerException | UnsupportedOperationException ex) {
|
2018-03-30 15:38:29 +02:00
|
|
|
// workaround for robolectric 3.1.2 not implementing getExternalFilesDirs()
|
2016-08-24 22:54:32 +02:00
|
|
|
// https://github.com/robolectric/robolectric/issues/2531
|
|
|
|
File dir = context.getExternalFilesDir(null);
|
|
|
|
if (dir != null) {
|
|
|
|
dirs = new File[] { dir };
|
|
|
|
} else {
|
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
2015-10-29 00:46:52 +01:00
|
|
|
if (dirs == null) {
|
|
|
|
throw new IOException("Unable to access external files dirs: null");
|
|
|
|
}
|
2015-11-18 23:21:56 +01:00
|
|
|
List<File> result = new ArrayList<>(dirs.length);
|
2015-10-29 00:46:52 +01:00
|
|
|
if (dirs.length == 0) {
|
|
|
|
throw new IOException("Unable to access external files dirs: 0");
|
2015-07-08 23:03:34 +02:00
|
|
|
}
|
2015-10-29 00:46:52 +01:00
|
|
|
for (int i = 0; i < dirs.length; i++) {
|
|
|
|
File dir = dirs[i];
|
2016-06-27 22:01:52 +02:00
|
|
|
if (dir == null) {
|
2015-10-29 00:46:52 +01:00
|
|
|
continue;
|
|
|
|
}
|
2016-06-27 22:01:52 +02:00
|
|
|
if (!dir.exists() && !dir.mkdirs()) {
|
|
|
|
GB.log("Unable to create directories: " + dir.getAbsolutePath(), GB.INFO, null);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2015-11-01 20:49:50 +01:00
|
|
|
// the first directory is also the primary external storage, i.e. the same as Environment.getExternalFilesDir()
|
|
|
|
// TODO: check the mount state of *all* dirs when switching to later API level
|
2017-04-27 07:52:11 +02:00
|
|
|
if (!GBEnvironment.env().isLocalTest()) { // don't do this with robolectric
|
|
|
|
if (i == 0 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
|
|
|
|
GB.log("ignoring unmounted external storage dir: " + dir, GB.INFO, null);
|
|
|
|
continue;
|
|
|
|
}
|
2015-10-29 00:46:52 +01:00
|
|
|
}
|
2015-11-01 20:49:50 +01:00
|
|
|
result.add(dir); // add last
|
2015-07-08 23:03:34 +02:00
|
|
|
}
|
2015-10-29 00:46:52 +01:00
|
|
|
return result;
|
2015-07-08 23:03:34 +02:00
|
|
|
}
|
2015-08-04 23:02:36 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Reads the contents of the given InputStream into a byte array, but does not
|
2015-08-17 22:46:39 +02:00
|
|
|
* read more than maxLen bytes. If the stream provides more than maxLen bytes,
|
|
|
|
* an IOException is thrown.
|
2015-09-24 14:45:21 +02:00
|
|
|
*
|
|
|
|
* @param in the stream to read from
|
2015-08-04 23:02:36 +02:00
|
|
|
* @param maxLen the maximum number of bytes to read/return
|
|
|
|
* @return the bytes read from the InputStream
|
|
|
|
* @throws IOException when reading failed or when maxLen was exceeded
|
|
|
|
*/
|
|
|
|
public static byte[] readAll(InputStream in, long maxLen) throws IOException {
|
|
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(8192, in.available()));
|
|
|
|
byte[] buf = new byte[8192];
|
2017-04-09 01:09:23 +02:00
|
|
|
int read;
|
2015-08-04 23:02:36 +02:00
|
|
|
long totalRead = 0;
|
|
|
|
while ((read = in.read(buf)) > 0) {
|
|
|
|
out.write(buf, 0, read);
|
|
|
|
totalRead += read;
|
|
|
|
if (totalRead > maxLen) {
|
2017-04-09 01:09:23 +02:00
|
|
|
throw new IOException("Too much data to read into memory. Got already " + totalRead);
|
2015-08-04 23:02:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return out.toByteArray();
|
|
|
|
}
|
2016-05-26 23:46:21 +02:00
|
|
|
|
|
|
|
public static boolean deleteRecursively(File dir) {
|
|
|
|
if (!dir.exists()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (dir.isFile()) {
|
|
|
|
return dir.delete();
|
|
|
|
}
|
|
|
|
for (File sub : dir.listFiles()) {
|
|
|
|
if (!deleteRecursively(sub)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dir.delete();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static File createTempDir(String prefix) throws IOException {
|
|
|
|
File parent = new File(System.getProperty("java.io.tmpdir", "/tmp"));
|
|
|
|
for (int i = 1; i < 100; i++) {
|
|
|
|
String name = prefix + (int) (Math.random() * 100000);
|
|
|
|
File dir = new File(parent, name);
|
|
|
|
if (dir.mkdirs()) {
|
|
|
|
return dir;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new IOException("Cannot create temporary directory in " + parent);
|
|
|
|
}
|
2017-10-19 21:52:38 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces some wellknown invalid characters in the given filename
|
|
|
|
* to underscrores.
|
|
|
|
* @param name the file name to make valid
|
|
|
|
* @return the valid file name
|
|
|
|
*/
|
|
|
|
public static String makeValidFileName(String name) {
|
2018-08-01 19:33:02 +02:00
|
|
|
return name.replaceAll("[\0/:\\r\\n\\\\]", "_");
|
2017-10-19 21:52:38 +02:00
|
|
|
}
|
2015-07-08 23:03:34 +02:00
|
|
|
}
|