diff options
5 files changed, 488 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/log/Event.java b/packages/SystemUI/src/com/android/systemui/log/Event.java new file mode 100644 index 000000000000..92862a2bc74c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/Event.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Stores information about an event that occurred in SystemUI to be used for debugging and triage. + * Every event has a time stamp, log level and message. + * Events are stored in {@link SysuiLog} and can be printed in a dumpsys. + */ +public class Event { + public static final int UNINITIALIZED = -1; + + @IntDef({ERROR, WARN, INFO, DEBUG, VERBOSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Level {} + public static final int VERBOSE = 2; + public static final int DEBUG = 3; + public static final int INFO = 4; + public static final int WARN = 5; + public static final int ERROR = 6; + + private long mTimestamp; + private @Level int mLogLevel = DEBUG; + protected String mMessage; + + public Event(String message) { + mTimestamp = System.currentTimeMillis(); + mMessage = message; + } + + public Event(@Level int logLevel, String message) { + mTimestamp = System.currentTimeMillis(); + mLogLevel = logLevel; + mMessage = message; + } + + public String getMessage() { + return mMessage; + } + + public long getTimestamp() { + return mTimestamp; + } + + public @Level int getLogLevel() { + return mLogLevel; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/RichEvent.java b/packages/SystemUI/src/com/android/systemui/log/RichEvent.java new file mode 100644 index 000000000000..89b7a8181c44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/RichEvent.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log; + +/** + * Stores information about an event that occurred in SystemUI to be used for debugging and triage. + * Every rich event has a time stamp, event type, and log level, with the option to provide the + * reason this event was triggered. + * Events are stored in {@link SysuiLog} and can be printed in a dumpsys. + */ +public abstract class RichEvent extends Event { + private final int mType; + private final String mReason; + + /** + * Create a rich event that includes an event type that matches with an index in the array + * getEventLabels(). + */ + public RichEvent(@Event.Level int logLevel, int type, String reason) { + super(logLevel, null); + final int numEvents = getEventLabels().length; + if (type < 0 || type >= numEvents) { + throw new IllegalArgumentException("Unsupported event type. Events only supported" + + " from 0 to " + (numEvents - 1) + ", but given type=" + type); + } + mType = type; + mReason = reason; + mMessage = getEventLabels()[mType] + " " + mReason; + } + + /** + * Returns an array of the event labels. The index represents the event type and the + * corresponding String stored at that index is the user-readable representation of that event. + * @return array of user readable events, where the index represents its event type constant + */ + public abstract String[] getEventLabels(); + + public int getType() { + return mType; + } + + public String getReason() { + return mReason; + } + + /** + * Builder to build a RichEvent. + * @param <B> Log specific builder that is extending this builder + */ + public abstract static class Builder<B extends Builder<B>> { + public static final int UNINITIALIZED = -1; + + private B mBuilder = getBuilder(); + protected int mType = UNINITIALIZED; + protected String mReason; + protected @Level int mLogLevel; + + /** + * Get the log-specific builder. + */ + public abstract B getBuilder(); + + /** + * Build the log-specific event. + */ + public abstract RichEvent build(); + + /** + * Optional - set the log level. Defaults to DEBUG. + */ + public B setLogLevel(@Level int logLevel) { + mLogLevel = logLevel; + return mBuilder; + } + + /** + * Required - set the event type. These events must correspond with the events from + * getEventLabels(). + */ + public B setType(int type) { + mType = type; + return mBuilder; + } + + /** + * Optional - set the reason why this event was triggered. + */ + public B setReason(String reason) { + mReason = reason; + return mBuilder; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/SysuiLog.java b/packages/SystemUI/src/com/android/systemui/log/SysuiLog.java new file mode 100644 index 000000000000..a6e10e6b345b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/SysuiLog.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log; + +import android.os.Build; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.DumpController; +import com.android.systemui.Dumpable; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.Locale; + +/** + * Thread-safe logger in SystemUI which prints logs to logcat and stores logs to be + * printed by the DumpController. This is an alternative to printing directly + * to avoid logs being deleted by chatty. The number of logs retained is varied based on + * whether the build is {@link Build.IS_DEBUGGABLE}. + * + * To manually view the logs via adb: + * adb shell dumpsys activity service com.android.systemui/.SystemUIService \ + * dependency DumpController <SysuiLogId> + */ +public class SysuiLog implements Dumpable { + public static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("MM-dd HH:mm:ss", Locale.US); + + private final Object mDataLock = new Object(); + private final String mId; + private final int mMaxLogs; + private boolean mEnabled; + + @VisibleForTesting protected ArrayDeque<Event> mTimeline; + + /** + * Creates a SysuiLog + * To enable or disable logs, set the system property and then restart the device: + * adb shell setprop sysui.log.enabled.<id> true/false && adb reboot + * @param dumpController where to register this logger's dumpsys + * @param id user-readable tag for this logger + * @param maxDebugLogs maximum number of logs to retain when {@link sDebuggable} is true + * @param maxLogs maximum number of logs to retain when {@link sDebuggable} is false + */ + public SysuiLog(DumpController dumpController, String id, int maxDebugLogs, int maxLogs) { + this(dumpController, id, sDebuggable ? maxDebugLogs : maxLogs, + SystemProperties.getBoolean(SYSPROP_ENABLED_PREFIX + id, DEFAULT_ENABLED)); + } + + @VisibleForTesting + protected SysuiLog(DumpController dumpController, String id, int maxLogs, boolean enabled) { + mId = id; + mMaxLogs = maxLogs; + mEnabled = enabled; + mTimeline = mEnabled ? new ArrayDeque<>(mMaxLogs) : null; + dumpController.registerDumpable(mId, this); + } + + public SysuiLog(DumpController dumpController, String id) { + this(dumpController, id, DEFAULT_MAX_DEBUG_LOGS, DEFAULT_MAX_LOGS); + } + + /** + * Logs an event to the timeline which can be printed by the dumpsys. + * May also log to logcat if enabled. + * @return true if event was logged, else false + */ + public boolean log(Event event) { + if (!mEnabled) { + return false; + } + + synchronized (mDataLock) { + if (mTimeline.size() >= mMaxLogs) { + mTimeline.removeFirst(); + } + + mTimeline.add(event); + } + + if (LOG_TO_LOGCAT_ENABLED) { + final String strEvent = eventToString(event); + switch (event.getLogLevel()) { + case Event.VERBOSE: + Log.v(mId, strEvent); + break; + case Event.DEBUG: + Log.d(mId, strEvent); + break; + case Event.ERROR: + Log.e(mId, strEvent); + break; + case Event.INFO: + Log.i(mId, strEvent); + break; + case Event.WARN: + Log.w(mId, strEvent); + break; + } + } + return true; + } + + /** + * @return user-readable string of the given event + */ + public String eventToString(Event event) { + StringBuilder sb = new StringBuilder(); + sb.append(SysuiLog.DATE_FORMAT.format(event.getTimestamp())); + sb.append(" "); + sb.append(event.getMessage()); + return sb.toString(); + } + + /** + * only call on this method if you have the mDataLock + */ + private void dumpTimelineLocked(PrintWriter pw) { + pw.println("\tTimeline:"); + + for (Event event : mTimeline) { + pw.println("\t" + eventToString(event)); + } + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(mId + ":"); + + if (mEnabled) { + synchronized (mDataLock) { + dumpTimelineLocked(pw); + } + } else { + pw.print(" - Logging disabled."); + } + } + + private static boolean sDebuggable = Build.IS_DEBUGGABLE; + private static final String SYSPROP_ENABLED_PREFIX = "sysui.log.enabled."; + private static final boolean LOG_TO_LOGCAT_ENABLED = sDebuggable; + private static final boolean DEFAULT_ENABLED = sDebuggable; + private static final int DEFAULT_MAX_DEBUG_LOGS = 100; + private static final int DEFAULT_MAX_LOGS = 50; +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/RichEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/RichEventTest.java new file mode 100644 index 000000000000..2f90641775e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/RichEventTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log; + +import static junit.framework.Assert.assertEquals; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class RichEventTest extends SysuiTestCase { + + private static final int TOTAL_EVENT_TYPES = 1; + + @Test + public void testCreateRichEvent_invalidType() { + try { + // indexing for events starts at 0, so TOTAL_EVENT_TYPES is an invalid type + new TestableRichEvent(Event.DEBUG, TOTAL_EVENT_TYPES, "msg"); + } catch (IllegalArgumentException e) { + // expected + return; + } + + Assert.fail("Expected an invalidArgumentException since the event type was invalid."); + } + + @Test + public void testCreateRichEvent() { + final int eventType = 0; + RichEvent e = new TestableRichEvent(Event.DEBUG, eventType, "msg"); + assertEquals(e.getType(), eventType); + } + + class TestableRichEvent extends RichEvent { + TestableRichEvent(int logLevel, int type, String reason) { + super(logLevel, type, reason); + } + + @Override + public String[] getEventLabels() { + return new String[]{"ACTION_NAME"}; + } + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SysuiLogTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SysuiLogTest.java new file mode 100644 index 000000000000..378bba1afda3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/SysuiLogTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log; + +import static junit.framework.Assert.assertEquals; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.DumpController; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class SysuiLogTest extends SysuiTestCase { + private static final String TEST_ID = "TestLogger"; + private static final int MAX_LOGS = 5; + + @Mock + private DumpController mDumpController; + private SysuiLog mSysuiLog; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLogDisabled_noLogsWritten() { + mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, false); + assertEquals(mSysuiLog.mTimeline, null); + + mSysuiLog.log(new Event("msg")); + assertEquals(mSysuiLog.mTimeline, null); + } + + @Test + public void testLogEnabled_logWritten() { + mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, true); + assertEquals(mSysuiLog.mTimeline.size(), 0); + + mSysuiLog.log(new Event("msg")); + assertEquals(mSysuiLog.mTimeline.size(), 1); + } + + @Test + public void testMaxLogs() { + mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, true); + assertEquals(mSysuiLog.mTimeline.size(), 0); + + final String msg = "msg"; + for (int i = 0; i < MAX_LOGS + 1; i++) { + mSysuiLog.log(new Event(msg + i)); + } + + assertEquals(mSysuiLog.mTimeline.size(), MAX_LOGS); + + // check the first message (msg0) is deleted: + assertEquals(mSysuiLog.mTimeline.getFirst().getMessage(), msg + "1"); + } +} |