package com.eleybourn.bookcatalogue.utils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.widget.Toast; import com.eleybourn.bookcatalogue.BookCatalogueApp; import com.eleybourn.bookcatalogue.CatalogueDBAdapter; import com.eleybourn.bookcatalogue.R; import com.eleybourn.bookcatalogue.debug.Tracker; import com.eleybourn.bookcatalogue.scanner.ScannerManager; import com.eleybourn.bookcatalogue.scanner.ZxingScanner; import com.eleybourn.bookcatalogue.scanner.pic2shop.Scan; /** * Class to wrap common storage related funcions. * * @author Philip Warner */ public class StorageUtils { private static String UTF8 = "utf8"; private static int BUFFER_SIZE = 8192; private static final String LOCATION = "bookCatalogue"; private static final String DATABASE_NAME = "book_catalogue"; private static final String EXTERNAL_FILE_PATH = Environment.getExternalStorageDirectory() + "/" + LOCATION; private static final String ERRORLOG_FILE = EXTERNAL_FILE_PATH + "/error.log"; /** * Accessor * * @return */ public static String getErrorLog() { return ERRORLOG_FILE; } /** * Accessor * * @return */ public static String getDatabaseName() { return DATABASE_NAME; } /** * Accessor * * @return */ public static final File getSharedStorage() { File dir = new File(StorageUtils.EXTERNAL_FILE_PATH); dir.mkdir(); return dir; } /** * Accessor * * @return */ public static final String getSharedStoragePath() { File dir = new File(StorageUtils.EXTERNAL_FILE_PATH); dir.mkdir(); return dir.getAbsolutePath(); } /** * Backup database file * @throws Exception */ public static void backupDbFile(SQLiteDatabase db, String suffix) { try { final String fileName = LOCATION + suffix; java.io.InputStream dbOrig = new java.io.FileInputStream(db.getPath()); File dir = getSharedStorage(); // Path to the external backup String fullFilename = dir.getPath() + "/" + fileName; //check if it exists File existing = new File(fullFilename); if (existing.exists()) { String backupFilename = dir.getPath() + "/" + fileName + ".bak"; File backup = new File(backupFilename); existing.renameTo(backup); } java.io.OutputStream dbCopy = new java.io.FileOutputStream(fullFilename); byte[] buffer = new byte[1024]; int length; while ((length = dbOrig.read(buffer))>0) { dbCopy.write(buffer, 0, length); } dbCopy.flush(); dbCopy.close(); dbOrig.close(); } catch (Exception e) { Logger.logError(e); } } /** * Make sure the external shared directory exists */ public static void initSharedDirectory() { new File(StorageUtils.EXTERNAL_FILE_PATH + "/").mkdirs(); try { new File(StorageUtils.EXTERNAL_FILE_PATH + "/.nomedia").createNewFile(); } catch (IOException e) { Logger.logError(e); } } /** * Compare two files based on date. Used for sorting file list by date. * * @author Philip Warner */ public static class FileDateComparator implements Comparator<File> { /** Ascending is >= 0, Descenting is < 0. */ private int mDirection; /** * Constructor */ public FileDateComparator(int direction) { mDirection = direction < 0? -1 : 1; } /** * Compare based on modified date */ @Override public int compare(File lhs, File rhs) { final long l = lhs.lastModified(); final long r = rhs.lastModified(); if (l < r) return -mDirection; else if (l > r) return mDirection; else return 0; } } /** * Scan all mount points for '/bookCatalogue' directory and collect a list * of all CSV files. * * @return */ public static ArrayList<File> findExportFiles() { //StringBuilder info = new StringBuilder(); ArrayList<File> files = new ArrayList<File>(); Pattern mountPointPat = Pattern.compile("^\\s*[^\\s]+\\s+([^\\s]+)"); BufferedReader in = null; // Make a filter for files ending in .csv FilenameFilter csvFilter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { final String fl = filename.toLowerCase(); return (fl.endsWith(".csv")); //ENHANCE: Allow for other files? Backups? || fl.endsWith(".csv.bak")); } }; ArrayList<File> dirs = new ArrayList<File>(); //info.append("Getting mounted file systems\n"); // Scan all mounted file systems try { in = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/mounts")),1024); String line = ""; while ((line = in.readLine()) != null) { //info.append(" checking " + line + "\n"); Matcher m = mountPointPat.matcher(line); // Get the mount point if (m.find()) { // See if it has a bookCatalogue directory File dir = new File(m.group(1).toString() + "/bookCatalogue"); //info.append(" matched " + dir.getAbsolutePath() + "\n"); dirs.add(dir); } else { //info.append(" NO match\n"); } } } catch (IOException e) { Logger.logError(e, "Failed to open/scan/read /proc/mounts"); } finally { if (in != null) try { in.close(); } catch (Exception e) {}; } // Sometimes (Android 6?) the /proc/mount search seems to fail, so we revert to environment vars //info.append("Found " + dirs.size() + " directories\n"); try { String loc1 = System.getenv("EXTERNAL_STORAGE"); if (loc1 != null) { File dir = new File(loc1 + "/bookCatalogue"); dirs.add(dir); //info.append("Loc1 added " + dir.getAbsolutePath() + "\n"); } else { //info.append("Loc1 ignored: " + loc1 + "\n"); } String loc2 = System.getenv("SECONDARY_STORAGE"); if (loc2 != null && !loc2.equals(loc1)) { File dir = new File(loc2 + "/bookCatalogue"); dirs.add(dir); //info.append("Loc2 added " + dir.getAbsolutePath() + "\n"); } else { //info.append("Loc2 ignored: " + loc2 + "\n"); } } catch (Exception e) { Logger.logError(e, "Failed to get external storage from environment variables"); } HashSet<String> paths = new HashSet<String>(); //info.append("Looking for files in directories\n"); for(File dir: dirs) { try { if (dir.exists()) { // Scan for csv files File[] csvFiles = dir.listFiles(csvFilter); if (csvFiles != null) { //info.append(" found " + csvFiles.length + " in " + dir.getAbsolutePath() + "\n"); for (File f : csvFiles) { System.out.println("Found: " + f.getAbsolutePath()); final String cp = f.getCanonicalPath(); if (paths.contains(cp)) { //info.append(" already present as " + cp + "\n"); } else { files.add(f); paths.add(cp); //info.append(" added as " + cp + "\n"); } } } else { //info.append(" null returned by listFiles() in " + dir.getAbsolutePath() + "\n"); } } else { //info.append(" " + dir.getAbsolutePath() + " does not exist\n"); } } catch (Exception e) { Logger.logError(e, "Failed to read directory " + dir.getAbsolutePath()); } } //Logger.logError(new RuntimeException("INFO"), info.toString()); // Sort descending based on modified date Collections.sort(files, new FileDateComparator(-1)); return files; } /** * Check if the sdcard is writable * * @return success or failure */ static public boolean sdCardWritable() { /* Test write to the SDCard */ try { BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(EXTERNAL_FILE_PATH + "/.nomedia"), UTF8), BUFFER_SIZE); out.write(""); out.close(); return true; } catch (IOException e) { return false; } } private static String[] mPurgeableFilePrefixes = new String[]{StorageUtils.LOCATION + "DbUpgrade", StorageUtils.LOCATION + "DbExport", "error.log", "tmp"}; private static String[] mDebugFilePrefixes = new String[]{StorageUtils.LOCATION + "DbUpgrade", StorageUtils.LOCATION + "DbExport", "error.log", "export.csv"}; /** * Collect and send com.eleybourn.bookcatalogue.debug info to a support email address. * * THIS SHOULD NOT BE A PUBLICALLY AVAILABLE MAINING LIST OR FORUM! * * @param context * @param dbHelper */ public static void sendDebugInfo(Context context, CatalogueDBAdapter dbHelper) { // Create a temp DB copy. String tmpName = StorageUtils.LOCATION + "DbExport-tmp.db"; dbHelper.backupDbFile(tmpName); File dbFile = new File(StorageUtils.EXTERNAL_FILE_PATH + "/" + tmpName); dbFile.deleteOnExit(); // setup the mail message final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE); emailIntent.setType("plain/text"); emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, context.getString(R.string.debug_email).split(";")); String subject = "[" + context.getString(R.string.app_name) + "] " + context.getString(R.string.debug_subject); emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject); String message = ""; try { // Get app info PackageManager manager = context.getPackageManager(); PackageInfo appInfo = manager.getPackageInfo( context.getPackageName(), 0); message += "App: " + appInfo.packageName + "\n"; message += "Version: " + appInfo.versionName + " (" + appInfo.versionCode + ")\n"; } catch (Exception e1) { // Not much we can do inside error logger... } message += "SDK: " + Build.VERSION.SDK + " (" + Build.VERSION.SDK_INT + " " + Build.TAGS + ")\n"; message += "Phone Model: " + Build.MODEL + "\n"; message += "Phone Manufacturer: " + Build.MANUFACTURER + "\n"; message += "Phone Device: " + Build.DEVICE + "\n"; message += "Phone Product: " + Build.PRODUCT + "\n"; message += "Phone Brand: " + Build.BRAND + "\n"; message += "Phone ID: " + Build.ID + "\n"; message += "Signed-By: " + Utils.signedBy(context) + "\n"; message += "\nHistory:\n" + Tracker.getEventsInfo() + "\n"; // Scanners installed try { message += "Pref. Scanner: " + BookCatalogueApp.getAppPreferences().getInt( ScannerManager.PREF_PREFERRED_SCANNER, -1) + "\n"; String[] scanners = new String[] { ZxingScanner.ACTION, Scan.ACTION, Scan.Pro.ACTION}; for(String scanner: scanners) { message += "Scanner [" + scanner + "]:\n"; final Intent mainIntent = new Intent(scanner, null); final List<ResolveInfo> resolved = context.getPackageManager().queryIntentActivities( mainIntent, 0); if (resolved.size() > 0) { for(ResolveInfo r: resolved) { message += " "; // Could be activity or service... if (r.activityInfo != null) { message += r.activityInfo.packageName; } else if (r.serviceInfo != null) { message += r.serviceInfo.packageName; } else { message += "UNKNOWN"; } message += " (priority " + r.priority + ", preference " + r.preferredOrder + ", match " + r.match + ", default=" + r.isDefault + ")\n"; } } else { message += " No packages found\n"; } } } catch (Exception e) { // Don't lose the other debug info if scanner data dies for some reason message += "Scanner failure: " + e.getMessage() + "\n"; } message += "\n"; message += "Details:\n\n" + context.getString(R.string.debug_body).toUpperCase() + "\n\n"; emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message); //has to be an ArrayList ArrayList<Uri> uris = new ArrayList<Uri>(); //convert from paths to Android friendly Parcelable Uri's ArrayList<String> files = new ArrayList<String>(); // Find all files of interest to send File dir = new File(StorageUtils.EXTERNAL_FILE_PATH); try { for (String name : dir.list()) { boolean send = false; for(String prefix : mDebugFilePrefixes) if (name.startsWith(prefix)) { send = true; break; } if (send) files.add(name); } // Build the attachment list for (String file : files) { File fileIn = new File(StorageUtils.EXTERNAL_FILE_PATH + "/" + file); if (fileIn.exists() && fileIn.length() > 0) { Uri u = Uri.fromFile(fileIn); uris.add(u); } } // We used to only send it if there are any files to send, but later versions added // useful debugging info. So now we always send. emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); context.startActivity(Intent.createChooser(emailIntent, "Send mail...")); } catch (NullPointerException e) { Logger.logError(e); Toast.makeText(context, R.string.export_failed_sdcard, Toast.LENGTH_LONG).show(); } } /** * Cleanup any purgeable files. */ public static void cleanupFiles() { if (StorageUtils.sdCardWritable()) { File dir = new File(StorageUtils.EXTERNAL_FILE_PATH); for (String name : dir.list()) { boolean purge = false; for(String prefix : mPurgeableFilePrefixes) if (name.startsWith(prefix)) { purge = true; break; } if (purge) try { File file = new File(StorageUtils.EXTERNAL_FILE_PATH + "/" + name); file.delete(); } catch (Exception e) { } } } } /** * Get the total size of purgeable files. * @return size, in bytes */ public static long cleanupFilesTotalSize() { if (!StorageUtils.sdCardWritable()) return 0; long totalSize = 0; File dir = new File(StorageUtils.EXTERNAL_FILE_PATH); for (String name : dir.list()) { boolean purge = false; for(String prefix : mPurgeableFilePrefixes) if (name.startsWith(prefix)) { purge = true; break; } if (purge) try { File file = new File(StorageUtils.EXTERNAL_FILE_PATH + "/" + name); totalSize += file.length(); } catch (Exception e) { } } return totalSize; } }