diff --git a/test-app/README.md b/test-app/README.md index e39f8d42..33897f22 100644 --- a/test-app/README.md +++ b/test-app/README.md @@ -29,6 +29,15 @@ Tier 3 configures `EventPersistence.IMMEDIATE`. With that setting, events record **Eval+Kill now** should be recovered and delivered after the next launch even though no process code ran on exit. +## Event storage + +The text under the persistence picker shows what the SDK has on disk for this app and mobile key: +`pending-` logs that processes append to, and `ready-` batches waiting for delivery, with the number +of events each holds. It reads names, sizes and modification times only, so looking never changes +what is there. It reads them again every second while the app is in the foreground, so a file that +comes and goes within a second, such as a `new-` log being opened, is never shown. A long press +copies the directory's path. + ## Over-refresh scenario Create a boolean flag named `trackevents-test` with event tracking enabled. Tap diff --git a/test-app/src/main/java/com/launchdarkly/sdk/android/EventStorageLocation.java b/test-app/src/main/java/com/launchdarkly/sdk/android/EventStorageLocation.java new file mode 100644 index 00000000..bf50f819 --- /dev/null +++ b/test-app/src/main/java/com/launchdarkly/sdk/android/EventStorageLocation.java @@ -0,0 +1,19 @@ +package com.launchdarkly.sdk.android; + +import java.io.File; + +/** + * Where the SDK keeps events for one mobile key, for the test app's event storage view. + *
+ * In the SDK's package because {@link EventDirectory} is package-private there, so the view asks the SDK + * for the path rather than working out the hash a second time. The app and the library share one class + * loader, so that access holds at run time. + */ +public final class EventStorageLocation { + private EventStorageLocation() { + } + + public static File directory(File noBackupFilesDir, String mobileKey) { + return EventDirectory.defaultDirectory(noBackupFilesDir, mobileKey); + } +} diff --git a/test-app/src/main/java/com/launchdarkly/sdk/testapp/EventStorageInspector.java b/test-app/src/main/java/com/launchdarkly/sdk/testapp/EventStorageInspector.java new file mode 100644 index 00000000..28492d2a --- /dev/null +++ b/test-app/src/main/java/com/launchdarkly/sdk/testapp/EventStorageInspector.java @@ -0,0 +1,156 @@ +package com.launchdarkly.sdk.testapp; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.format.Formatter; +import android.widget.TextView; + +import com.launchdarkly.sdk.android.EventStorageLocation; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * What the SDK has left in this app's event directory: the logs live processes append to and the batches + * waiting to be delivered, shown in a text view and read again every second while it is watched. + *
+ * Read from the file names, sizes and modification times alone, and never opened, so looking cannot change + * what it looks at: a lock taken to see whether a log is held would stop another process's recovery from + * claiming it. + *
+ * Anything that comes and goes between two reads is never shown, such as a {@code new-} log, which is
+ * renamed as soon as it has its header, or a batch delivered within a second of being closed.
+ */
+final class EventStorageInspector {
+ private static final long INTERVAL_MILLIS = 1_000;
+ /** Enough to see a few processes' logs and a few batches without pushing the rest of the screen off it. */
+ private static final int MAX_ENTRIES_SHOWN = 6;
+
+ private static final String LOG_PREFIX = "pending-";
+ private static final String OPENING_PREFIX = "new-";
+ private static final String BATCH_PREFIX = "ready-";
+ /** The version of how the rest of a batch's name reads; the count follows the payload ID. */
+ private static final String OWN_BATCH_PREFIX = BATCH_PREFIX + "v1-";
+
+ private final Context context;
+ private final TextView view;
+ private final File directory;
+ private final Handler main = new Handler(Looper.getMainLooper());
+ private final Runnable tick = new Runnable() {
+ @Override
+ public void run() {
+ refresh();
+ main.postDelayed(this, INTERVAL_MILLIS);
+ }
+ };
+ private boolean watching;
+
+ EventStorageInspector(Context context, TextView view, String mobileKey) {
+ this.context = context;
+ this.view = view;
+ this.directory = EventStorageLocation.directory(context.getNoBackupFilesDir(), mobileKey);
+ }
+
+ File directory() {
+ return directory;
+ }
+
+ void startWatching() {
+ if (watching) {
+ return;
+ }
+ watching = true;
+ tick.run();
+ }
+
+ void stopWatching() {
+ watching = false;
+ main.removeCallbacks(tick);
+ }
+
+ private void refresh() {
+ File[] files = directory.listFiles(file -> !file.getName().startsWith("."));
+ if (files == null) {
+ view.setText("Event storage: nothing stored yet");
+ return;
+ }
+ Arrays.sort(files, (a, b) -> {
+ int byTime = Long.compare(a.lastModified(), b.lastModified());
+ return byTime != 0 ? byTime : a.getName().compareTo(b.getName());
+ });
+
+ int logs = 0;
+ int batches = 0;
+ int events = 0;
+ List