/** * */ package info.guardianproject.otr.app.im.app; import info.guardianproject.iocipher.File; import info.guardianproject.iocipher.FileInputStream; import info.guardianproject.iocipher.FileOutputStream; import info.guardianproject.iocipher.VirtualFileSystem; import info.guardianproject.otr.app.im.R; import info.guardianproject.util.LogCleaner; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.IOUtils; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Environment; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; /** * Copyright (C) 2014 Guardian Project. All rights reserved. * * @author liorsaar * */ public class ChatFileStore { public static final String TAG = ChatFileStore.class.getName(); private static String dbFilePath; private static final String BLOB_NAME = "media.db"; public static void unmount() { VirtualFileSystem.get().unmount(); } public static void list(String parent) { File file = new File(parent); String[] list = file.list(); // Log.e(TAG, file.getAbsolutePath()); for (int i = 0 ; i < list.length ; i++) { String fullname = parent + list[i]; File child = new File(fullname); if (child.isDirectory()) { list(fullname+"/"); } else { File full = new File(fullname); // Log.e(TAG, fullname + " " + full.length()); } } } public static void deleteSession( String sessionId ) throws IOException { String dirName = "/" + sessionId; File file = new File(dirName); // if the session doesnt have any ul/dl files - bail if (!file.exists()) { return; } // delete recursive delete( dirName ); } private static void delete(String parentName) throws IOException { File parent = new File(parentName); // if a file or an empty directory - delete it if (!parent.isDirectory() || parent.list().length == 0 ) { // Log.e(TAG, "delete:" + parent ); if (!parent.delete()) { throw new IOException("Error deleting " + parent); } return; } // directory - recurse String[] list = parent.list(); for (int i = 0 ; i < list.length ; i++) { String childName = parentName + "/" + list[i]; delete( childName ); } delete( parentName ); } private static final String VFS_SCHEME = "vfs"; public static Uri vfsUri(String filename) { return Uri.parse(VFS_SCHEME + ":" + filename); } public static boolean isVfsUri(Uri uri) { return TextUtils.equals(VFS_SCHEME, uri.getScheme()); } public static boolean isVfsUri(String uriString) { if (TextUtils.isEmpty(uriString)) return false; else return uriString.startsWith(VFS_SCHEME + ":/"); } public static Bitmap getThumbnailVfs(Uri uri, int thumbnailSize) { if (!VirtualFileSystem.get().isMounted()) return null; File image = new File(uri.getPath()); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inInputShareable = true; options.inPurgeable = true; try { FileInputStream fis = new FileInputStream(new File(image.getPath())); BitmapFactory.decodeStream(fis, null, options); } catch (Exception e) { Log.e(ImApp.LOG_TAG,"unable to read vfs thumbnail",e); return null; } if ((options.outWidth == -1) || (options.outHeight == -1)) return null; int originalSize = (options.outHeight > options.outWidth) ? options.outHeight : options.outWidth; BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = originalSize / thumbnailSize; try { FileInputStream fis = new FileInputStream(new File(image.getPath())); Bitmap scaledBitmap = BitmapFactory.decodeStream(fis, null, opts); return scaledBitmap; } catch (FileNotFoundException e) { LogCleaner.error(ImApp.LOG_TAG, "can't find IOcipher file: " + image.getPath(), e); return null; } catch (OutOfMemoryError oe) { LogCleaner.error(ImApp.LOG_TAG, "out of memory loading thumbnail: " + image.getPath(), oe); return null; } } /** * Setup IOCipher VirtualFileSystem without a user-provided password. * @param context */ public static void initWithoutPassword(Context context) { init(context, null); } /** * Careful! All of the {@code File}s in this method are {@link java.io.File} * not {@link info.guardianproject.iocipher.File}s * * @param context * @param key * @throws IllegalArgumentException */ public static void init(Context context, byte[] key) throws IllegalArgumentException { // there is only one VFS, so if its already mounted, nothing to do VirtualFileSystem vfs = VirtualFileSystem.get(); if (vfs.isMounted()) { Log.w(TAG, "VFS " + vfs.getContainerPath() + " is already mounted, skipping init()"); return; } /* TODO None of these key/keyText transformations are necessary since IOCipher/SQLCipher * will handle long strings and blank strings, but changing it might break existing * DBs. These transformations where gathered from a couple places to be centralized here. */ String keyText; if (key == null) keyText = ""; else keyText = new String(Hex.encodeHex(key)); if (keyText.length() > 32) keyText = keyText.substring(0, 32); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); if (settings.getBoolean( context.getString(R.string.key_store_media_on_external_storage_pref), false)) dbFilePath = getExternalDbFilePath(context); else dbFilePath = getInternalDbFilePath(context); if (!new java.io.File(dbFilePath).exists()) { vfs.createNewContainer(dbFilePath, keyText); } vfs.mount(dbFilePath, keyText); } /** * get the external storage path for the chat media file storage file. */ public static String getExternalDbFilePath(Context c) { java.io.File externalFilesDir = c.getExternalFilesDir(null); if (externalFilesDir == null) return null; else return externalFilesDir.getAbsolutePath() + "/" + BLOB_NAME; } /** * get the internal storage path for the chat media file storage file. */ public static String getInternalDbFilePath(Context c) { return c.getFilesDir() + "/" + BLOB_NAME; } /** * Copy device content into vfs. * All imported content is stored under /SESSION_NAME/ * The original full path is retained to facilitate browsing * The session content can be deleted when the session is over * @param sourcePath * @return vfs uri * @throws IOException */ public static Uri importContent(String sessionId, String sourcePath) throws IOException { list("/"); File sourceFile = new File(sourcePath); String targetPath = "/" + sessionId + "/upload/" + sourceFile.getName(); targetPath = createUniqueFilename(targetPath); copyToVfs( sourcePath, targetPath ); list("/"); return vfsUri(targetPath); } /** * Copy device content into vfs. * All imported content is stored under /SESSION_NAME/ * The original full path is retained to facilitate browsing * The session content can be deleted when the session is over * @param sourcePath * @return vfs uri * @throws IOException */ public static Uri importContent(String sessionId, String fileName, InputStream sourceStream) throws IOException { list("/"); String targetPath = "/" + sessionId + "/upload/" + fileName; targetPath = createUniqueFilename(targetPath); copyToVfs( sourceStream, targetPath ); list("/"); return vfsUri(targetPath); } /** * Resize an image to an efficient size for sending via OTRDATA, then copy * that resized version into vfs. All imported content is stored under * /SESSION_NAME/ The original full path is retained to facilitate browsing * The session content can be deleted when the session is over * * @param imagePath * @return vfs uri * @throws IOException */ public static Uri resizeAndImportImage(Context context, String sessionId, Uri uri, String mimeType) throws IOException { String imagePath = uri.getPath(); String targetPath = "/" + sessionId + "/upload/" + imagePath; targetPath = createUniqueFilename(targetPath); int defaultImageWidth = 600; //load lower-res bitmap Bitmap bmp = getThumbnailFile(context, uri, defaultImageWidth); File file = new File(targetPath); FileOutputStream out = new FileOutputStream(file); if (imagePath.endsWith(".png") || mimeType.contains("png")) //preserve alpha channel bmp.compress(Bitmap.CompressFormat.PNG, 100, out); else bmp.compress(Bitmap.CompressFormat.JPEG, 90, out); out.flush(); out.close(); bmp.recycle(); return vfsUri(targetPath); } public static Bitmap getThumbnailFile(Context context, Uri uri, int thumbnailSize) throws IOException { InputStream is = context.getContentResolver().openInputStream(uri); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inInputShareable = true; options.inPurgeable = true; BitmapFactory.decodeStream(is, null, options); if ((options.outWidth == -1) || (options.outHeight == -1)) return null; int originalSize = (options.outHeight > options.outWidth) ? options.outHeight : options.outWidth; is.close(); is = context.getContentResolver().openInputStream(uri); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = originalSize / thumbnailSize; Bitmap scaledBitmap = BitmapFactory.decodeStream(is, null, opts); is.close(); return scaledBitmap; } public static void exportAll(String sessionId ) throws IOException { } public static void exportContent(String mimeType, Uri mediaUri, java.io.File exportPath) throws IOException { String sourcePath = mediaUri.getPath(); copyToExternal( sourcePath, exportPath); } public static java.io.File exportPath(String mimeType, Uri mediaUri) { java.io.File targetFilename; if (mimeType.startsWith("image")) { targetFilename = new java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),mediaUri.getLastPathSegment()); } else if (mimeType.startsWith("audio")) { targetFilename = new java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),mediaUri.getLastPathSegment()); } else { targetFilename = new java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),mediaUri.getLastPathSegment()); } java.io.File targetUniqueFilename = createUniqueFilenameExternal(targetFilename); return targetFilename; } public static void copyToVfs(String sourcePath, String targetPath) throws IOException { // create the target directories tree mkdirs( targetPath ); // copy java.io.FileInputStream fis = new java.io.FileInputStream(new java.io.File(sourcePath)); FileOutputStream fos = new FileOutputStream(new File(targetPath), false); IOUtils.copyLarge(fis, fos); fos.close(); fis.close(); } public static void copyToVfs(InputStream sourceIS, String targetPath) throws IOException { // create the target directories tree mkdirs( targetPath ); // copy FileOutputStream fos = new FileOutputStream(new File(targetPath), false); IOUtils.copyLarge(sourceIS, fos); fos.close(); sourceIS.close(); } /** * Write a {@link byte[]} into an IOCipher File * @param sessionId * @param buf * @return * @throws IOException */ public static void copyToVfs(byte buf[], String targetPath) throws IOException { File file = new File(targetPath); FileOutputStream out = new FileOutputStream(file); out.write(buf); out.close(); } public static void copyToExternal(String sourcePath, java.io.File targetPath) throws IOException { // copy FileInputStream fis = new FileInputStream(new File(sourcePath)); java.io.FileOutputStream fos = new java.io.FileOutputStream(targetPath, false); IOUtils.copyLarge(fis, fos); fos.close(); fis.close(); } private static void mkdirs(String targetPath) throws IOException { File targetFile = new File(targetPath); if (!targetFile.exists()) { File dirFile = targetFile.getParentFile(); if (!dirFile.exists()) { boolean created = dirFile.mkdirs(); if (!created) { throw new IOException("Error creating " + targetPath); } } } } public static boolean exists(String path) { return new File(path).exists(); } public static boolean sessionExists(String sessionId) { return exists( "/" + sessionId ); } private static String createUniqueFilename( String filename ) { if (!exists(filename)) { return filename; } int count = 1; String uniqueName; File file; do { uniqueName = formatUnique(filename, count++); file = new File(uniqueName); } while(file.exists()); return uniqueName; } private static String formatUnique(String filename, int counter) { int lastDot = filename.lastIndexOf("."); if (lastDot != -1) { String name = filename.substring(0,lastDot); String ext = filename.substring(lastDot); return name + "-" + counter + "." + ext; } else { return filename + counter; } } public static String getDownloadFilename(String sessionId, String filenameFromUrl) { String filename = "/" + sessionId + "/download/" + filenameFromUrl; String uniqueFilename = createUniqueFilename(filename); return uniqueFilename; } private static java.io.File createUniqueFilenameExternal(java.io.File filename ) { if (!filename.exists()) { return filename; } int count = 1; String uniqueName; java.io.File file; do { uniqueName = formatUnique(filename.getName(), count++); file = new java.io.File(filename.getParentFile(),uniqueName); } while(file.exists()); return file; } }