package org.commcare.android.util;
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.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Vector;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import org.commcare.resources.model.MissingMediaException;
import org.commcare.resources.model.Resource;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.Reference;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.util.PropertyUtils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
/**
* @author ctsims
*
*/
public class FileUtil {
public static final String LOG_TOKEN = "cc-file-util";
public static boolean createFolder(String path) {
boolean made = true;
File dir = new File(path);
if (!dir.exists()) {
made = dir.mkdirs();
}
return made;
}
public static boolean deleteFile(File f) {
if(!f.exists()) { return true; }
if(!f.isDirectory()) {
return f.delete();
} else {
for(File child : f.listFiles()) {
if(!deleteFile(child)) {
return false;
}
}
return f.delete();
}
}
public static boolean cleanFilePath(String fullPath, String extendedPath) {
//There are actually a few things that can go wrong here, should be careful
//No extended path, life is good.
if(extendedPath == null) { return true;}
//Something's weird, bail!
if(!fullPath.contains(extendedPath)) { return true;}
//Get the root that we should stop at
File terminal = new File(fullPath.replace(extendedPath, ""));
File walker = new File(fullPath);
//technically we shouldn't ever hit the first case here, but also don't wanna get stuck by a weird equality bug.
while(walker != null && !terminal.equals(walker)) {
if(walker.isDirectory()) {
//only wipe out empty directories.
if(walker.list().length == 0) {
if(!walker.delete()) {
//I don't think we actually want to fail here, it's not a showstopper.
Log.w("cleanup", "couldn't delete directory " + walker.getAbsolutePath() + " while cleaning up file paths");
//throw an exception/false here if we care.
}
}
}
walker = walker.getParentFile();
}
return true;
}
public static void deleteFileOrDir(String fileName) {
File file = new File(fileName);
if (file.exists()) {
if (file.isDirectory()) {
// delete all the containing files
File[] files = file.listFiles();
for (File f : files) {
// should make this recursive if we get worried about
// the media directory containing directories
Log.i(LOG_TOKEN, "attempting to delete file: " + f.getAbsolutePath());
f.delete();
}
}
file.delete();
Log.i(LOG_TOKEN, "attempting to delete file: " + file.getAbsolutePath());
}
}
public static String getMd5Hash(File file) {
try {
// CTS (6/15/2010) : stream file through digest instead of handing it the byte[]
MessageDigest md = MessageDigest.getInstance("MD5");
int chunkSize = 256;
byte[] chunk = new byte[chunkSize];
// Get the size of the file
long lLength = file.length();
if (lLength > Integer.MAX_VALUE) {
Log.e(LOG_TOKEN, "File " + file.getName() + "is too large");
return null;
}
int length = (int) lLength;
InputStream is = null;
is = new FileInputStream(file);
int l = 0;
for (l = 0; l + chunkSize < length; l += chunkSize) {
is.read(chunk, 0, chunkSize);
md.update(chunk, 0, chunkSize);
}
int remaining = length - l;
if (remaining > 0) {
is.read(chunk, 0, remaining);
md.update(chunk, 0, remaining);
}
byte[] messageDigest = md.digest();
BigInteger number = new BigInteger(1, messageDigest);
String md5 = number.toString(16);
while (md5.length() < 32)
md5 = "0" + md5;
is.close();
return md5;
} catch (NoSuchAlgorithmException e) {
Log.e("MD5", e.getMessage());
return null;
} catch (FileNotFoundException e) {
Log.e("No Cache File", e.getMessage());
return null;
} catch (IOException e) {
Log.e("Problem reading from file", e.getMessage());
return null;
}
}
private static final String illegalChars = "'*','+'~|<> !?:./\\";
public static String SanitizeFileName(String input) {
for(char c : illegalChars.toCharArray()) {
input = input.replace(c, '_');
}
return input;
}
public static void copyFile(File oldPath, File newPath) throws IOException {
copyFile(oldPath, newPath, null, null);
}
public static void copyFile(File oldPath, File newPath, Cipher oldRead, Cipher newWrite) throws IOException {
if(!newPath.createNewFile()) { throw new IOException("Couldn't create new file @ " + newPath.toString()); }
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(oldPath);
if(oldRead != null) {
is = new CipherInputStream(is, oldRead);
}
os = new FileOutputStream(newPath);
if(newWrite != null) {
os = new CipherOutputStream(os, newWrite);
}
AndroidStreamUtil.writeFromInputToOutput(is, os);
} finally {
try{
if(is != null) {
is.close();
}
} catch(IOException e) {
}
try{
if(os != null) {
os.close();
}
} catch(IOException e) {
}
}
}
/**
* Get a new, clean location to put a file in the same path as the incoming file
*
* @param f The existing file
* @param slug A new chunk to append to the file name
* @param removeExisting Whether to remove any files which already appear in this location.
* If false, the method will continue trying to generate new paths until there is no conflict
*
* @return A new file location which does not reference an existing file.
*/
public static File getNewFileLocation(File f, String slug, boolean removeExisting) {
if(slug == null) {
slug = PropertyUtils.genGUID(5);
}
String name = f.getName();
int lastDot = name.lastIndexOf(".");
if(lastDot != -1) {
String prefix = name.substring(0, lastDot);
String postfix = name.substring(lastDot);
name = prefix + "_" + slug + postfix;
} else {
name = name + "_" + slug;
}
File newLocation = new File(f.getParent() + File.separator + name);
if(newLocation.exists()) {
if(removeExisting) {
deleteFile(newLocation);
} else {
return getNewFileLocation(newLocation, null, removeExisting);
}
}
return newLocation;
}
public static void copyFileDeep(File oldFolder, File newFolder) throws IOException {
//Create the new folder
newFolder.mkdir();
if(oldFolder.listFiles() != null) {
//Start copying over files
for(File oldFile : oldFolder.listFiles()) {
File newFile = new File(newFolder.getPath() + File.separator + oldFile.getName());
if(oldFile.isDirectory()) {
copyFileDeep(oldFile, newFile);
} else {
FileUtil.copyFile(oldFile, newFile);
}
}
}
}
/**
* http://stackoverflow.com/questions/11281010/how-can-i-get-external-sd-card-path-for-android-4-0
*
* Used in SD Card functionality to get the location of the SD card for reads and writes
* Returns a list of available mounts; for our purposes, we just use the first
*/
public static ArrayList<String> getExternalMounts() {
final ArrayList<String> out = new ArrayList<String>();
String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
String s = "";
try {
final Process process = new ProcessBuilder().command("mount")
.redirectErrorStream(true).start();
process.waitFor();
final InputStream is = process.getInputStream();
final byte[] buffer = new byte[1024];
while (is.read(buffer) != -1) {
s = s + new String(buffer);
}
is.close();
} catch (final Exception e) {
e.printStackTrace();
}
// parse output
final String[] lines = s.split("\n");
for (String line : lines) {
if (!line.toLowerCase(Locale.US).contains("asec")) {
if (line.matches(reg)) {
String[] parts = line.split(" ");
for (String part : parts) {
if (part.startsWith("/"))
if (!part.toLowerCase(Locale.US).contains("vold"))
out.add(part);
}
}
}
}
return out;
}
/**
* Turn a filepath into a global android URI that can be passed
* to an intent.
*
* @param fileLocation
* @return
*/
public static String getGlobalStringUri(String fileLocation) {
return "file://" + fileLocation;
}
public static void checkReferenceURI(Resource r, String URI, Vector<MissingMediaException> problems) throws IOException{
try{
Reference mRef = ReferenceManager._().DeriveReference(URI);
if(!mRef.doesBinaryExist()){
String mLocalReference = mRef.getLocalURI();
problems.addElement(new MissingMediaException(r,"Missing external media: " + mLocalReference, mLocalReference));
}
} catch(InvalidReferenceException ire){
//do nothing for now
}
}
/**
* Ensure that everything between "localpart" and f exists
* and create it if not.
*
* @param f
*/
public static void ensureFilePathExists(File f) {
File folder = f.getParentFile();
if(folder != null) {
//Don't worry about return value
folder.mkdirs();
}
}
/*
* if we are on KitKat we need use the new API to find the mounted roots, then append our application
* specific path that we're allowed to write to
*/
@SuppressLint("NewApi")
private static String getExternalDirectoryKitKat(Context c){
File[] extMounts = c.getExternalFilesDirs(null);
// first entry is emualted storage. Second if it exists is secondary (real) SD.
if(extMounts.length <2){
return null;
}
/*
* First volume returned by getExternalFilesDirs is always "primary" volume,
* or emulated. Further entries, if they exist, will be "secondary" or external SD
*
* http://www.doubleencore.com/2014/03/android-external-storage/
*
*/
File sdRoot = extMounts[1];
// because apparently getExternalFilesDirs entries can be null
if(sdRoot == null){
return null;
}
String domainedFolder = sdRoot.getAbsolutePath() + "/Android/data/org.commcare.dalvik";
return domainedFolder;
}
/*
* If we're on KitKat use the new OS path
*/
public static String getDumpDirectory(Context c){
if (android.os.Build.VERSION.SDK_INT>=19){
return getExternalDirectoryKitKat(c);
} else{
ArrayList<String> mArrayList = getExternalMounts();
if (mArrayList.size() > 0){
return getExternalMounts().get(0);
}
return null;
}
}
}