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;
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;
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;
// 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;
}
}
}
- 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) {
}
}
- 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);
}
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;
// 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() {
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;
try {
screenIds.add(sc.getLong(idIndex));
} catch (Exception e) {
- addDumpLog("Invalid screen id: " + e);
+ FileLog.d(TAG, "Invalid screen id", e);
}
}
} finally {
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 {
} 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.
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;
}
} 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;
}
restored = false;
}
} catch (URISyntaxException e) {
- addDumpLog("Invalid uri: " + intentDescription);
+ FileLog.d(TAG, "Invalid uri: " + intentDescription);
itemsToRemove.add(id);
continue;
}
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 {
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;
}
}
}
} finally {
- if (c != null) {
- c.close();
- }
+ Utilities.closeSilently(c);
}
// Break early if we've stopped loading
public static Looper getWorkerLooper() {
return sWorkerThread.getLooper();
}
-
- @Thunk static final void addDumpLog(String log) {
- Launcher.addDumpLog(TAG, log);
- }
}
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;
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
public static final String AUTHORITY = "com.android.launcher3.settings".intern();
- public static boolean IS_DOGFOOD_BUILD = false;
+ public static boolean IS_DOGFOOD_BUILD = true;
}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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"));
+ }
+}