OSDN Git Service

Adding a utility class for persistant logging.
authorSunny Goyal <sunnygoyal@google.com>
Fri, 6 May 2016 16:58:34 +0000 (09:58 -0700)
committerSunny Goyal <sunnygoyal@google.com>
Mon, 9 May 2016 19:47:42 +0000 (12:47 -0700)
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

src/com/android/launcher3/Launcher.java
src/com/android/launcher3/LauncherAppState.java
src/com/android/launcher3/LauncherModel.java
src/com/android/launcher3/Utilities.java
src/com/android/launcher3/config/ProviderConfig.java
src/com/android/launcher3/logging/FileLog.java [new file with mode: 0644]
tests/src/com/android/launcher3/logging/FileLogTest.java [new file with mode: 0644]

index d2d1d02..21adcb7 100644 (file)
@@ -114,6 +114,7 @@ import com.android.launcher3.logging.UserEventDispatcher;
 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 com.android.launcher3.widget.WidgetsContainerView;
 
 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 @@ public class Launcher extends Activity
     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 @@ public class Launcher extends Activity
 
             // 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 @@ public class Launcher extends Activity
             }
         }
 
-        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 class Launcher extends Activity
         }
     }
 
-    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);
     }
index f84e4b5..0fe6398 100644 (file)
@@ -30,6 +30,7 @@ import com.android.launcher3.compat.UserManagerCompat;
 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 @@ public class LauncherAppState {
         // 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() {
index 884685c..2fd12fd 100644 (file)
@@ -60,6 +60,7 @@ import com.android.launcher3.model.GridSizeMigrationTask;
 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 @@ public class LauncherModel extends BroadcastReceiver
                 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 @@ public class LauncherModel extends BroadcastReceiver
                                             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 @@ public class LauncherModel extends BroadcastReceiver
                                         } 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 @@ public class LauncherModel extends BroadcastReceiver
                                                     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 @@ public class LauncherModel extends BroadcastReceiver
                                         } 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 @@ public class LauncherModel extends BroadcastReceiver
                                         restored = false;
                                     }
                                 } catch (URISyntaxException e) {
-                                    addDumpLog("Invalid uri: " + intentDescription);
+                                    FileLog.d(TAG, "Invalid uri: " + intentDescription);
                                     itemsToRemove.add(id);
                                     continue;
                                 }
@@ -2073,7 +2074,7 @@ public class LauncherModel extends BroadcastReceiver
                                 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 @@ public class LauncherModel extends BroadcastReceiver
                                             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 @@ public class LauncherModel extends BroadcastReceiver
                         }
                     }
                 } finally {
-                    if (c != null) {
-                        c.close();
-                    }
+                    Utilities.closeSilently(c);
                 }
 
                 // Break early if we've stopped loading
@@ -3541,8 +3540,4 @@ public class LauncherModel extends BroadcastReceiver
     public static Looper getWorkerLooper() {
         return sWorkerThread.getLooper();
     }
-
-    @Thunk static final void addDumpLog(String log) {
-        Launcher.addDumpLog(TAG, log);
-    }
 }
index 1acbfc1..871f390 100644 (file)
@@ -63,9 +63,11 @@ import android.widget.Toast;
 
 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 @@ public final class Utilities {
         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
index 825b434..1d964b1 100644 (file)
@@ -20,5 +20,5 @@ public class ProviderConfig {
 
     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 (file)
index 0000000..f822695
--- /dev/null
@@ -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 (file)
index 0000000..c24cc3f
--- /dev/null
@@ -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"));
+    }
+}