From f21d1dad39c5c54f993eba3937ef8e91f41fac5b Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Mon, 14 Jan 2019 13:56:10 -0800 Subject: [PATCH 1/3] log library version in diagnostics --- src/com/amplitude/api/Diagnostics.java | 1 + test/com/amplitude/api/DiagnosticsTest.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/com/amplitude/api/Diagnostics.java b/src/com/amplitude/api/Diagnostics.java index 568c016f..afb5da14 100644 --- a/src/com/amplitude/api/Diagnostics.java +++ b/src/com/amplitude/api/Diagnostics.java @@ -115,6 +115,7 @@ public void run() { event.put("timestamp", System.currentTimeMillis()); event.put("device_id", deviceId); event.put("count", 1); + event.put("library", String.format("amplitude-android/%s", Constants.VERSION)); if (exception != null) { String stackTrace = Log.getStackTraceString(exception); diff --git a/test/com/amplitude/api/DiagnosticsTest.java b/test/com/amplitude/api/DiagnosticsTest.java index 9c4b201d..6a38264b 100644 --- a/test/com/amplitude/api/DiagnosticsTest.java +++ b/test/com/amplitude/api/DiagnosticsTest.java @@ -129,14 +129,17 @@ public void testLogError() { assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(0)).optString("error"), "test_error"); assertTrue(logger.unsentErrors.get(logger.unsentErrorStrings.get(0)).optLong("timestamp") >= timestamp); assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(0)).optInt("count"), 1); + assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(0)).optString("library"), "amplitude-android/2.21.0"); assertEquals(logger.unsentErrorStrings.get(1), "test_error1"); assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(1)).optString("error"), "test_error1"); assertTrue(logger.unsentErrors.get(logger.unsentErrorStrings.get(1)).optLong("timestamp") >= timestamp); assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(1)).optInt("count"), 1); + assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(1)).optString("library"), "amplitude-android/2.21.0"); assertEquals(logger.unsentErrorStrings.get(2), "test_error2"); assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(2)).optString("error"), "test_error2"); assertTrue(logger.unsentErrors.get(logger.unsentErrorStrings.get(2)).optLong("timestamp") >= timestamp); assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(2)).optInt("count"), 1); + assertEquals(logger.unsentErrors.get(logger.unsentErrorStrings.get(2)).optString("library"), "amplitude-android/2.21.0"); // test truncation logger.setDiagnosticEventMaxCount(7); From f309d6d20d949b4e52c03248aa618fa670491b4e Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Tue, 15 Jan 2019 16:00:03 -0800 Subject: [PATCH 2/3] add corruption handler --- src/com/amplitude/api/DatabaseHelper.java | 69 ++++++++++++++++++++++- src/com/amplitude/api/Diagnostics.java | 28 ++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/com/amplitude/api/DatabaseHelper.java b/src/com/amplitude/api/DatabaseHelper.java index 96c00cf6..98e8b218 100644 --- a/src/com/amplitude/api/DatabaseHelper.java +++ b/src/com/amplitude/api/DatabaseHelper.java @@ -9,6 +9,7 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; +import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; @@ -21,6 +22,72 @@ class DatabaseHelper extends SQLiteOpenHelper { + private static class DatabaseCorruptionHandler implements DatabaseErrorHandler { + + @Override + public void onCorruption(SQLiteDatabase dbObj) { + + Diagnostics.getLogger().logError( + "DB: corruption detected, deleting database files", new Throwable() + ); + + // Android's DefaultDatabaseErrorHandler + logger.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(dbObj.getPath()); + return; + } + else { + List> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = dbObj.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(dbObj.getPath()); + } + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + logger.e(TAG, "deleting the database file: " + fileName); + try { + SQLiteDatabase.deleteDatabase(new File(fileName)); + } catch (Exception e) { + /* print warning and ignore exception */ + logger.w(TAG, "delete failed: " + e.getMessage()); + } + } + } + static final Map instances = new HashMap(); private static final String TAG = "com.amplitude.api.DatabaseHelper"; @@ -79,7 +146,7 @@ protected DatabaseHelper(Context context) { } protected DatabaseHelper(Context context, String instance) { - super(context, getDatabaseName(instance), null, Constants.DATABASE_VERSION); + super(context, getDatabaseName(instance), null, Constants.DATABASE_VERSION, new DatabaseCorruptionHandler()); file = context.getDatabasePath(getDatabaseName(instance)); instanceName = Utils.normalizeInstanceName(instance); } diff --git a/src/com/amplitude/api/Diagnostics.java b/src/com/amplitude/api/Diagnostics.java index afb5da14..c9cb0e95 100644 --- a/src/com/amplitude/api/Diagnostics.java +++ b/src/com/amplitude/api/Diagnostics.java @@ -1,5 +1,7 @@ package com.amplitude.api; +import android.os.Environment; +import android.os.StatFs; import android.util.Log; import org.json.JSONArray; @@ -111,12 +113,36 @@ public void run() { if (event == null) { event = new JSONObject(); try { + + // add attributes to diagnostic event + event.put("error", AmplitudeClient.truncate(error)); event.put("timestamp", System.currentTimeMillis()); event.put("device_id", deviceId); event.put("count", 1); event.put("library", String.format("amplitude-android/%s", Constants.VERSION)); + // get memory stats + Runtime runtime = Runtime.getRuntime(); + long usedMemInMB = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L; + long maxHeapSizeInMB = runtime.maxMemory() / 1048576L; + long availHeapSizeInMB = maxHeapSizeInMB - usedMemInMB; + event.put("used_mem_mb", usedMemInMB); + event.put("max_heap_mb", maxHeapSizeInMB); + event.put("heap_avail_mb", availHeapSizeInMB); + + // get disk stats + StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath()); + long bytesAvailable; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { + bytesAvailable = stat.getBlockSizeLong() * stat.getAvailableBlocksLong(); + } + else { + bytesAvailable = (long)stat.getBlockSize() * (long)stat.getAvailableBlocks(); + } + long megAvailable = bytesAvailable / (1024 * 1024); + event.put("disk_avail_mb", megAvailable); + if (exception != null) { String stackTrace = Log.getStackTraceString(exception); if (!Utils.isEmptyString(stackTrace)) { @@ -124,7 +150,7 @@ public void run() { } } - // unsent queues are full, make room by removing + // add unsent errors to queue. if full, make room by removing if (unsentErrorStrings.size() >= diagnosticEventMaxCount) { for (int i = 0; i < DIAGNOSTIC_EVENT_MIN_COUNT; i++) { String errorString = unsentErrorStrings.remove(0); From e89dfa59f6c1a674d31860dd8429b3a367d848f5 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Thu, 17 Jan 2019 16:47:53 -0800 Subject: [PATCH 3/3] delegate to default handler --- src/com/amplitude/api/DatabaseHelper.java | 61 ++--------------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/src/com/amplitude/api/DatabaseHelper.java b/src/com/amplitude/api/DatabaseHelper.java index 98e8b218..7ff6410a 100644 --- a/src/com/amplitude/api/DatabaseHelper.java +++ b/src/com/amplitude/api/DatabaseHelper.java @@ -4,12 +4,12 @@ import android.content.Context; import android.database.Cursor; import android.database.DatabaseErrorHandler; +import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; -import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; @@ -24,67 +24,16 @@ class DatabaseHelper extends SQLiteOpenHelper { private static class DatabaseCorruptionHandler implements DatabaseErrorHandler { + DatabaseErrorHandler defaultHandler = new DefaultDatabaseErrorHandler(); + @Override public void onCorruption(SQLiteDatabase dbObj) { Diagnostics.getLogger().logError( - "DB: corruption detected, deleting database files", new Throwable() + "DB: corruption detected, deleting database files", new Throwable() ); - // Android's DefaultDatabaseErrorHandler - logger.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); - // is the corruption detected even before database could be 'opened'? - if (!dbObj.isOpen()) { - // database files are not even openable. delete this database file. - // NOTE if the database has attached databases, then any of them could be corrupt. - // and not deleting all of them could cause corrupted database file to remain and - // make the application crash on database open operation. To avoid this problem, - // the application should provide its own {@link DatabaseErrorHandler} impl class - // to delete ALL files of the database (including the attached databases). - deleteDatabaseFile(dbObj.getPath()); - return; - } - else { - List> attachedDbs = null; - try { - // Close the database, which will cause subsequent operations to fail. - // before that, get the attached database list first. - try { - attachedDbs = dbObj.getAttachedDbs(); - } catch (SQLiteException e) { - /* ignore */ - } - try { - dbObj.close(); - } catch (SQLiteException e) { - /* ignore */ - } - } finally { - // Delete all files of this corrupt database and/or attached databases - if (attachedDbs != null) { - for (Pair p : attachedDbs) { - deleteDatabaseFile(p.second); - } - } else { - // attachedDbs = null is possible when the database is so corrupt that even - // "PRAGMA database_list;" also fails. delete the main database file - deleteDatabaseFile(dbObj.getPath()); - } - } - } - } - - private void deleteDatabaseFile(String fileName) { - if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { - return; - } - logger.e(TAG, "deleting the database file: " + fileName); - try { - SQLiteDatabase.deleteDatabase(new File(fileName)); - } catch (Exception e) { - /* print warning and ignore exception */ - logger.w(TAG, "delete failed: " + e.getMessage()); - } + defaultHandler.onCorruption(dbObj); } }