Add a way to configure log levels

Bug: 381112373
Bug: 380949304
Bug: 380938496
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -s
Flag: EXEMPT host test change only

Change-Id: I3851ba55abf8cdbdf518dda83b4780f2128c013d
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 172cec3..3d2bb8e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -105,6 +105,9 @@
     private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
 
+    private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
+    private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
+
     /**
      * When enabled, attempt to dump all thread stacks just before we hit the
      * overall Tradefed timeout, to aid in debugging deadlocks.
@@ -232,20 +235,22 @@
         // Make sure libravenwood_runtime is loaded.
         System.load(RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_RUNTIME_NAME));
 
+        Log_ravenwood.setLogLevels(getLogTags());
         Log_ravenwood.onRavenwoodRuntimeNativeReady();
 
         // Do the basic set up for the android sysprops.
         RavenwoodSystemProperties.initialize();
 
+        // Enable all log levels for native logging, until we'll have a way to change the native
+        // side log level at runtime.
         // Do this after loading RAVENWOOD_NATIVE_RUNTIME_NAME (which backs Os.setenv()),
         // before loadFrameworkNativeCode() (which uses $ANDROID_LOG_TAGS).
-        if (RAVENWOOD_VERBOSE_LOGGING) {
-            RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging");
-            try {
-                Os.setenv("ANDROID_LOG_TAGS", "*:v", true);
-            } catch (ErrnoException e) {
-                throw new RuntimeException(e);
-            }
+        // This would also prevent libbase from crashing the process (b/381112373) because
+        // the string format it accepts is very limited.
+        try {
+            Os.setenv("ANDROID_LOG_TAGS", "*:v", true);
+        } catch (ErrnoException e) {
+            throw new RuntimeException(e);
         }
 
         // Make sure libandroid_runtime is loaded.
@@ -333,6 +338,18 @@
         initializeCompatIds();
     }
 
+    /**
+     * Get log tags from environmental variable.
+     */
+    @Nullable
+    private static String getLogTags() {
+        var logTags = System.getenv(RAVENWOOD_ANDROID_LOG_TAGS);
+        if (logTags == null) {
+            logTags = System.getenv(ANDROID_LOG_TAGS);
+        }
+        return logTags;
+    }
+
     private static void loadRavenwoodProperties() {
         var props = RavenwoodSystemProperties.readProperties("ravenwood.properties");
 
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index e49d3d9..d4b15c6 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -193,6 +193,12 @@
         return IS_ON_RAVENWOOD;
     }
 
+    private static void ensureOnRavenwood(String featureName) {
+        if (!IS_ON_RAVENWOOD) {
+            throw new RuntimeException(featureName + " is only supported on Ravenwood.");
+        }
+    }
+
     /**
      * @deprecated Use
      * {@code androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getContext()}
@@ -242,6 +248,40 @@
         return System.currentTimeMillis();
     }
 
+    /**
+     * Equivalent to setting the ANDROID_LOG_TAGS environmental variable.
+     *
+     * See https://developer.android.com/tools/logcat#filteringOutput for the string format.
+     *
+     * NOTE: this works only on Ravenwood.
+     */
+    public static void setAndroidLogTags(@Nullable String androidLogTags) {
+        ensureOnRavenwood("RavenwoodRule.setAndroidLogTags()");
+        try {
+            Class<?> logRavenwoodClazz = Class.forName("android.util.Log_ravenwood");
+            var setter = logRavenwoodClazz.getMethod("setLogLevels", String.class);
+            setter.invoke(null, androidLogTags);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Set a log level for a given tag. Pass NULL to {@code tag} to change the default level.
+     *
+     * NOTE: this works only on Ravenwood.
+     */
+    public static void setLogLevel(@Nullable String tag, int level) {
+        ensureOnRavenwood("RavenwoodRule.setLogLevel()");
+        try {
+            Class<?> logRavenwoodClazz = Class.forName("android.util.Log_ravenwood");
+            var setter = logRavenwoodClazz.getMethod("setLogLevel", String.class, int.class);
+            setter.invoke(null, tag, level);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     // Below are internal to ravenwood. Don't use them from normal tests...
 
     public static class RavenwoodPrivate {
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
index 7b26fe5..96fa212 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
@@ -15,8 +15,10 @@
  */
 package android.util;
 
+import android.annotation.Nullable;
 import android.util.Log.Level;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.RuntimeInit;
 import com.android.ravenwood.RavenwoodRuntimeNative;
 import com.android.ravenwood.common.RavenwoodCommonUtils;
@@ -24,7 +26,9 @@
 import java.io.PrintStream;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
 
 /**
  * Ravenwood "native substitution" class for {@link android.util.Log}.
@@ -35,16 +39,101 @@
  */
 public class Log_ravenwood {
 
-    public static final SimpleDateFormat sTimestampFormat =
+    private static final SimpleDateFormat sTimestampFormat =
             new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US);
 
-    public static boolean isLoggable(String tag, @Level int level) {
-        return true;
+    private static final Object sLock = new Object();
+
+    @GuardedBy("sLock")
+    private static int sDefaultLogLevel;
+
+    @GuardedBy("sLock")
+    private static final Map<String, Integer> sTagLogLevels = new HashMap<>();
+
+    /**
+     * Used by {@link android.platform.test.ravenwood.RavenwoodRule#setAndroidLogTags(String)}
+     * via reflections.
+     */
+    public static void setLogLevels(String androidLogTags) {
+        var map = parseLogLevels(androidLogTags);
+
+        synchronized (sLock) {
+            sTagLogLevels.clear();
+            sTagLogLevels.putAll(map);
+
+            var def = map.get("*");
+            sDefaultLogLevel = def != null ? def
+                    : RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING ?  Log.VERBOSE : Log.INFO;
+        }
+    }
+
+    private static Map<String, Integer> parseLogLevels(String androidLogTags) {
+        final Map<String, Integer> ret = new HashMap<>();
+
+        if (androidLogTags == null) {
+            return ret;
+        }
+
+        String[] tagPairs = androidLogTags.trim().split("\\s+");
+        for (String tagPair : tagPairs) {
+            String[] parts = tagPair.split(":");
+            if (parts.length == 2) {
+                String tag = parts[0];
+                try {
+                    int priority = switch (parts[1]) {
+                        case "V": yield Log.VERBOSE;
+                        case "D": yield Log.DEBUG;
+                        case "I": yield Log.INFO;
+                        case "W": yield Log.WARN;
+                        case "E": yield Log.ERROR;
+                        case "F": yield Log.ERROR + 1; // Not used in the java side.
+                        case "S": yield Integer.MAX_VALUE; // Silent
+                        default: throw new IllegalArgumentException(
+                                "Invalid priority level for tag: " + tag);
+                    };
+
+                    ret.put(tag, priority);
+                } catch (IllegalArgumentException e) {
+                    System.err.println(e.getMessage());
+                }
+            } else {
+                System.err.println("Invalid tag format: " + tagPair);
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Used by {@link android.platform.test.ravenwood.RavenwoodRule#setLogLevel(String, int)}
+     * via reflections. Pass NULL to {@code tag} to set the default level.
+     */
+    public static void setLogLevel(@Nullable String tag, int level) {
+        synchronized (sLock) {
+            if (tag == null) {
+                sDefaultLogLevel = level;
+            } else {
+                sTagLogLevels.put(tag, level);
+            }
+        }
+    }
+
+    /**
+     * Replaces {@link Log#isLoggable}.
+     */
+    public static boolean isLoggable(String tag, @Level int priority) {
+        synchronized (sLock) {
+            var threshold = sTagLogLevels.get(tag);
+            if (threshold == null) {
+                threshold = sDefaultLogLevel;
+            }
+            return priority >= threshold;
+        }
     }
 
     public static int println_native(int bufID, int priority, String tag, String msg) {
-        if (priority < Log.INFO && !RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING) {
-            return msg.length(); // No verbose logging.
+        if (!isLoggable(tag, priority)) {
+            return msg.length();
         }
 
         final String prio;
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodLogLevelTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodLogLevelTest.java
new file mode 100644
index 0000000..7be562c
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodLogLevelTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.coretest;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import org.junit.Test;
+
+public class RavenwoodLogLevelTest {
+    /**
+     * Assert that the `priority` is loggable, but one level below is not.
+     */
+    private void assertBarelyLoggable(String tag, int priority) {
+        assertEquals(true, Log.isLoggable(tag, priority));
+        assertEquals(false, Log.isLoggable(tag, priority - 1));
+    }
+
+    @Test
+    public void testDefaultLogTags() {
+        RavenwoodRule.setAndroidLogTags(null);
+
+        // Info should always be loggable.
+        assertEquals(true, Log.isLoggable("TAG1", Log.INFO));
+        assertEquals(true, Log.isLoggable("TAG2", Log.INFO));
+
+        var verboseEnabled = RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+        assertEquals(verboseEnabled, Log.isLoggable("TAG1", Log.DEBUG));
+        assertEquals(verboseEnabled, Log.isLoggable("TAG2", Log.VERBOSE));
+    }
+
+    @Test
+    public void testAllVerbose() {
+        RavenwoodRule.setAndroidLogTags("*:V");
+
+        assertEquals(true, Log.isLoggable("TAG1", Log.INFO));
+        assertEquals(true, Log.isLoggable("TAG2", Log.INFO));
+
+        assertEquals(true, Log.isLoggable("TAG1", Log.DEBUG));
+        assertEquals(true, Log.isLoggable("TAG2", Log.VERBOSE));
+    }
+
+    @Test
+    public void testAllSilent() {
+        RavenwoodRule.setAndroidLogTags("*:S");
+
+        assertEquals(false, Log.isLoggable("TAG1", Log.ASSERT));
+        assertEquals(false, Log.isLoggable("TAG2", Log.ASSERT));
+    }
+
+    @Test
+    public void testComplex() {
+        RavenwoodRule.setAndroidLogTags("TAG1:W TAG2:D *:I");
+
+        assertBarelyLoggable("TAG1", Log.WARN);
+        assertBarelyLoggable("TAG2", Log.DEBUG);
+        assertBarelyLoggable("TAG3", Log.INFO);
+    }
+
+    @Test
+    public void testAllVerbose_setLogLevel() {
+        RavenwoodRule.setAndroidLogTags(null);
+        RavenwoodRule.setLogLevel(null, Log.VERBOSE);
+
+        assertEquals(true, Log.isLoggable("TAG1", Log.INFO));
+        assertEquals(true, Log.isLoggable("TAG2", Log.INFO));
+
+        assertEquals(true, Log.isLoggable("TAG1", Log.DEBUG));
+        assertEquals(true, Log.isLoggable("TAG2", Log.VERBOSE));
+    }
+
+    @Test
+    public void testAllSilent_setLogLevel() {
+        RavenwoodRule.setAndroidLogTags(null);
+        RavenwoodRule.setLogLevel(null, Log.ASSERT + 1);
+
+        assertEquals(false, Log.isLoggable("TAG1", Log.ASSERT));
+        assertEquals(false, Log.isLoggable("TAG2", Log.ASSERT));
+    }
+
+    @Test
+    public void testComplex_setLogLevel() {
+        RavenwoodRule.setAndroidLogTags(null);
+        RavenwoodRule.setLogLevel(null, Log.INFO);
+        RavenwoodRule.setLogLevel("TAG1", Log.WARN);
+        RavenwoodRule.setLogLevel("TAG2", Log.DEBUG);
+
+        assertBarelyLoggable("TAG1", Log.WARN);
+        assertBarelyLoggable("TAG2", Log.DEBUG);
+        assertBarelyLoggable("TAG3", Log.INFO);
+    }
+}