Adding a utility class for persistant logging.

The logs are kept for at max 48 hours. It uses two log files and switches
between the two based on the day of the year.

Change-Id: I9a99499b3445a62f29f62a5cd13db20b1783bcd3
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d2d1d02..21adcb7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -114,6 +114,7 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.ViewOnDrawExecutor;
@@ -123,11 +124,9 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -307,11 +306,6 @@
     private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>();
     private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false;
 
-    private static final ArrayList<String> sDumpLogs = new ArrayList<String>();
-    private static final Date sDateStamp = new Date();
-    private static final DateFormat sDateFormat =
-            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
-
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
     private SharedPreferences mSharedPrefs;
@@ -3979,7 +3973,7 @@
 
             // Verify that we own the widget
             if (appWidgetInfo == null) {
-                Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
+                FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
                 deleteWidgetInfo(item);
                 return;
             }
@@ -4652,12 +4646,10 @@
             }
         }
 
-        synchronized (sDumpLogs) {
-            writer.println();
-            writer.println(prefix + "Debug logs");
-            for (String log : sDumpLogs) {
-                writer.println(prefix + "  " + log);
-            }
+        try {
+            FileLog.flushAll(writer);
+        } catch (Exception e) {
+            // Ignore
         }
 
         if (mLauncherCallbacks != null) {
@@ -4665,14 +4657,6 @@
         }
     }
 
-    public static void addDumpLog(String tag, String log) {
-        Log.d(tag, log);
-        synchronized(sDumpLogs) {
-            sDateStamp.setTime(System.currentTimeMillis());
-            sDumpLogs.add(sDateFormat.format(sDateStamp) + ": " + tag + ", " + log);
-        }
-    }
-
     public static CustomAppWidget getCustomAppWidget(String name) {
         return sCustomAppWidgets.get(name);
     }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index f84e4b5..0fe6398 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.util.ConfigMonitor;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
 
@@ -79,6 +80,7 @@
         // is the first component to get created. Initializing application context here ensures
         // that LauncherAppState always exists in the main process.
         sContext = provider.getContext().getApplicationContext();
+        FileLog.setDir(sContext.getFilesDir());
     }
 
     private LauncherAppState() {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 884685c..2fd12fd 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -60,6 +60,7 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.CursorIconInfo;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
@@ -1335,7 +1336,7 @@
                 try {
                     screenIds.add(sc.getLong(idIndex));
                 } catch (Exception e) {
-                    addDumpLog("Invalid screen id: " + e);
+                    FileLog.d(TAG, "Invalid screen id", e);
                 }
             }
         } finally {
@@ -1813,7 +1814,7 @@
                                             if (intent == null) {
                                                 // The app is installed but the component is no
                                                 // longer available.
-                                                addDumpLog("Invalid component removed: " + cn);
+                                                FileLog.d(TAG, "Invalid component removed: " + cn);
                                                 itemsToRemove.add(id);
                                                 continue;
                                             } else {
@@ -1824,7 +1825,7 @@
                                         } else if (restored) {
                                             // Package is not yet available but might be
                                             // installed later.
-                                            addDumpLog("package not yet restored: " + cn);
+                                            FileLog.d(TAG, "package not yet restored: " + cn);
 
                                             if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) {
                                                 // Restore has started once.
@@ -1850,12 +1851,12 @@
                                                     itemReplaced = true;
 
                                                 } else if (REMOVE_UNRESTORED_ICONS) {
-                                                    addDumpLog("Unrestored package removed: " + cn);
+                                                    FileLog.d(TAG, "Unrestored package removed: " + cn);
                                                     itemsToRemove.add(id);
                                                     continue;
                                                 }
                                             } else if (REMOVE_UNRESTORED_ICONS) {
-                                                addDumpLog("Unrestored package removed: " + cn);
+                                                FileLog.d(TAG, "Unrestored package removed: " + cn);
                                                 itemsToRemove.add(id);
                                                 continue;
                                             }
@@ -1880,7 +1881,7 @@
                                         } else {
                                             // Do not wait for external media load anymore.
                                             // Log the invalid package, and remove it
-                                            addDumpLog("Invalid package removed: " + cn);
+                                            FileLog.d(TAG, "Invalid package removed: " + cn);
                                             itemsToRemove.add(id);
                                             continue;
                                         }
@@ -1890,7 +1891,7 @@
                                         restored = false;
                                     }
                                 } catch (URISyntaxException e) {
-                                    addDumpLog("Invalid uri: " + intentDescription);
+                                    FileLog.d(TAG, "Invalid uri: " + intentDescription);
                                     itemsToRemove.add(id);
                                     continue;
                                 }
@@ -2073,7 +2074,7 @@
                                 final boolean isProviderReady = isValidProvider(provider);
                                 if (!isSafeMode && !customWidget &&
                                         wasProviderReady && !isProviderReady) {
-                                    addDumpLog("Deleting widget that isn't installed anymore: "
+                                    FileLog.d(TAG, "Deleting widget that isn't installed anymore: "
                                             + provider);
                                     itemsToRemove.add(id);
                                 } else {
@@ -2115,7 +2116,7 @@
                                             appWidgetInfo.restoreStatus |=
                                                     LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
                                         } else if (REMOVE_UNRESTORED_ICONS && !isSafeMode) {
-                                            addDumpLog("Unrestored widget removed: " + component);
+                                            FileLog.d(TAG, "Unrestored widget removed: " + component);
                                             itemsToRemove.add(id);
                                             continue;
                                         }
@@ -2171,9 +2172,7 @@
                         }
                     }
                 } finally {
-                    if (c != null) {
-                        c.close();
-                    }
+                    Utilities.closeSilently(c);
                 }
 
                 // Break early if we've stopped loading
@@ -3541,8 +3540,4 @@
     public static Looper getWorkerLooper() {
         return sWorkerThread.getLooper();
     }
-
-    @Thunk static final void addDumpLog(String log) {
-        Launcher.addDumpLog(TAG, log);
-    }
 }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1acbfc1..871f390 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -63,9 +63,11 @@
 
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.util.IconNormalizer;
 
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -845,6 +847,18 @@
         return true;
     }
 
+    public static void closeSilently(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException e) {
+                if (ProviderConfig.IS_DOGFOOD_BUILD) {
+                    Log.d(TAG, "Error closing", e);
+                }
+            }
+        }
+    }
+
     /**
      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
      * This allows the badging to be done based on the action bitmap size rather than
diff --git a/src/com/android/launcher3/config/ProviderConfig.java b/src/com/android/launcher3/config/ProviderConfig.java
index 825b434..1d964b1 100644
--- a/src/com/android/launcher3/config/ProviderConfig.java
+++ b/src/com/android/launcher3/config/ProviderConfig.java
@@ -20,5 +20,5 @@
 
     public static final String AUTHORITY = "com.android.launcher3.settings".intern();
 
-    public static boolean IS_DOGFOOD_BUILD = false;
+    public static boolean IS_DOGFOOD_BUILD = true;
 }
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
new file mode 100644
index 0000000..f822695
--- /dev/null
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -0,0 +1,211 @@
+package com.android.launcher3.logging;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.ProviderConfig;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around {@link Log} to allow writing to a file.
+ * This class can safely be called from main thread.
+ */
+public final class FileLog {
+
+    private static final String FILE_NAME_PREFIX = "log-";
+    private static final DateFormat DATE_FORMAT =
+            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
+
+    private static final long MAX_LOG_FILE_SIZE = 4 << 20;  // 4 mb
+
+    private static Handler sHandler = null;
+    private static File sLogsDirectory = null;
+
+    public static void setDir(File logsDir) {
+        sLogsDirectory = logsDir;
+    }
+
+    public static void d(String tag, String msg, Exception e) {
+        Log.d(tag, msg, e);
+        print(tag, msg, e);
+    }
+
+    public static void d(String tag, String msg) {
+        Log.d(tag, msg);
+        print(tag, msg);
+    }
+
+    public static void e(String tag, String msg, Exception e) {
+        Log.e(tag, msg, e);
+        print(tag, msg, e);
+    }
+
+    public static void e(String tag, String msg) {
+        Log.e(tag, msg);
+        print(tag, msg);
+    }
+
+    public static void print(String tag, String msg) {
+        print(tag, msg, null);
+    }
+
+    public static void print(String tag, String msg, Exception e) {
+        if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+            return;
+        }
+        String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
+        if (e != null) {
+            out += "\n" + Log.getStackTraceString(e);
+        }
+        Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
+    }
+
+    private static Handler getHandler() {
+        synchronized (DATE_FORMAT) {
+            if (sHandler == null) {
+                // We can use any non-ui looper, but why create another just for logging!
+                sHandler = new Handler(LauncherModel.getWorkerLooper(), new LogWriterCallback());
+            }
+        }
+        return sHandler;
+    }
+
+    /**
+     * Blocks until all the pending logs are written to the disk
+     * @param out if not null, all the persisted logs are copied to the writer.
+     */
+    public static void flushAll(PrintWriter out) throws InterruptedException {
+        if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+            return;
+        }
+        CountDownLatch latch = new CountDownLatch(1);
+        Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
+                Pair.create(out, latch)).sendToTarget();
+
+        latch.await(2, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Writes logs to the file.
+     * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
+     * Logs older than 36 hours are purged.
+     */
+    private static class LogWriterCallback implements Handler.Callback {
+
+        private static final long CLOSE_DELAY = 5000;  // 5 seconds
+
+        private static final int MSG_WRITE = 1;
+        private static final int MSG_CLOSE = 2;
+        private static final int MSG_FLUSH = 3;
+
+        private String mCurrentFileName = null;
+        private PrintWriter mCurrentWriter = null;
+
+        private void closeWriter() {
+            Utilities.closeSilently(mCurrentWriter);
+            mCurrentWriter = null;
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            if (sLogsDirectory == null || !ProviderConfig.IS_DOGFOOD_BUILD) {
+                return true;
+            }
+            switch (msg.what) {
+                case MSG_WRITE: {
+                    Calendar cal = Calendar.getInstance();
+                    // suffix with 0 or 1 based on the day of the year.
+                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+
+                    if (!fileName.equals(mCurrentFileName)) {
+                        closeWriter();
+                    }
+
+                    try {
+                        if (mCurrentWriter == null) {
+                            mCurrentFileName = fileName;
+
+                            boolean append = false;
+                            File logFile = new File(sLogsDirectory, fileName);
+                            if (logFile.exists()) {
+                                Calendar modifiedTime = Calendar.getInstance();
+                                modifiedTime.setTimeInMillis(logFile.lastModified());
+
+                                // If the file was modified more that 36 hours ago, purge the file.
+                                // We use instead of 24 to account for day-365 followed by day-1
+                                modifiedTime.add(Calendar.HOUR, 36);
+                                append = cal.before(modifiedTime)
+                                        && logFile.length() < MAX_LOG_FILE_SIZE;
+                            }
+                            mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
+                        }
+
+                        mCurrentWriter.println((String) msg.obj);
+                        mCurrentWriter.flush();
+
+                        // Auto close file stream after some time.
+                        sHandler.removeMessages(MSG_CLOSE);
+                        sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
+                    } catch (Exception e) {
+                        Log.e("FileLog", "Error writing logs to file", e);
+                        // Close stream, will try reopening during next log
+                        closeWriter();
+                    }
+                    return true;
+                }
+                case MSG_CLOSE: {
+                    closeWriter();
+                    return true;
+                }
+                case MSG_FLUSH: {
+                    closeWriter();
+                    Pair<PrintWriter, CountDownLatch> p =
+                            (Pair<PrintWriter, CountDownLatch>) msg.obj;
+
+                    if (p.first != null) {
+                        dumpFile(p.first, FILE_NAME_PREFIX + 0);
+                        dumpFile(p.first, FILE_NAME_PREFIX + 1);
+                    }
+                    p.second.countDown();
+                    return true;
+                }
+            }
+            return true;
+        }
+    }
+
+    private static void dumpFile(PrintWriter out, String fileName) {
+        File logFile = new File(sLogsDirectory, fileName);
+        if (logFile.exists()) {
+
+            BufferedReader in = null;
+            try {
+                in = new BufferedReader(new FileReader(logFile));
+                out.println();
+                out.println("--- logfile: " + fileName + " ---");
+                String line;
+                while ((line = in.readLine()) != null) {
+                    out.println(line);
+                }
+            } catch (Exception e) {
+                // ignore
+            } finally {
+                Utilities.closeSilently(in);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/logging/FileLogTest.java b/tests/src/com/android/launcher3/logging/FileLogTest.java
new file mode 100644
index 0000000..c24cc3f
--- /dev/null
+++ b/tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -0,0 +1,77 @@
+package com.android.launcher3.logging;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Calendar;
+
+/**
+ * Tests for {@link FileLog}
+ */
+@SmallTest
+public class FileLogTest extends AndroidTestCase {
+
+    private File mTempDir;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        int count = 0;
+        do {
+            mTempDir = new File(getContext().getCacheDir(), "log-test-" + (count++));
+        } while(!mTempDir.mkdir());
+
+        FileLog.setDir(mTempDir);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Clear existing logs
+        new File(mTempDir, "log-0").delete();
+        new File(mTempDir, "log-1").delete();
+        mTempDir.delete();
+        super.tearDown();
+    }
+
+    public void testPrintLog() throws Exception {
+        FileLog.print("Testing", "hoolalala");
+        StringWriter writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("hoolalala"));
+
+        FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+        writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("abracadabra"));
+        // Exception is also printed
+        assertTrue(writer.toString().contains("cat! cat!"));
+
+        // Old logs still present after flush
+        assertTrue(writer.toString().contains("hoolalala"));
+    }
+
+    public void testOldFileTruncated() throws Exception {
+        FileLog.print("Testing", "hoolalala");
+        StringWriter writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("hoolalala"));
+
+        Calendar threeDaysAgo = Calendar.getInstance();
+        threeDaysAgo.add(Calendar.HOUR, -72);
+        new File(mTempDir, "log-0").setLastModified(threeDaysAgo.getTimeInMillis());
+        new File(mTempDir, "log-1").setLastModified(threeDaysAgo.getTimeInMillis());
+
+        FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+        writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("abracadabra"));
+        // Exception is also printed
+        assertTrue(writer.toString().contains("cat! cat!"));
+
+        // Old logs have been truncated
+        assertFalse(writer.toString().contains("hoolalala"));
+    }
+}