Merge "startop: add compiler type support to host and device switch."
diff --git a/Android.bp b/Android.bp
index 121decb..ee3cb8f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -234,6 +234,11 @@
"wifi/java",
],
},
+
+ required: [
+ // TODO: remove gps_debug when the build system propagates "required" properly.
+ "gps_debug.conf",
+ ],
}
// Collection of classes that are generated from non-Java files that are not listed in
@@ -307,11 +312,6 @@
static_libs: ["framework-internal-utils"],
- required: [
- // TODO: remove gps_debug when the build system propagates "required" properly.
- "gps_debug.conf",
- ],
-
dxflags: [
"--core-library",
"--multi-dex",
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 1f283ea..bb87d96 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -5845,39 +5845,42 @@
ActivityThread.currentOpPackageName())) {
// This app is noting an app-op for itself. Deliver immediately.
sNotedAppOpsCollector.onSelfNoted(new SyncNotedAppOp(code));
- } else if (binderUid != null && binderUid == uid) {
- // We are inside of a two-way binder call. Delivered to caller via
- // {@link #prefixParcelWithAppOpsIfNeeded}
- long[] appOpsNotedInThisBinderTransaction;
- appOpsNotedInThisBinderTransaction = sAppOpsNotedInThisBinderTransaction.get();
- if (appOpsNotedInThisBinderTransaction == null) {
- appOpsNotedInThisBinderTransaction = new long[2];
- sAppOpsNotedInThisBinderTransaction.set(appOpsNotedInThisBinderTransaction);
- }
+ return;
+ }
+ }
- if (code < 64) {
- appOpsNotedInThisBinderTransaction[0] |= 1L << code;
- } else {
- appOpsNotedInThisBinderTransaction[1] |= 1L << (code - 64);
- }
+ if (binderUid != null && binderUid == uid) {
+ // If this is inside of a two-way binder call: Delivered to caller via
+ // {@link #prefixParcelWithAppOpsIfNeeded}
+ long[] appOpsNotedInThisBinderTransaction;
+
+ appOpsNotedInThisBinderTransaction = sAppOpsNotedInThisBinderTransaction.get();
+ if (appOpsNotedInThisBinderTransaction == null) {
+ appOpsNotedInThisBinderTransaction = new long[2];
+ sAppOpsNotedInThisBinderTransaction.set(appOpsNotedInThisBinderTransaction);
+ }
+
+ if (code < 64) {
+ appOpsNotedInThisBinderTransaction[0] |= 1L << code;
} else {
- // We cannot deliver the note synchronous. Hence send it to the system server to
- // notify the noted process.
- if (message == null) {
- // Default message is a stack trace
- message = getFormattedStackTrace();
- }
+ appOpsNotedInThisBinderTransaction[1] |= 1L << (code - 64);
+ }
+ } else {
+ // Cannot deliver the note synchronous: Hence send it to the system server to
+ // notify the noted process.
+ if (message == null) {
+ // Default message is a stack trace
+ message = getFormattedStackTrace();
+ }
- long token = Binder.clearCallingIdentity();
- try {
- mService.noteAsyncOp(mContext.getOpPackageName(), uid, packageName, code,
- message);
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ long token = Binder.clearCallingIdentity();
+ try {
+ mService.noteAsyncOp(mContext.getOpPackageName(), uid, packageName, code, message);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
}
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index faeecda..de77aaa 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -493,6 +493,9 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
final long intentReceivedTime = System.currentTimeMillis();
+ // This is the only place this value is being set. Effectively final.
+ mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable();
+
mIsSuccessfullySelected = false;
Intent intent = getIntent();
Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
@@ -619,9 +622,6 @@
.addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType())
.addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
- // This is the only place this value is being set. Effectively final.
- mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable();
-
AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled();
if (appPredictor != null) {
mDirectShareAppTargetCache = new HashMap<>();
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index b5d4945..31995f7 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -460,7 +460,7 @@
ZygoteHooks.gcAndFinalize();
}
- private static boolean profileSystemServer() {
+ private static boolean shouldProfileSystemServer() {
boolean defaultValue = SystemProperties.getBoolean("dalvik.vm.profilesystemserver",
/*default=*/ false);
// Can't use DeviceConfig since it's not initialized at this point.
@@ -492,7 +492,7 @@
}
// Capturing profiles is only supported for debug or eng builds since selinux normally
// prevents it.
- if (profileSystemServer() && (Build.IS_USERDEBUG || Build.IS_ENG)) {
+ if (shouldProfileSystemServer() && (Build.IS_USERDEBUG || Build.IS_ENG)) {
try {
Log.d(TAG, "Preparing system server profile");
prepareSystemServerProfile(systemServerClasspath);
@@ -765,8 +765,7 @@
Zygote.applyDebuggerSystemProperty(parsedArgs);
Zygote.applyInvokeWithSystemProperty(parsedArgs);
- if (profileSystemServer()) {
-
+ if (shouldProfileSystemServer()) {
parsedArgs.mRuntimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;
}
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index 7fd94c6..cac691c 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -1,6 +1,7 @@
package com.android.internal.util;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -13,6 +14,8 @@
import android.os.UserHandle;
import android.util.Log;
+import java.util.function.Consumer;
+
public class ScreenshotHelper {
private static final String TAG = "ScreenshotHelper";
@@ -34,17 +37,58 @@
}
/**
- * Request a screenshot be taken.
+ * Request a screenshot be taken with a specific timeout.
*
- * @param screenshotType The type of screenshot, for example either
- * {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
- * or {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
- * @param hasStatus {@code true} if the status bar is currently showing. {@code false} if not.
- * @param hasNav {@code true} if the navigation bar is currently showing. {@code false} if not.
- * @param handler A handler used in case the screenshot times out
+ * Added to support reducing unit test duration; the method variant without a timeout argument
+ * is recommended for general use.
+ *
+ * @param screenshotType The type of screenshot, for example either
+ * {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
+ * or
+ * {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
+ * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
+ * if
+ * not.
+ * @param hasNav {@code true} if the navigation bar is currently showing. {@code
+ * false}
+ * if not.
+ * @param handler A handler used in case the screenshot times out
+ * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
+ * screenshot was taken.
*/
public void takeScreenshot(final int screenshotType, final boolean hasStatus,
- final boolean hasNav, @NonNull Handler handler) {
+ final boolean hasNav, @NonNull Handler handler,
+ @Nullable Consumer<Boolean> completionConsumer) {
+ takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler,
+ completionConsumer);
+ }
+
+ /**
+ * Request a screenshot be taken.
+ *
+ * Added to support reducing unit test duration; the method variant without a timeout argument
+ * is recommended for general use.
+ *
+ * @param screenshotType The type of screenshot, for example either
+ * {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
+ * or
+ * {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
+ * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
+ * if
+ * not.
+ * @param hasNav {@code true} if the navigation bar is currently showing. {@code
+ * false}
+ * if not.
+ * @param timeoutMs If the screenshot hasn't been completed within this time period,
+ * the screenshot attempt will be cancelled and `completionConsumer`
+ * will be run.
+ * @param handler A handler used in case the screenshot times out
+ * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
+ * screenshot was taken.
+ */
+ public void takeScreenshot(final int screenshotType, final boolean hasStatus,
+ final boolean hasNav, long timeoutMs, @NonNull Handler handler,
+ @Nullable Consumer<Boolean> completionConsumer) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
return;
@@ -54,7 +98,8 @@
final Intent serviceIntent = new Intent();
final Runnable mScreenshotTimeout = new Runnable() {
- @Override public void run() {
+ @Override
+ public void run() {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
mContext.unbindService(mScreenshotConnection);
@@ -62,6 +107,9 @@
notifyScreenshotError();
}
}
+ if (completionConsumer != null) {
+ completionConsumer.accept(false);
+ }
}
};
@@ -86,15 +134,22 @@
handler.removeCallbacks(mScreenshotTimeout);
}
}
+ if (completionConsumer != null) {
+ completionConsumer.accept(true);
+ }
}
};
msg.replyTo = new Messenger(h);
- msg.arg1 = hasStatus ? 1: 0;
- msg.arg2 = hasNav ? 1: 0;
+ msg.arg1 = hasStatus ? 1 : 0;
+ msg.arg2 = hasNav ? 1 : 0;
+
try {
messenger.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't take screenshot: " + e);
+ if (completionConsumer != null) {
+ completionConsumer.accept(false);
+ }
}
}
}
@@ -115,7 +170,7 @@
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
UserHandle.CURRENT)) {
mScreenshotConnection = conn;
- handler.postDelayed(mScreenshotTimeout, SCREENSHOT_TIMEOUT_MS);
+ handler.postDelayed(mScreenshotTimeout, timeoutMs);
}
}
}
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 13e1dfa..cf8df28 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -564,11 +564,13 @@
// Read system memory info including ZRAM. The values are stored in the vector
// in the same order as MEMINFO_* enum
- std::vector<uint64_t> mem(MEMINFO_COUNT);
- std::vector<std::string> tags(::android::meminfo::SysMemInfo::kDefaultSysMemInfoTags);
+ std::vector<std::string_view> tags(
+ ::android::meminfo::SysMemInfo::kDefaultSysMemInfoTags.begin(),
+ ::android::meminfo::SysMemInfo::kDefaultSysMemInfoTags.end());
tags.insert(tags.begin() + MEMINFO_ZRAM_TOTAL, "Zram:");
+ std::vector<uint64_t> mem(tags.size());
::android::meminfo::SysMemInfo smi;
- if (!smi.ReadMemInfo(tags, &mem)) {
+ if (!smi.ReadMemInfo(tags.size(), tags.data(), mem.data())) {
jniThrowRuntimeException(env, "SysMemInfo read failed");
return;
}
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 4cb2d15..8aa6f86 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -31,6 +31,7 @@
#include <android-base/unique_fd.h>
#include <algorithm>
+#include <array>
#include <limits>
#include <memory>
#include <string>
@@ -630,14 +631,16 @@
static jlong android_os_Process_getFreeMemory(JNIEnv* env, jobject clazz)
{
- static const std::vector<std::string> memFreeTags = {
+ std::array<std::string_view, 2> memFreeTags = {
::android::meminfo::SysMemInfo::kMemFree,
::android::meminfo::SysMemInfo::kMemCached,
};
std::vector<uint64_t> mem(memFreeTags.size());
::android::meminfo::SysMemInfo smi;
- if (!smi.ReadMemInfo(memFreeTags, &mem)) {
+ if (!smi.ReadMemInfo(memFreeTags.size(),
+ memFreeTags.data(),
+ mem.data())) {
jniThrowRuntimeException(env, "SysMemInfo read failed to get Free Memory");
return -1L;
}
diff --git a/core/proto/Android.bp b/core/proto/Android.bp
index 3b891d6..e199dab 100644
--- a/core/proto/Android.bp
+++ b/core/proto/Android.bp
@@ -28,3 +28,13 @@
"android/bluetooth/smp/enums.proto",
],
}
+
+java_library_host {
+ name: "windowmanager-log-proto",
+ srcs: [
+ "android/server/windowmanagerlog.proto"
+ ],
+ proto: {
+ type: "full",
+ },
+}
diff --git a/core/proto/android/server/windowmanagerlog.proto b/core/proto/android/server/windowmanagerlog.proto
new file mode 100644
index 0000000..5bee1bd
--- /dev/null
+++ b/core/proto/android/server/windowmanagerlog.proto
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+syntax = "proto2";
+
+package com.android.server.wm;
+
+option java_multiple_files = true;
+
+/* represents a single log entry */
+message ProtoLogMessage {
+ /* log statement identifier, created from message string and log level. */
+ optional fixed32 message_hash = 1;
+ /* log time, relative to the elapsed system time clock. */
+ optional fixed64 elapsed_realtime_nanos = 2;
+ /* string parameters passed to the log call. */
+ repeated string str_params = 3;
+ /* integer parameters passed to the log call. */
+ repeated sint64 sint64_params = 4 [packed=true];
+ /* floating point parameters passed to the log call. */
+ repeated double double_params = 5 [packed=true];
+ /* boolean parameters passed to the log call. */
+ repeated bool boolean_params = 6 [packed=true];
+}
+
+/* represents a log file containing window manager log entries.
+ Encoded, it should start with 0x9 0x57 0x49 0x4e 0x44 0x4f 0x4c 0x4f 0x47 (.WINDOLOG), such
+ that they can be easily identified. */
+message WindowManagerLogFileProto {
+ /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+ (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+ constants into .proto files. */
+ enum MagicNumber {
+ INVALID = 0;
+ MAGIC_NUMBER_L = 0x444e4957; /* WIND (little-endian ASCII) */
+ MAGIC_NUMBER_H = 0x474f4c4f; /* OLOG (little-endian ASCII) */
+ }
+
+ /* the magic number header */
+ optional fixed64 magic_number = 1;
+ /* log proto version. */
+ optional string version = 2;
+ /* offset between real-time clock and elapsed system time clock in miliseconds.
+ Calculated as: (System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() / 1000000) */
+ optional fixed64 realTimeToElapsedTimeOffsetMillis = 3;
+ /* log entries */
+ repeated ProtoLogMessage log = 4;
+}
diff --git a/core/proto/android/stats/docsui/docsui_enums.proto b/core/proto/android/stats/docsui/docsui_enums.proto
index 655b5e3..f648912 100644
--- a/core/proto/android/stats/docsui/docsui_enums.proto
+++ b/core/proto/android/stats/docsui/docsui_enums.proto
@@ -184,6 +184,8 @@
TYPE_CHIP_DOCS = 4;
TYPE_SEARCH_HISTORY = 5;
TYPE_SEARCH_STRING = 6;
+ TYPE_CHIP_LARGE_FILES = 7;
+ TYPE_CHIP_FROM_THIS_WEEK = 8;
}
enum SearchMode {
diff --git a/core/tests/screenshothelpertests/Android.bp b/core/tests/screenshothelpertests/Android.bp
new file mode 100644
index 0000000..3d54b68
--- /dev/null
+++ b/core/tests/screenshothelpertests/Android.bp
@@ -0,0 +1,28 @@
+android_test {
+ name: "ScreenshotHelperTests",
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "frameworks-base-testutils",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "mockito-target-minus-junit4",
+ "platform-test-annotations",
+ ],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+
+ platform_apis: true,
+ test_suites: ["device-tests"],
+
+ certificate: "platform",
+}
+
diff --git a/core/tests/screenshothelpertests/AndroidManifest.xml b/core/tests/screenshothelpertests/AndroidManifest.xml
new file mode 100644
index 0000000..2e12ef4d
--- /dev/null
+++ b/core/tests/screenshothelpertests/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:installLocation="internalOnly"
+ package="com.android.internal.util"
+ android:sharedUserId="android.uid.systemui" >
+
+ <application >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.internal.util"
+ android:label="Screenshot Helper Tests" />
+
+ <protected-broadcast android:name="android.intent.action.USER_PRESENT" />
+
+</manifest>
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
new file mode 100644
index 0000000..8483645
--- /dev/null
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.internal.util;
+
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class ScreenshotHelperTest {
+ private Context mContext;
+ private ScreenshotHelper mScreenshotHelper;
+ private Handler mHandler;
+
+
+ @Before
+ public void setUp() {
+ // `ScreenshotHelper.notifyScreenshotError()` calls `Context.sendBroadcastAsUser()` and
+ // `Context.bindServiceAsUser`.
+ //
+ // This raises a `SecurityException` if the device is locked. Calling either `Context`
+ // method results in a broadcast of `android.intent.action. USER_PRESENT`. Only the system
+ // process is allowed to broadcast that `Intent`.
+ mContext = Mockito.spy(Context.class);
+ Mockito.doNothing().when(mContext).sendBroadcastAsUser(any(), any());
+ Mockito.doReturn(true).when(mContext).bindServiceAsUser(any(), any(), anyInt(), any());
+
+ mHandler = new Handler(Looper.getMainLooper());
+ mScreenshotHelper = new ScreenshotHelper(mContext);
+ }
+
+ @Test
+ public void testFullscreenScreenshot() {
+ mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, mHandler, null);
+ }
+
+ @Test
+ public void testSelectedRegionScreenshot() {
+ mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, false, false, mHandler,
+ null);
+ }
+
+ @Test
+ public void testScreenshotTimesOut() {
+ long timeoutMs = 10;
+
+ CountDownLatch lock = new CountDownLatch(1);
+ mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, timeoutMs,
+ mHandler,
+ worked -> {
+ assertFalse(worked);
+ lock.countDown();
+ });
+
+ try {
+ // Add tolerance for delay to prevent flakes.
+ long awaitDurationMs = timeoutMs + 100;
+ if (!lock.await(awaitDurationMs, TimeUnit.MILLISECONDS)) {
+ fail("lock never freed");
+ }
+ } catch (InterruptedException e) {
+ fail("lock interrupted");
+ }
+ }
+}
diff --git a/packages/BackupEncryption/Android.bp b/packages/BackupEncryption/Android.bp
index 50dbcdb..9bcd677 100644
--- a/packages/BackupEncryption/Android.bp
+++ b/packages/BackupEncryption/Android.bp
@@ -17,8 +17,15 @@
android_app {
name: "BackupEncryption",
srcs: ["src/**/*.java"],
+ libs: ["backup-encryption-protos"],
optimize: { enabled: false },
platform_apis: true,
certificate: "platform",
privileged: true,
-}
\ No newline at end of file
+}
+
+java_library {
+ name: "backup-encryption-protos",
+ proto: { type: "nano" },
+ srcs: ["proto/**/*.proto"],
+}
diff --git a/packages/BackupEncryption/proto/wrapped_key.proto b/packages/BackupEncryption/proto/wrapped_key.proto
new file mode 100644
index 0000000..817b7b4
--- /dev/null
+++ b/packages/BackupEncryption/proto/wrapped_key.proto
@@ -0,0 +1,52 @@
+syntax = "proto2";
+
+package android_backup_crypto;
+
+option java_package = "com.android.server.backup.encryption.protos";
+option java_outer_classname = "WrappedKeyProto";
+
+// Metadata associated with a tertiary key.
+message KeyMetadata {
+ // Type of Cipher algorithm the key is used for.
+ enum Type {
+ UNKNOWN = 0;
+ // No padding. Uses 12-byte nonce. Tag length 16 bytes.
+ AES_256_GCM = 1;
+ }
+
+ // What kind of Cipher algorithm the key is used for. We assume at the moment
+ // that this will always be AES_256_GCM and throw if this is not the case.
+ // Provided here for forwards compatibility in case at some point we need to
+ // change Cipher algorithm.
+ optional Type type = 1;
+}
+
+// An encrypted tertiary key.
+message WrappedKey {
+ // The Cipher with which the key was encrypted.
+ enum WrapAlgorithm {
+ UNKNOWN = 0;
+ // No padding. Uses 16-byte nonce (see nonce field). Tag length 16 bytes.
+ // The nonce is 16-bytes as this is wrapped with a key in AndroidKeyStore.
+ // AndroidKeyStore requires that it generates the IV, and it generates a
+ // 16-byte IV for you. You CANNOT provide your own IV.
+ AES_256_GCM = 1;
+ }
+
+ // Cipher algorithm used to wrap the key. We assume at the moment that this
+ // is always AES_256_GC and throw if this is not the case. Provided here for
+ // forwards compatibility if at some point we need to change Cipher algorithm.
+ optional WrapAlgorithm wrap_algorithm = 1;
+
+ // The nonce used to initialize the Cipher in AES/256/GCM mode.
+ optional bytes nonce = 2;
+
+ // The encrypted bytes of the key material.
+ optional bytes key = 3;
+
+ // Associated key metadata.
+ optional KeyMetadata metadata = 4;
+
+ // Deprecated field; Do not use
+ reserved 5;
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java
new file mode 100644
index 0000000..a043c1f
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java
@@ -0,0 +1,132 @@
+/*
+ * 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.server.backup.encryption.keys;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+/** Utility functions for wrapping and unwrapping tertiary keys. */
+public class KeyWrapUtils {
+ private static final String AES_GCM_MODE = "AES/GCM/NoPadding";
+ private static final int GCM_TAG_LENGTH_BYTES = 16;
+ private static final int BITS_PER_BYTE = 8;
+ private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
+ private static final String KEY_ALGORITHM = "AES";
+
+ /**
+ * Uses the secondary key to unwrap the wrapped tertiary key.
+ *
+ * @param secondaryKey The secondary key used to wrap the tertiary key.
+ * @param wrappedKey The wrapped tertiary key.
+ * @return The unwrapped tertiary key.
+ * @throws InvalidKeyException if the provided secondary key cannot unwrap the tertiary key.
+ */
+ public static SecretKey unwrap(SecretKey secondaryKey, WrappedKeyProto.WrappedKey wrappedKey)
+ throws InvalidKeyException, NoSuchAlgorithmException,
+ InvalidAlgorithmParameterException, NoSuchPaddingException {
+ if (wrappedKey.wrapAlgorithm != WrappedKeyProto.WrappedKey.AES_256_GCM) {
+ throw new InvalidKeyException(
+ String.format(
+ Locale.US,
+ "Could not unwrap key wrapped with %s algorithm",
+ wrappedKey.wrapAlgorithm));
+ }
+
+ if (wrappedKey.metadata == null) {
+ throw new InvalidKeyException("Metadata missing from wrapped tertiary key.");
+ }
+
+ if (wrappedKey.metadata.type != WrappedKeyProto.KeyMetadata.AES_256_GCM) {
+ throw new InvalidKeyException(
+ String.format(
+ Locale.US,
+ "Wrapped key was unexpected %s algorithm. Only support"
+ + " AES/GCM/NoPadding.",
+ wrappedKey.metadata.type));
+ }
+
+ Cipher cipher = getCipher();
+
+ cipher.init(
+ Cipher.UNWRAP_MODE,
+ secondaryKey,
+ new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.nonce));
+
+ return (SecretKey) cipher.unwrap(wrappedKey.key, KEY_ALGORITHM, Cipher.SECRET_KEY);
+ }
+
+ /**
+ * Wraps the tertiary key with the secondary key.
+ *
+ * @param secondaryKey The secondary key to use for wrapping.
+ * @param tertiaryKey The key to wrap.
+ * @return The wrapped key.
+ * @throws InvalidKeyException if the key is not good for wrapping.
+ * @throws IllegalBlockSizeException if there is an issue wrapping.
+ */
+ public static WrappedKeyProto.WrappedKey wrap(SecretKey secondaryKey, SecretKey tertiaryKey)
+ throws InvalidKeyException, IllegalBlockSizeException, NoSuchAlgorithmException,
+ NoSuchPaddingException {
+ Cipher cipher = getCipher();
+ cipher.init(Cipher.WRAP_MODE, secondaryKey);
+
+ WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
+ wrappedKey.key = cipher.wrap(tertiaryKey);
+ wrappedKey.nonce = cipher.getIV();
+ wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.AES_256_GCM;
+ wrappedKey.metadata = new WrappedKeyProto.KeyMetadata();
+ wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.AES_256_GCM;
+ return wrappedKey;
+ }
+
+ /**
+ * Rewraps a tertiary key with a new secondary key.
+ *
+ * @param oldSecondaryKey The old secondary key, used to unwrap the tertiary key.
+ * @param newSecondaryKey The new secondary key, used to rewrap the tertiary key.
+ * @param tertiaryKey The tertiary key, wrapped by {@code oldSecondaryKey}.
+ * @return The tertiary key, wrapped by {@code newSecondaryKey}.
+ * @throws InvalidKeyException if the key is not good for wrapping or unwrapping.
+ * @throws IllegalBlockSizeException if there is an issue wrapping.
+ */
+ public static WrappedKeyProto.WrappedKey rewrap(
+ SecretKey oldSecondaryKey,
+ SecretKey newSecondaryKey,
+ WrappedKeyProto.WrappedKey tertiaryKey)
+ throws InvalidKeyException, IllegalBlockSizeException,
+ InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+ NoSuchPaddingException {
+ return wrap(newSecondaryKey, unwrap(oldSecondaryKey, tertiaryKey));
+ }
+
+ private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
+ return Cipher.getInstance(AES_GCM_MODE);
+ }
+
+ // Statics only
+ private KeyWrapUtils() {}
+}
diff --git a/packages/BackupEncryption/test/robolectric/Android.bp b/packages/BackupEncryption/test/robolectric/Android.bp
index 6d1abbb..3376ec9 100644
--- a/packages/BackupEncryption/test/robolectric/Android.bp
+++ b/packages/BackupEncryption/test/robolectric/Android.bp
@@ -20,6 +20,7 @@
],
java_resource_dirs: ["config"],
libs: [
+ "backup-encryption-protos",
"platform-test-annotations",
"testng",
],
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
new file mode 100644
index 0000000..b607404
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.server.backup.encryption.keys;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.InvalidKeyException;
+
+import javax.crypto.SecretKey;
+
+/** Key wrapping tests */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class KeyWrapUtilsTest {
+ private static final int KEY_SIZE_BITS = 256;
+ private static final int BITS_PER_BYTE = 8;
+ private static final int GCM_NONCE_LENGTH_BYTES = 16;
+ private static final int GCM_TAG_LENGTH_BYTES = 16;
+
+ /** Test a wrapped key has metadata */
+ @Test
+ public void wrap_addsMetadata() throws Exception {
+ WrappedKeyProto.WrappedKey wrappedKey =
+ KeyWrapUtils.wrap(
+ /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+ assertThat(wrappedKey.metadata).isNotNull();
+ assertThat(wrappedKey.metadata.type).isEqualTo(WrappedKeyProto.KeyMetadata.AES_256_GCM);
+ }
+
+ /** Test a wrapped key has an algorithm specified */
+ @Test
+ public void wrap_addsWrapAlgorithm() throws Exception {
+ WrappedKeyProto.WrappedKey wrappedKey =
+ KeyWrapUtils.wrap(
+ /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+ assertThat(wrappedKey.wrapAlgorithm).isEqualTo(WrappedKeyProto.WrappedKey.AES_256_GCM);
+ }
+
+ /** Test a wrapped key haas an nonce of the right length */
+ @Test
+ public void wrap_addsNonceOfAppropriateLength() throws Exception {
+ WrappedKeyProto.WrappedKey wrappedKey =
+ KeyWrapUtils.wrap(
+ /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+ assertThat(wrappedKey.nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
+ }
+
+ /** Test a wrapped key has a key of the right length */
+ @Test
+ public void wrap_addsTagOfAppropriateLength() throws Exception {
+ WrappedKeyProto.WrappedKey wrappedKey =
+ KeyWrapUtils.wrap(
+ /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+ assertThat(wrappedKey.key).hasLength(KEY_SIZE_BITS / BITS_PER_BYTE + GCM_TAG_LENGTH_BYTES);
+ }
+
+ /** Ensure a key can be wrapped and unwrapped again */
+ @Test
+ public void unwrap_unwrapsEncryptedKey() throws Exception {
+ SecretKey secondaryKey = generateAesKey();
+ SecretKey tertiaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, tertiaryKey);
+ SecretKey unwrappedKey = KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+ assertThat(unwrappedKey).isEqualTo(tertiaryKey);
+ }
+
+ /** Ensure the unwrap method rejects keys with bad algorithms */
+ @Test(expected = InvalidKeyException.class)
+ public void unwrap_throwsForBadWrapAlgorithm() throws Exception {
+ SecretKey secondaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+ wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.UNKNOWN;
+
+ KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+ }
+
+ /** Ensure the unwrap method rejects metadata indicating the encryption type is unknown */
+ @Test(expected = InvalidKeyException.class)
+ public void unwrap_throwsForBadKeyAlgorithm() throws Exception {
+ SecretKey secondaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+ wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.UNKNOWN;
+
+ KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+ }
+
+ /** Ensure the unwrap method rejects wrapped keys missing the metadata */
+ @Test(expected = InvalidKeyException.class)
+ public void unwrap_throwsForMissingMetadata() throws Exception {
+ SecretKey secondaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+ wrappedKey.metadata = null;
+
+ KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+ }
+
+ /** Ensure unwrap rejects invalid secondary keys */
+ @Test(expected = InvalidKeyException.class)
+ public void unwrap_throwsForBadSecondaryKey() throws Exception {
+ WrappedKeyProto.WrappedKey wrappedKey =
+ KeyWrapUtils.wrap(
+ /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+
+ KeyWrapUtils.unwrap(generateAesKey(), wrappedKey);
+ }
+
+ /** Ensure rewrap can rewrap keys */
+ @Test
+ public void rewrap_canBeUnwrappedWithNewSecondaryKey() throws Exception {
+ SecretKey tertiaryKey = generateAesKey();
+ SecretKey oldSecondaryKey = generateAesKey();
+ SecretKey newSecondaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
+
+ WrappedKeyProto.WrappedKey wrappedWithNew =
+ KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
+
+ assertThat(KeyWrapUtils.unwrap(newSecondaryKey, wrappedWithNew)).isEqualTo(tertiaryKey);
+ }
+
+ /** Ensure rewrap doesn't create something decryptable by an old key */
+ @Test(expected = InvalidKeyException.class)
+ public void rewrap_cannotBeUnwrappedWithOldSecondaryKey() throws Exception {
+ SecretKey tertiaryKey = generateAesKey();
+ SecretKey oldSecondaryKey = generateAesKey();
+ SecretKey newSecondaryKey = generateAesKey();
+ WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
+
+ WrappedKeyProto.WrappedKey wrappedWithNew =
+ KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
+
+ KeyWrapUtils.unwrap(oldSecondaryKey, wrappedWithNew);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 55ff591..a2fa461 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -135,6 +135,7 @@
mEnableOk = true;
mOk.setEnabled(true);
+ mOk.setFilterTouchesWhenObscured(true);
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 84f5a04..98db7c8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -27,6 +27,7 @@
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.text.TextUtils;
+import android.util.EventLog;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -799,10 +800,9 @@
== BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
mDevice.getBluetoothClass().getDeviceClass()
== BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
- mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
- } else {
- mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
+ EventLog.writeEvent(0x534e4554, "138529441", -1, "");
}
+ mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index e4a8aab..720074b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -217,8 +217,6 @@
}
}
- private static KeyguardUpdateMonitor sInstance;
-
private final Context mContext;
private final boolean mIsPrimaryUser;
HashMap<Integer, SimData> mSimDatas = new HashMap<Integer, SimData>();
@@ -1389,15 +1387,6 @@
Trace.endSection();
}
-
- /** Provides access to the static instance. */
- public static KeyguardUpdateMonitor getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new KeyguardUpdateMonitor(context, Looper.getMainLooper());
- }
- return sInstance;
- }
-
protected void handleStartedGoingToSleep(int arg1) {
checkIsHandlerThread();
mLockIconPressed = false;
@@ -1830,7 +1819,10 @@
return shouldListen;
}
- private boolean shouldListenForFace() {
+ /**
+ * If face auth is allows to scan on this exact moment.
+ */
+ public boolean shouldListenForFace() {
final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep;
final int user = getCurrentUser();
final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 00bfb3f..bab64db 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -417,6 +417,9 @@
mDozeSensors.dump(pw);
}
+ /**
+ * @see DozeSensors.ProxSensor
+ */
private abstract class ProximityCheck implements SensorEventListener, Runnable {
private static final int TIMEOUT_DELAY_MS = 500;
@@ -428,6 +431,7 @@
private boolean mRegistered;
private boolean mFinished;
private float mMaxRange;
+ private boolean mUsingBrightnessSensor;
protected abstract void onProximityResult(int result);
@@ -435,6 +439,7 @@
Preconditions.checkState(!mFinished && !mRegistered);
Sensor sensor = DozeSensors.findSensorWithType(mSensorManager,
mContext.getString(R.string.doze_brightness_sensor_type));
+ mUsingBrightnessSensor = sensor != null;
if (sensor == null) {
sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
}
@@ -453,6 +458,9 @@
mRegistered = true;
}
+ /**
+ * @see DozeSensors.ProxSensor#onSensorChanged(SensorEvent)
+ */
@Override
public void onSensorChanged(SensorEvent event) {
if (event.values.length == 0) {
@@ -462,7 +470,14 @@
if (DozeMachine.DEBUG) {
Log.d(TAG, "ProxCheck: Event: value=" + event.values[0] + " max=" + mMaxRange);
}
- final boolean isNear = event.values[0] < mMaxRange;
+ final boolean isNear;
+ if (mUsingBrightnessSensor) {
+ // The custom brightness sensor is gated by the proximity sensor and will
+ // return 0 whenever prox is covered.
+ isNear = event.values[0] == 0;
+ } else {
+ isNear = event.values[0] < mMaxRange;
+ }
finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 3f598ff..c9c6a0c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -614,7 +614,7 @@
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
- mScreenshotHelper.takeScreenshot(1, true, true, mHandler);
+ mScreenshotHelper.takeScreenshot(1, true, true, mHandler, null);
MetricsLogger.action(mContext,
MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
index 1740290..3be3422 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
@@ -16,6 +16,9 @@
package com.android.systemui.pip.phone;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.IActivityManager;
@@ -182,7 +185,6 @@
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
mInputConsumerController = InputConsumerController.getPipInputConsumer();
- mInputConsumerController.registerInputConsumer();
mMediaController = new PipMediaController(context, mActivityManager);
mMenuController = new PipMenuActivityController(context, mActivityManager, mMediaController,
mInputConsumerController);
@@ -190,6 +192,18 @@
mMenuController, mInputConsumerController);
mAppOpsListener = new PipAppOpsListener(context, mActivityManager,
mTouchHandler.getMotionHelper());
+
+ // If SystemUI restart, and it already existed a pinned stack,
+ // register the pip input consumer to ensure touch can send to it.
+ try {
+ ActivityManager.StackInfo stackInfo = mActivityTaskManager.getStackInfo(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
+ if (stackInfo != null) {
+ mInputConsumerController.registerInputConsumer();
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
index 21f5812..4f2a6d8 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -48,7 +48,6 @@
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.graphics.Color;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -64,7 +63,6 @@
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityManager;
@@ -92,6 +90,7 @@
public static final int MESSAGE_UPDATE_ACTIONS = 4;
public static final int MESSAGE_UPDATE_DISMISS_FRACTION = 5;
public static final int MESSAGE_ANIMATION_ENDED = 6;
+ public static final int MESSAGE_TOUCH_EVENT = 7;
private static final int INITIAL_DISMISS_DELAY = 3500;
private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
@@ -128,10 +127,6 @@
}
};
- private PipTouchState mTouchState;
- private PointF mDownPosition = new PointF();
- private PointF mDownDelta = new PointF();
- private ViewConfiguration mViewConfig;
private Handler mHandler = new Handler();
private Messenger mToControllerMessenger;
private Messenger mMessenger = new Messenger(new Handler() {
@@ -169,6 +164,12 @@
mAllowTouches = true;
break;
}
+
+ case MESSAGE_TOUCH_EVENT: {
+ final MotionEvent ev = (MotionEvent) msg.obj;
+ dispatchTouchEvent(ev);
+ break;
+ }
}
}
});
@@ -184,15 +185,7 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
// Set the flags to allow us to watch for outside touches and also hide the menu and start
// manipulating the PIP in the same touch gesture
- mViewConfig = ViewConfiguration.get(this);
- mTouchState = new PipTouchState(mViewConfig, mHandler, () -> {
- if (mMenuState == MENU_STATE_CLOSE) {
- showPipMenu();
- } else {
- expandPip();
- }
- });
- getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_SLIPPERY);
+ getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
super.onCreate(savedInstanceState);
setContentView(R.layout.pip_menu_activity);
@@ -204,32 +197,6 @@
mViewRoot.setBackground(mBackgroundDrawable);
mMenuContainer = findViewById(R.id.menu_container);
mMenuContainer.setAlpha(0);
- mMenuContainer.setOnTouchListener((v, event) -> {
- mTouchState.onTouchEvent(event);
- switch (event.getAction()) {
- case MotionEvent.ACTION_UP:
- if (mTouchState.isDoubleTap() || mMenuState == MENU_STATE_FULL) {
- // Expand to fullscreen if this is a double tap or we are already expanded
- expandPip();
- } else if (!mTouchState.isWaitingForDoubleTap()) {
- // User has stalled long enough for this not to be a drag or a double tap,
- // just expand the menu if necessary
- if (mMenuState == MENU_STATE_CLOSE) {
- showPipMenu();
- }
- } else {
- // Next touch event _may_ be the second tap for the double-tap, schedule a
- // fallback runnable to trigger the menu if no touch event occurs before the
- // next tap
- mTouchState.scheduleDoubleTapTimeoutCallback();
- }
- // Fall through
- case MotionEvent.ACTION_CANCEL:
- mTouchState.reset();
- break;
- }
- return true;
- });
mSettingsButton = findViewById(R.id.settings);
mSettingsButton.setAlpha(0);
mSettingsButton.setOnClickListener((v) -> {
@@ -240,7 +207,11 @@
mDismissButton = findViewById(R.id.dismiss);
mDismissButton.setAlpha(0);
mDismissButton.setOnClickListener(v -> dismissPip());
- findViewById(R.id.expand_button).setOnClickListener(v -> expandPip());
+ findViewById(R.id.expand_button).setOnClickListener(v -> {
+ if (mMenuContainer.getAlpha() != 0) {
+ expandPip();
+ }
+ });
mActionsGroup = findViewById(R.id.actions_group);
mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
R.dimen.pip_between_action_padding_land);
@@ -298,27 +269,14 @@
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!mAllowTouches) {
- return super.dispatchTouchEvent(ev);
+ return false;
}
// On the first action outside the window, hide the menu
switch (ev.getAction()) {
case MotionEvent.ACTION_OUTSIDE:
hideMenu();
- break;
- case MotionEvent.ACTION_DOWN:
- mDownPosition.set(ev.getX(), ev.getY());
- mDownDelta.set(0f, 0f);
- break;
- case MotionEvent.ACTION_MOVE:
- mDownDelta.set(ev.getX() - mDownPosition.x, ev.getY() - mDownPosition.y);
- if (mDownDelta.length() > mViewConfig.getScaledTouchSlop()
- && mMenuState != MENU_STATE_NONE) {
- // Restore the input consumer and let that drive the movement of this menu
- notifyRegisterInputConsumer();
- cancelDelayedFinish();
- }
- break;
+ return true;
}
return super.dispatchTouchEvent(ev);
}
@@ -381,7 +339,6 @@
if (allowMenuTimeout) {
repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
}
- notifyUnregisterInputConsumer();
}
}
@@ -506,11 +463,13 @@
actionView.setContentDescription(action.getContentDescription());
if (action.isEnabled()) {
actionView.setOnClickListener(v -> {
- try {
- action.getActionIntent().send();
- } catch (CanceledException e) {
- Log.w(TAG, "Failed to send action", e);
- }
+ mHandler.post(() -> {
+ try {
+ action.getActionIntent().send();
+ } catch (CanceledException e) {
+ Log.w(TAG, "Failed to send action", e);
+ }
+ });
});
}
actionView.setEnabled(action.isEnabled());
@@ -554,18 +513,6 @@
mBackgroundDrawable.setAlpha(alpha);
}
- private void notifyRegisterInputConsumer() {
- Message m = Message.obtain();
- m.what = PipMenuActivityController.MESSAGE_REGISTER_INPUT_CONSUMER;
- sendMessage(m, "Could not notify controller to register input consumer");
- }
-
- private void notifyUnregisterInputConsumer() {
- Message m = Message.obtain();
- m.what = PipMenuActivityController.MESSAGE_UNREGISTER_INPUT_CONSUMER;
- sendMessage(m, "Could not notify controller to unregister input consumer");
- }
-
private void notifyMenuStateChange(int menuState) {
mMenuState = menuState;
Message m = Message.obtain();
@@ -583,11 +530,6 @@
}, false /* notifyMenuVisibility */, false /* isDismissing */);
}
- private void minimizePip() {
- sendEmptyMessage(PipMenuActivityController.MESSAGE_MINIMIZE_PIP,
- "Could not notify controller to minimize PIP");
- }
-
private void dismissPip() {
// Do not notify menu visibility when hiding the menu, the controller will do this when it
// handles the message
@@ -597,12 +539,6 @@
}, false /* notifyMenuVisibility */, true /* isDismissing */);
}
- private void showPipMenu() {
- Message m = Message.obtain();
- m.what = PipMenuActivityController.MESSAGE_SHOW_MENU;
- sendMessage(m, "Could not notify controller to show PIP menu");
- }
-
private void showSettings() {
final Pair<ComponentName, Integer> topPipActivityInfo =
PipUtils.getTopPinnedActivity(this, ActivityManager.getService());
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
index 14459d6..62c59e5 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
@@ -37,6 +37,7 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
+import android.view.MotionEvent;
import com.android.systemui.pip.phone.PipMediaController.ActionListener;
import com.android.systemui.shared.system.InputConsumerController;
@@ -156,14 +157,6 @@
mListeners.forEach(l -> l.onPipShowMenu());
break;
}
- case MESSAGE_REGISTER_INPUT_CONSUMER: {
- mInputConsumerController.registerInputConsumer();
- break;
- }
- case MESSAGE_UNREGISTER_INPUT_CONSUMER: {
- mInputConsumerController.unregisterInputConsumer();
- break;
- }
case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
mToActivityMessenger = msg.replyTo;
setStartActivityRequested(false);
@@ -212,15 +205,12 @@
}
public void onActivityPinned() {
- if (mMenuState == MENU_STATE_NONE) {
- // If the menu is not visible, then re-register the input consumer if it is not already
- // registered
- mInputConsumerController.registerInputConsumer();
- }
+ mInputConsumerController.registerInputConsumer();
}
public void onActivityUnpinned() {
hideMenu();
+ mInputConsumerController.unregisterInputConsumer();
setStartActivityRequested(false);
}
@@ -495,11 +485,7 @@
Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState
+ " menuState=" + menuState + " resize=" + resize);
}
- if (menuState == MENU_STATE_NONE) {
- mInputConsumerController.registerInputConsumer();
- } else {
- mInputConsumerController.unregisterInputConsumer();
- }
+
if (menuState != mMenuState) {
mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize));
if (menuState == MENU_STATE_FULL) {
@@ -521,6 +507,22 @@
mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0;
}
+ /**
+ * Handles touch event sent from pip input consumer.
+ */
+ void handleTouchEvent(MotionEvent ev) {
+ if (mToActivityMessenger != null) {
+ Message m = Message.obtain();
+ m.what = PipMenuActivity.MESSAGE_TOUCH_EVENT;
+ m.obj = ev;
+ try {
+ mToActivityMessenger.send(m);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not dispatch touch event", e);
+ }
+ }
+ }
+
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
index b05058a..30cf412 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -363,6 +363,8 @@
// Update the touch state
mTouchState.onTouchEvent(ev);
+ boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE;
+
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mMotionHelper.synchronizePinnedStackBounds();
@@ -378,6 +380,8 @@
break;
}
}
+
+ shouldDeliverToMenu = !mTouchState.isDragging();
break;
}
case MotionEvent.ACTION_UP: {
@@ -394,6 +398,7 @@
// Fall through to clean up
}
case MotionEvent.ACTION_CANCEL: {
+ shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging();
mTouchState.reset();
break;
}
@@ -425,7 +430,20 @@
break;
}
}
- return mMenuState == MENU_STATE_NONE;
+
+ // Deliver the event to PipMenuActivity to handle button click if the menu has shown.
+ if (shouldDeliverToMenu) {
+ final MotionEvent cloneEvent = MotionEvent.obtain(ev);
+ // Send the cancel event and cancel menu timeout if it starts to drag.
+ if (mTouchState.startedDragging()) {
+ cloneEvent.setAction(MotionEvent.ACTION_CANCEL);
+ mMenuController.pokeMenu();
+ }
+
+ mMenuController.handleTouchEvent(cloneEvent);
+ }
+
+ return true;
}
/**
@@ -741,11 +759,11 @@
mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */,
null /* animatorListener */);
setMinimizedStateInternal(false);
+ } else if (mTouchState.isDoubleTap()) {
+ // Expand to fullscreen if this is a double tap
+ mMotionHelper.expandPip();
} else if (mMenuState != MENU_STATE_FULL) {
- if (mTouchState.isDoubleTap()) {
- // Expand to fullscreen if this is a double tap
- mMotionHelper.expandPip();
- } else if (!mTouchState.isWaitingForDoubleTap()) {
+ if (!mTouchState.isWaitingForDoubleTap()) {
// User has stalled long enough for this not to be a drag or a double tap, just
// expand the menu
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
@@ -756,9 +774,6 @@
// next tap
mTouchState.scheduleDoubleTapTimeoutCallback();
}
- } else {
- mMenuController.hideMenu();
- mMotionHelper.expandPip();
}
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
index 69efbc8..e3f65ef 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
@@ -106,6 +106,7 @@
mIsDoubleTap = !mPreviouslyDragging &&
(mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
mIsWaitingForDoubleTap = false;
+ mIsDragging = false;
mLastDownTouchTime = mDownTouchTime;
if (mDoubleTapTimeoutCallback != null) {
mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index f91b03e..a79ecd9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -39,6 +39,7 @@
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Region;
+import android.hardware.biometrics.BiometricSourceType;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.DeviceConfig;
@@ -62,6 +63,7 @@
import com.android.keyguard.KeyguardClockSwitch;
import com.android.keyguard.KeyguardStatusView;
import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.DejankUtils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
@@ -172,6 +174,25 @@
R.id.keyguard_hun_animator_start_tag);
private static final AnimationProperties KEYGUARD_HUN_PROPERTIES =
new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ @VisibleForTesting
+ final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onBiometricRunningStateChanged(boolean running,
+ BiometricSourceType biometricSourceType) {
+ boolean keyguardOrShadeLocked = mBarState == StatusBarState.KEYGUARD
+ || mBarState == StatusBarState.SHADE_LOCKED;
+ if (!running && mFirstBypassAttempt && keyguardOrShadeLocked && !mDozing) {
+ mFirstBypassAttempt = false;
+ animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ }
+ }
+
+ @Override
+ public void onFinishedGoingToSleep(int why) {
+ mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
+ }
+ };
private final InjectionInflationController mInjectionInflationController;
private final PowerManager mPowerManager;
@@ -392,6 +413,7 @@
private boolean mShowingKeyguardHeadsUp;
private boolean mAllowExpandForSmallExpansion;
private Runnable mExpandAfterLayoutRunnable;
+ private boolean mFirstBypassAttempt;
private PluginManager mPluginManager;
private FrameLayout mPluginFrame;
@@ -427,6 +449,7 @@
mThemeResId = context.getThemeResId();
mKeyguardBypassController = bypassController;
mUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
+ mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
dynamicPrivacyController.addListener(this);
mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0);
@@ -519,6 +542,7 @@
Dependency.get(StatusBarStateController.class).addCallback(this);
Dependency.get(ZenModeController.class).addCallback(this);
Dependency.get(ConfigurationController.class).addCallback(this);
+ mUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
// Theme might have changed between inflating this view and attaching it to the window, so
// force a call to onThemeChanged
onThemeChanged();
@@ -531,6 +555,7 @@
Dependency.get(StatusBarStateController.class).removeCallback(this);
Dependency.get(ZenModeController.class).removeCallback(this);
Dependency.get(ConfigurationController.class).removeCallback(this);
+ mUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
}
@Override
@@ -2366,7 +2391,9 @@
* mKeyguardStatusBarAnimateAlpha;
newAlpha *= 1.0f - mKeyguardHeadsUpShowingAmount;
mKeyguardStatusBar.setAlpha(newAlpha);
- mKeyguardStatusBar.setVisibility(newAlpha != 0f && !mDozing ? VISIBLE : INVISIBLE);
+ boolean hideForBypass = mFirstBypassAttempt && mUpdateMonitor.shouldListenForFace();
+ mKeyguardStatusBar.setVisibility(newAlpha != 0f && !mDozing && !hideForBypass
+ ? VISIBLE : INVISIBLE);
}
private void updateKeyguardBottomAreaAlpha() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
index 86ab3a7..2f24494 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
@@ -21,19 +21,23 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.StatusBarManager;
+import android.hardware.biometrics.BiometricSourceType;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardStatusView;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.FalsingManager;
@@ -51,7 +55,6 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ZenModeController;
-import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.InjectionInflationController;
import org.junit.Before;
@@ -101,7 +104,11 @@
@Mock
private KeyguardAffordanceHelper mAffordanceHelper;
@Mock
+ private KeyguardUpdateMonitor mUpdateMonitor;
+ @Mock
private FalsingManager mFalsingManager;
+ @Mock
+ private KeyguardBypassController mKeyguardBypassController;
private NotificationPanelView mNotificationPanelView;
@Before
@@ -112,22 +119,21 @@
when(mHeadsUpCallback.getContext()).thenReturn(mContext);
mDependency.injectTestDependency(StatusBarStateController.class,
mStatusBarStateController);
+ mDependency.injectTestDependency(KeyguardUpdateMonitor.class, mUpdateMonitor);
mDependency.injectMockDependency(ShadeController.class);
mDependency.injectMockDependency(NotificationLockscreenUserManager.class);
mDependency.injectMockDependency(ConfigurationController.class);
mDependency.injectMockDependency(ZenModeController.class);
- KeyguardBypassController bypassController = new KeyguardBypassController(mContext,
- mock(TunerService.class), mStatusBarStateController,
- mock(NotificationLockscreenUserManager.class));
NotificationWakeUpCoordinator coordinator =
new NotificationWakeUpCoordinator(mContext,
mock(HeadsUpManagerPhone.class),
new StatusBarStateControllerImpl(),
- bypassController);
+ mKeyguardBypassController);
PulseExpansionHandler expansionHandler = new PulseExpansionHandler(mContext, coordinator,
- bypassController, mHeadsUpManager, mock(NotificationRoundnessManager.class));
+ mKeyguardBypassController, mHeadsUpManager,
+ mock(NotificationRoundnessManager.class));
mNotificationPanelView = new TestableNotificationPanelView(coordinator, expansionHandler,
- bypassController);
+ mKeyguardBypassController);
mNotificationPanelView.setHeadsUpManager(mHeadsUpManager);
mNotificationPanelView.setBar(mPanelBar);
@@ -187,6 +193,20 @@
assertThat(mNotificationPanelView.isTrackingBlocked()).isFalse();
}
+ @Test
+ public void testKeyguardStatusBarVisibility_hiddenForBypass() {
+ when(mUpdateMonitor.shouldListenForFace()).thenReturn(true);
+ mNotificationPanelView.mKeyguardUpdateCallback.onBiometricRunningStateChanged(true,
+ BiometricSourceType.FACE);
+ verify(mKeyguardStatusBar, never()).setVisibility(View.VISIBLE);
+
+ when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
+ mNotificationPanelView.mKeyguardUpdateCallback.onFinishedGoingToSleep(0);
+ mNotificationPanelView.mKeyguardUpdateCallback.onBiometricRunningStateChanged(true,
+ BiometricSourceType.FACE);
+ verify(mKeyguardStatusBar, never()).setVisibility(View.VISIBLE);
+ }
+
private class TestableNotificationPanelView extends NotificationPanelView {
TestableNotificationPanelView(NotificationWakeUpCoordinator coordinator,
PulseExpansionHandler expansionHandler,
diff --git a/services/accessibility/java/com/android/server/accessibility/GlobalActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/GlobalActionPerformer.java
index 672518c..b9b2654 100644
--- a/services/accessibility/java/com/android/server/accessibility/GlobalActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/GlobalActionPerformer.java
@@ -24,10 +24,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
-import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.SystemClock;
-import android.view.IWindowManager;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -191,7 +188,7 @@
ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
- true, true, new Handler(Looper.getMainLooper()));
+ true, true, new Handler(Looper.getMainLooper()), null);
return true;
}
}
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 5672a13..3fbb21e 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -865,8 +865,8 @@
MonitoredPackage p = it.next();
int oldState = p.getHealthCheckStateLocked();
int newState = p.handleElapsedTimeLocked(elapsedMs);
- if (oldState != MonitoredPackage.STATE_FAILED
- && newState == MonitoredPackage.STATE_FAILED) {
+ if (oldState != HealthCheckState.FAILED
+ && newState == HealthCheckState.FAILED) {
Slog.i(TAG, "Package " + p.mName + " failed health check");
failedPackages.add(p);
}
@@ -941,6 +941,23 @@
}
}
+ @Retention(SOURCE)
+ @IntDef(value = {
+ HealthCheckState.ACTIVE,
+ HealthCheckState.INACTIVE,
+ HealthCheckState.PASSED,
+ HealthCheckState.FAILED})
+ public @interface HealthCheckState {
+ // The package has not passed health check but has requested a health check
+ int ACTIVE = 0;
+ // The package has not passed health check and has not requested a health check
+ int INACTIVE = 1;
+ // The package has passed health check
+ int PASSED = 2;
+ // The package has failed health check
+ int FAILED = 3;
+ }
+
/**
* Represents a package and its health check state along with the time
* it should be monitored for.
@@ -949,23 +966,12 @@
* instances of this class.
*/
class MonitoredPackage {
- // Health check states
- // TODO(b/120598832): Prefix with HEALTH_CHECK
- // mName has not passed health check but has requested a health check
- public static final int STATE_ACTIVE = 0;
- // mName has not passed health check and has not requested a health check
- public static final int STATE_INACTIVE = 1;
- // mName has passed health check
- public static final int STATE_PASSED = 2;
- // mName has failed health check
- public static final int STATE_FAILED = 3;
-
//TODO(b/120598832): VersionedPackage?
private final String mName;
// One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after
// methods that could change the health check state: handleElapsedTimeLocked and
// tryPassHealthCheckLocked
- private int mHealthCheckState = STATE_INACTIVE;
+ private int mHealthCheckState = HealthCheckState.INACTIVE;
// Whether an explicit health check has passed.
// This value in addition with mHealthCheckDurationMs determines the health check state
// of the package, see #getHealthCheckStateLocked
@@ -1052,7 +1058,7 @@
+ ". Using total duration " + mDurationMs + "ms instead");
initialHealthCheckDurationMs = mDurationMs;
}
- if (mHealthCheckState == STATE_INACTIVE) {
+ if (mHealthCheckState == HealthCheckState.INACTIVE) {
// Transitions to ACTIVE
mHealthCheckDurationMs = initialHealthCheckDurationMs;
}
@@ -1072,7 +1078,7 @@
}
// Transitions to FAILED if now <= 0 and health check not passed
mDurationMs -= elapsedMs;
- if (mHealthCheckState == STATE_ACTIVE) {
+ if (mHealthCheckState == HealthCheckState.ACTIVE) {
// We only update health check durations if we have #setHealthCheckActiveLocked
// This ensures we don't leave the INACTIVE state for an unexpected elapsed time
// Transitions to FAILED if now <= 0 and health check not passed
@@ -1082,14 +1088,15 @@
}
/**
- * Marks the health check as passed and transitions to {@link #STATE_PASSED}
- * if not yet {@link #STATE_FAILED}.
+ * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED}
+ * if not yet {@link HealthCheckState.FAILED}.
*
- * @return the new health check state
+ * @return the new {@link HealthCheckState health check state}
*/
@GuardedBy("mLock")
+ @HealthCheckState
public int tryPassHealthCheckLocked() {
- if (mHealthCheckState != STATE_FAILED) {
+ if (mHealthCheckState != HealthCheckState.FAILED) {
// FAILED is a final state so only pass if we haven't failed
// Transition to PASSED
mHasPassedHealthCheck = true;
@@ -1102,12 +1109,11 @@
return mName;
}
- //TODO(b/120598832): IntDef
/**
- * Returns the current health check state, any of {@link #STATE_ACTIVE},
- * {@link #STATE_INACTIVE} or {@link #STATE_PASSED}
+ * Returns the current {@link HealthCheckState health check state}.
*/
@GuardedBy("mLock")
+ @HealthCheckState
public int getHealthCheckStateLocked() {
return mHealthCheckState;
}
@@ -1140,28 +1146,30 @@
*/
@GuardedBy("mLock")
public boolean isPendingHealthChecksLocked() {
- return mHealthCheckState == STATE_ACTIVE || mHealthCheckState == STATE_INACTIVE;
+ return mHealthCheckState == HealthCheckState.ACTIVE
+ || mHealthCheckState == HealthCheckState.INACTIVE;
}
/**
* Updates the health check state based on {@link #mHasPassedHealthCheck}
* and {@link #mHealthCheckDurationMs}.
*
- * @return the new health check state
+ * @return the new {@link HealthCheckState health check state}
*/
@GuardedBy("mLock")
+ @HealthCheckState
private int updateHealthCheckStateLocked() {
int oldState = mHealthCheckState;
if (mHasPassedHealthCheck) {
// Set final state first to avoid ambiguity
- mHealthCheckState = STATE_PASSED;
+ mHealthCheckState = HealthCheckState.PASSED;
} else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) {
// Set final state first to avoid ambiguity
- mHealthCheckState = STATE_FAILED;
+ mHealthCheckState = HealthCheckState.FAILED;
} else if (mHealthCheckDurationMs == Long.MAX_VALUE) {
- mHealthCheckState = STATE_INACTIVE;
+ mHealthCheckState = HealthCheckState.INACTIVE;
} else {
- mHealthCheckState = STATE_ACTIVE;
+ mHealthCheckState = HealthCheckState.ACTIVE;
}
Slog.i(TAG, "Updated health check state for package " + mName + ": "
+ toString(oldState) + " -> " + toString(mHealthCheckState));
@@ -1169,15 +1177,15 @@
}
/** Returns a {@link String} representation of the current health check state. */
- private String toString(int state) {
+ private String toString(@HealthCheckState int state) {
switch (state) {
- case STATE_ACTIVE:
+ case HealthCheckState.ACTIVE:
return "ACTIVE";
- case STATE_INACTIVE:
+ case HealthCheckState.INACTIVE:
return "INACTIVE";
- case STATE_PASSED:
+ case HealthCheckState.PASSED:
return "PASSED";
- case STATE_FAILED:
+ case HealthCheckState.FAILED:
return "FAILED";
default:
return "UNKNOWN";
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index f7e825e..eb2723a 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -79,7 +79,6 @@
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
-import java.util.stream.Collectors;
/**
* Since phone process can be restarted, this class provides a centralized place
@@ -849,10 +848,7 @@
}
}
if ((events & PhoneStateListener
- .LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE) != 0
- && TelephonyPermissions.checkReadPhoneStateOnAnyActiveSub(
- r.context, r.callerPid, r.callerUid, r.callingPackage,
- "listen_active_data_subid_change")) {
+ .LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE) != 0) {
try {
r.callback.onActiveDataSubIdChanged(mActiveDataSubId);
} catch (RemoteException ex) {
@@ -1829,23 +1825,11 @@
log("notifyActiveDataSubIdChanged: activeDataSubId=" + activeDataSubId);
}
- // Create a copy to prevent the IPC call while checking carrier privilege under the lock.
- List<Record> copiedRecords;
- synchronized (mRecords) {
- copiedRecords = new ArrayList<>(mRecords);
- }
mActiveDataSubId = activeDataSubId;
-
- // Filter the record that does not listen to this change or does not have the permission.
- copiedRecords = copiedRecords.stream().filter(r -> r.matchPhoneStateListenerEvent(
- PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE)
- && TelephonyPermissions.checkReadPhoneStateOnAnyActiveSub(
- mContext, r.callerPid, r.callerUid, r.callingPackage,
- "notifyActiveDataSubIdChanged")).collect(Collectors.toCollection(ArrayList::new));
-
synchronized (mRecords) {
- for (Record r : copiedRecords) {
- if (mRecords.contains(r)) {
+ for (Record r : mRecords) {
+ if (r.matchPhoneStateListenerEvent(
+ PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE)) {
try {
r.callback.onActiveDataSubIdChanged(activeDataSubId);
} catch (RemoteException ex) {
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 36e872a..4a62bc5 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -55,6 +55,7 @@
import android.os.ShellCallback;
import android.os.UserHandle;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -64,6 +65,8 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BinderDeathDispatcher;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -89,6 +92,14 @@
/** Do a WTF if a single observer is registered more than this times. */
private static final int TOO_MANY_OBSERVERS_THRESHOLD = 1000;
+ /**
+ * Delay to apply to content change notifications dispatched to apps running
+ * in the background. This is used to help prevent stampeding when the user
+ * is performing CPU/RAM intensive foreground tasks, such as when capturing
+ * burst photos.
+ */
+ private static final long BACKGROUND_OBSERVER_DELAY = 10 * DateUtils.SECOND_IN_MILLIS;
+
public static class Lifecycle extends SystemService {
private ContentService mService;
@@ -426,28 +437,15 @@
flags, userHandle, calls);
}
final int numCalls = calls.size();
- for (int i=0; i<numCalls; i++) {
- ObserverCall oc = calls.get(i);
- try {
- oc.mObserver.onChange(oc.mSelfChange, uri, userHandle);
- if (DEBUG) Slog.d(TAG, "Notified " + oc.mObserver + " of " + "update at "
- + uri);
- } catch (RemoteException ex) {
- synchronized (mRootNode) {
- Log.w(TAG, "Found dead observer, removing");
- IBinder binder = oc.mObserver.asBinder();
- final ArrayList<ObserverNode.ObserverEntry> list
- = oc.mNode.mObservers;
- int numList = list.size();
- for (int j=0; j<numList; j++) {
- ObserverNode.ObserverEntry oe = list.get(j);
- if (oe.observer.asBinder() == binder) {
- list.remove(j);
- j--;
- numList--;
- }
- }
- }
+ for (int i = 0; i < numCalls; i++) {
+ // Immediately dispatch notifications to foreground apps that
+ // are important to the user; all other background observers are
+ // delayed to avoid stampeding
+ final ObserverCall oc = calls.get(i);
+ if (oc.mProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
+ oc.run();
+ } else {
+ BackgroundThread.getHandler().postDelayed(oc, BACKGROUND_OBSERVER_DELAY);
}
}
if ((flags&ContentResolver.NOTIFY_SYNC_TO_NETWORK) != 0) {
@@ -486,23 +484,33 @@
UserHandle.getCallingUserId(), Build.VERSION_CODES.CUR_DEVELOPMENT, callingPackage);
}
- /**
- * Hide this class since it is not part of api,
- * but current unittest framework requires it to be public
- * @hide
- *
- */
- public static final class ObserverCall {
- final ObserverNode mNode;
+ /** {@hide} */
+ @VisibleForTesting
+ public static final class ObserverCall implements Runnable {
final IContentObserver mObserver;
final boolean mSelfChange;
- final int mObserverUserId;
+ final Uri mUri;
+ final int mUserId;
+ final int mProcState;
- ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange, int observerUserId) {
- mNode = node;
+ ObserverCall(IContentObserver observer, boolean selfChange, Uri uri, int userId,
+ int procState) {
mObserver = observer;
mSelfChange = selfChange;
- mObserverUserId = observerUserId;
+ mUri = uri;
+ mUserId = userId;
+ mProcState = procState;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mObserver.onChange(mSelfChange, mUri, mUserId);
+ if (DEBUG) Slog.d(TAG, "Notified " + mObserver + " of update at " + mUri);
+ } catch (RemoteException ignored) {
+ // We already have a death observer that will clean up if the
+ // remote process dies
+ }
}
}
@@ -1345,11 +1353,8 @@
return ContentResolver.SYNC_EXEMPTION_NONE;
}
- /**
- * Hide this class since it is not part of api,
- * but current unittest framework requires it to be public
- * @hide
- */
+ /** {@hide} */
+ @VisibleForTesting
public static final class ObserverNode {
private class ObserverEntry implements IBinder.DeathRecipient {
public final IContentObserver observer;
@@ -1546,7 +1551,7 @@
return false;
}
- private void collectMyObserversLocked(boolean leaf, IContentObserver observer,
+ private void collectMyObserversLocked(Uri uri, boolean leaf, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags,
int targetUserHandle, ArrayList<ObserverCall> calls) {
int N = mObservers.size();
@@ -1588,8 +1593,10 @@
if (DEBUG) Slog.d(TAG, "Reporting to " + entry.observer + ": leaf=" + leaf
+ " flags=" + Integer.toHexString(flags)
+ " desc=" + entry.notifyForDescendants);
- calls.add(new ObserverCall(this, entry.observer, selfChange,
- UserHandle.getUserId(entry.uid)));
+ final int procState = LocalServices.getService(ActivityManagerInternal.class)
+ .getUidProcessState(entry.uid);
+ calls.add(new ObserverCall(entry.observer, selfChange, uri,
+ targetUserHandle, procState));
}
}
}
@@ -1605,14 +1612,14 @@
if (index >= segmentCount) {
// This is the leaf node, notify all observers
if (DEBUG) Slog.d(TAG, "Collecting leaf observers @ #" + index + ", node " + mName);
- collectMyObserversLocked(true, observer, observerWantsSelfNotifications,
+ collectMyObserversLocked(uri, true, observer, observerWantsSelfNotifications,
flags, targetUserHandle, calls);
} else if (index < segmentCount){
segment = getUriSegment(uri, index);
if (DEBUG) Slog.d(TAG, "Collecting non-leaf observers @ #" + index + " / "
+ segment);
// Notify any observers at this level who are interested in descendants
- collectMyObserversLocked(false, observer, observerWantsSelfNotifications,
+ collectMyObserversLocked(uri, false, observer, observerWantsSelfNotifications,
flags, targetUserHandle, calls);
}
diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java
index 0d5746b..6769fe0 100644
--- a/services/core/java/com/android/server/rollback/Rollback.java
+++ b/services/core/java/com/android/server/rollback/Rollback.java
@@ -65,7 +65,7 @@
/**
* The directory where the rollback data is stored.
*/
- public final File backupDir;
+ private final File mBackupDir;
/**
* The time when the upgrade occurred, for purposes of expiring
@@ -74,24 +74,24 @@
* The timestamp is not applicable for all rollback states, but we make
* sure to keep it non-null to avoid potential errors there.
*/
- public @NonNull Instant timestamp;
+ private @NonNull Instant mTimestamp;
/**
* The session ID for the staged session if this rollback data represents a staged session,
* {@code -1} otherwise.
*/
- public final int stagedSessionId;
+ private final int mStagedSessionId;
/**
* The current state of the rollback.
* ENABLING, AVAILABLE, or COMMITTED.
*/
- public @RollbackState int state;
+ private @RollbackState int mState;
/**
* The id of the post-reboot apk session for a staged install, if any.
*/
- public int apkSessionId = -1;
+ private int mApkSessionId = -1;
/**
* True if we are expecting the package manager to call restoreUserData
@@ -99,7 +99,7 @@
* has not yet been fully applied.
*/
// NOTE: All accesses to this field are from the RollbackManager handler thread.
- public boolean restoreUserDataInProgress = false;
+ private boolean mRestoreUserDataInProgress = false;
/**
* Constructs a new, empty Rollback instance.
@@ -114,10 +114,10 @@
/* isStaged */ stagedSessionId != -1,
/* causePackages */ new ArrayList<>(),
/* committedSessionId */ -1);
- this.backupDir = backupDir;
- this.stagedSessionId = stagedSessionId;
- this.state = ROLLBACK_STATE_ENABLING;
- this.timestamp = Instant.now();
+ mBackupDir = backupDir;
+ mStagedSessionId = stagedSessionId;
+ mState = ROLLBACK_STATE_ENABLING;
+ mTimestamp = Instant.now();
}
/**
@@ -126,21 +126,115 @@
Rollback(RollbackInfo info, File backupDir, Instant timestamp, int stagedSessionId,
@RollbackState int state, int apkSessionId, boolean restoreUserDataInProgress) {
this.info = info;
- this.backupDir = backupDir;
- this.timestamp = timestamp;
- this.stagedSessionId = stagedSessionId;
- this.state = state;
- this.apkSessionId = apkSessionId;
- this.restoreUserDataInProgress = restoreUserDataInProgress;
+ mBackupDir = backupDir;
+ mTimestamp = timestamp;
+ mStagedSessionId = stagedSessionId;
+ mState = state;
+ mApkSessionId = apkSessionId;
+ mRestoreUserDataInProgress = restoreUserDataInProgress;
}
/**
* Whether the rollback is for rollback of a staged install.
*/
- public boolean isStaged() {
+ boolean isStaged() {
return info.isStaged();
}
+ /**
+ * Returns the directory in which rollback data should be stored.
+ */
+ File getBackupDir() {
+ return mBackupDir;
+ }
+
+ /**
+ * Returns the time when the upgrade occurred, for purposes of expiring rollback data.
+ */
+ Instant getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * Sets the time at which upgrade occurred.
+ */
+ void setTimestamp(Instant timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * Returns the session ID for the staged session if this rollback data represents a staged
+ * session, or {@code -1} otherwise.
+ */
+ int getStagedSessionId() {
+ return mStagedSessionId;
+ }
+
+ /**
+ * Returns true if the rollback is in the ENABLING state.
+ */
+ boolean isEnabling() {
+ return mState == ROLLBACK_STATE_ENABLING;
+ }
+
+ /**
+ * Returns true if the rollback is in the AVAILABLE state.
+ */
+ boolean isAvailable() {
+ return mState == ROLLBACK_STATE_AVAILABLE;
+ }
+
+ /**
+ * Returns true if the rollback is in the COMMITTED state.
+ */
+ boolean isCommitted() {
+ return mState == ROLLBACK_STATE_COMMITTED;
+ }
+
+ /**
+ * Sets the state of the rollback to AVAILABLE.
+ */
+ void setAvailable() {
+ mState = ROLLBACK_STATE_AVAILABLE;
+ }
+
+ /**
+ * Sets the state of the rollback to COMMITTED.
+ */
+ void setCommitted() {
+ mState = ROLLBACK_STATE_COMMITTED;
+ }
+
+ /**
+ * Returns the id of the post-reboot apk session for a staged install, if any.
+ */
+ int getApkSessionId() {
+ return mApkSessionId;
+ }
+
+ /**
+ * Sets the id of the post-reboot apk session for a staged install.
+ */
+ void setApkSessionId(int apkSessionId) {
+ mApkSessionId = apkSessionId;
+ }
+
+ /**
+ * Returns true if we are expecting the package manager to call restoreUserData for this
+ * rollback because it has just been committed but the rollback has not yet been fully applied.
+ */
+ boolean isRestoreUserDataInProgress() {
+ return mRestoreUserDataInProgress;
+ }
+
+ /**
+ * Sets whether we are expecting the package manager to call restoreUserData for this
+ * rollback because it has just been committed but the rollback has not yet been fully applied.
+ */
+ void setRestoreUserDataInProgress(boolean restoreUserDataInProgress) {
+ mRestoreUserDataInProgress = restoreUserDataInProgress;
+ }
+
static String rollbackStateToString(@RollbackState int state) {
switch (state) {
case Rollback.ROLLBACK_STATE_ENABLING: return "enabling";
@@ -160,7 +254,7 @@
throw new ParseException("Invalid rollback state: " + state, 0);
}
- public String getStateAsString() {
- return rollbackStateToString(state);
+ String getStateAsString() {
+ return rollbackStateToString(mState);
}
}
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 3147bc6..96d284b 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -282,7 +282,7 @@
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.state == Rollback.ROLLBACK_STATE_AVAILABLE) {
+ if (rollback.isAvailable()) {
rollbacks.add(rollback.info);
}
}
@@ -298,7 +298,7 @@
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.state == Rollback.ROLLBACK_STATE_COMMITTED) {
+ if (rollback.isCommitted()) {
rollbacks.add(rollback.info);
}
}
@@ -332,7 +332,7 @@
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- rollback.timestamp = rollback.timestamp.plusMillis(timeDifference);
+ rollback.setTimestamp(rollback.getTimestamp().plusMillis(timeDifference));
saveRollback(rollback);
}
}
@@ -358,7 +358,7 @@
Slog.i(TAG, "Initiating rollback");
Rollback rollback = getRollbackForId(rollbackId);
- if (rollback == null || rollback.state != Rollback.ROLLBACK_STATE_AVAILABLE) {
+ if (rollback == null || !rollback.isAvailable()) {
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
"Rollback unavailable");
return;
@@ -454,8 +454,8 @@
// TODO: Could this cause a rollback to be
// resurrected if it should otherwise have
// expired by now?
- rollback.state = Rollback.ROLLBACK_STATE_AVAILABLE;
- rollback.restoreUserDataInProgress = false;
+ rollback.setAvailable();
+ rollback.setRestoreUserDataInProgress(false);
}
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_INSTALL,
"Rollback downgrade install failed: "
@@ -468,7 +468,7 @@
if (!rollback.isStaged()) {
// All calls to restoreUserData should have
// completed by now for a non-staged install.
- rollback.restoreUserDataInProgress = false;
+ rollback.setRestoreUserDataInProgress(false);
}
rollback.info.setCommittedSessionId(parentSessionId);
@@ -490,8 +490,8 @@
);
synchronized (mLock) {
- rollback.state = Rollback.ROLLBACK_STATE_COMMITTED;
- rollback.restoreUserDataInProgress = true;
+ rollback.setCommitted();
+ rollback.setRestoreUserDataInProgress(true);
}
parentSession.commit(receiver.getIntentSender());
} catch (IOException e) {
@@ -618,9 +618,9 @@
synchronized (mLock) {
for (Rollback rollback : mRollbacks) {
if (rollback.isStaged()) {
- if (rollback.state == Rollback.ROLLBACK_STATE_ENABLING) {
+ if (rollback.isEnabling()) {
enabling.add(rollback);
- } else if (rollback.restoreUserDataInProgress) {
+ } else if (rollback.isRestoreUserDataInProgress()) {
restoreInProgress.add(rollback);
}
@@ -635,8 +635,8 @@
for (Rollback rollback : enabling) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo session = installer.getSessionInfo(
- rollback.stagedSessionId);
+ PackageInstaller.SessionInfo session =
+ installer.getSessionInfo(rollback.getStagedSessionId());
if (session == null || session.isStagedSessionFailed()) {
// TODO: Do we need to remove this from
// mRollbacks, or is it okay to leave as
@@ -650,13 +650,13 @@
for (Rollback rollback : restoreInProgress) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo session = installer.getSessionInfo(
- rollback.stagedSessionId);
+ PackageInstaller.SessionInfo session =
+ installer.getSessionInfo(rollback.getStagedSessionId());
// TODO: What if session is null?
if (session != null) {
if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) {
synchronized (mLock) {
- rollback.restoreUserDataInProgress = false;
+ rollback.setRestoreUserDataInProgress(false);
}
saveRollback(rollback);
}
@@ -694,8 +694,7 @@
while (iter.hasNext()) {
Rollback rollback = iter.next();
// TODO: Should we remove rollbacks in the ENABLING state here?
- if (rollback.state == Rollback.ROLLBACK_STATE_AVAILABLE
- || rollback.state == Rollback.ROLLBACK_STATE_ENABLING) {
+ if (rollback.isEnabling() || rollback.isAvailable()) {
for (PackageRollbackInfo info : rollback.info.getPackages()) {
if (info.getPackageName().equals(packageName)
&& !packageVersionsEqual(
@@ -761,15 +760,16 @@
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- if (rollback.state != Rollback.ROLLBACK_STATE_AVAILABLE) {
+ if (!rollback.isAvailable()) {
continue;
}
if (!now.isBefore(
- rollback.timestamp.plusMillis(mRollbackLifetimeDurationInMillis))) {
+ rollback.getTimestamp()
+ .plusMillis(mRollbackLifetimeDurationInMillis))) {
iter.remove();
deleteRollback(rollback);
- } else if (oldest == null || oldest.isAfter(rollback.timestamp)) {
- oldest = rollback.timestamp;
+ } else if (oldest == null || oldest.isAfter(rollback.getTimestamp())) {
+ oldest = rollback.getTimestamp();
}
}
}
@@ -877,7 +877,7 @@
synchronized (mLock) {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.apkSessionId == parentSession.getSessionId()) {
+ if (rollback.getApkSessionId() == parentSession.getSessionId()) {
// This is the apk session for a staged session with rollback enabled. We do not
// need to create a new rollback for this session.
return true;
@@ -1020,7 +1020,7 @@
// staged installs
for (int i = 0; i < mRollbacks.size(); i++) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.state != Rollback.ROLLBACK_STATE_ENABLING) {
+ if (!rollback.isEnabling()) {
continue;
}
@@ -1053,7 +1053,7 @@
synchronized (mLock) {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback candidate = mRollbacks.get(i);
- if (candidate.restoreUserDataInProgress) {
+ if (candidate.isRestoreUserDataInProgress()) {
info = getPackageRollbackInfo(candidate, packageName);
if (info != null) {
rollback = candidate;
@@ -1146,8 +1146,8 @@
synchronized (mLock) {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback candidate = mRollbacks.get(i);
- if (candidate.stagedSessionId == originalSessionId) {
- candidate.apkSessionId = apkSessionId;
+ if (candidate.getStagedSessionId() == originalSessionId) {
+ candidate.setApkSessionId(apkSessionId);
rollback = candidate;
break;
}
@@ -1333,8 +1333,8 @@
// to a new package being installed. Won't this revive an expired
// rollback? Consider adding a ROLLBACK_STATE_EXPIRED to address this.
synchronized (mLock) {
- rollback.state = Rollback.ROLLBACK_STATE_AVAILABLE;
- rollback.timestamp = Instant.now();
+ rollback.setAvailable();
+ rollback.setTimestamp(Instant.now());
}
saveRollback(rollback);
@@ -1434,9 +1434,9 @@
ipw.println(info.getRollbackId() + ":");
ipw.increaseIndent();
ipw.println("-state: " + rollback.getStateAsString());
- ipw.println("-timestamp: " + rollback.timestamp);
- if (rollback.stagedSessionId != -1) {
- ipw.println("-stagedSessionId: " + rollback.stagedSessionId);
+ ipw.println("-timestamp: " + rollback.getTimestamp());
+ if (rollback.getStagedSessionId() != -1) {
+ ipw.println("-stagedSessionId: " + rollback.getStagedSessionId());
}
ipw.println("-packages:");
ipw.increaseIndent();
@@ -1446,7 +1446,7 @@
+ " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
}
ipw.decreaseIndent();
- if (rollback.state == Rollback.ROLLBACK_STATE_COMMITTED) {
+ if (rollback.isCommitted()) {
ipw.println("-causePackages:");
ipw.increaseIndent();
for (VersionedPackage cPkg : info.getCausePackages()) {
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index b2448f6..772c53f 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -17,7 +17,6 @@
package com.android.server.rollback;
import static com.android.server.rollback.Rollback.rollbackStateFromString;
-import static com.android.server.rollback.Rollback.rollbackStateToString;
import android.annotation.NonNull;
import android.content.pm.VersionedPackage;
@@ -216,7 +215,7 @@
static void backupPackageCodePath(Rollback rollback, String packageName, String codePath)
throws IOException {
File sourceFile = new File(codePath);
- File targetDir = new File(rollback.backupDir, packageName);
+ File targetDir = new File(rollback.getBackupDir(), packageName);
targetDir.mkdirs();
File targetFile = new File(targetDir, sourceFile.getName());
@@ -229,7 +228,7 @@
* Includes the base apk and any splits. Returns null if none found.
*/
static File[] getPackageCodePaths(Rollback rollback, String packageName) {
- File targetDir = new File(rollback.backupDir, packageName);
+ File targetDir = new File(rollback.getBackupDir(), packageName);
File[] files = targetDir.listFiles();
if (files == null || files.length == 0) {
return null;
@@ -243,7 +242,7 @@
*/
static void deletePackageCodePaths(Rollback rollback) {
for (PackageRollbackInfo info : rollback.info.getPackages()) {
- File targetDir = new File(rollback.backupDir, info.getPackageName());
+ File targetDir = new File(rollback.getBackupDir(), info.getPackageName());
removeFile(targetDir);
}
}
@@ -255,13 +254,13 @@
try {
JSONObject dataJson = new JSONObject();
dataJson.put("info", rollbackInfoToJson(rollback.info));
- dataJson.put("timestamp", rollback.timestamp.toString());
- dataJson.put("stagedSessionId", rollback.stagedSessionId);
- dataJson.put("state", rollbackStateToString(rollback.state));
- dataJson.put("apkSessionId", rollback.apkSessionId);
- dataJson.put("restoreUserDataInProgress", rollback.restoreUserDataInProgress);
+ dataJson.put("timestamp", rollback.getTimestamp().toString());
+ dataJson.put("stagedSessionId", rollback.getStagedSessionId());
+ dataJson.put("state", rollback.getStateAsString());
+ dataJson.put("apkSessionId", rollback.getApkSessionId());
+ dataJson.put("restoreUserDataInProgress", rollback.isRestoreUserDataInProgress());
- PrintWriter pw = new PrintWriter(new File(rollback.backupDir, "rollback.json"));
+ PrintWriter pw = new PrintWriter(new File(rollback.getBackupDir(), "rollback.json"));
pw.println(dataJson.toString());
pw.close();
} catch (JSONException e) {
@@ -273,7 +272,7 @@
* Removes all persistent storage associated with the given rollback.
*/
void deleteRollback(Rollback rollback) {
- removeFile(rollback.backupDir);
+ removeFile(rollback.getBackupDir());
}
/**
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index aecbca3..b502bd5 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -3551,7 +3551,8 @@
if (mScreenshotHelper != null) {
mScreenshotHelper.takeScreenshot(screenshotType,
mStatusBar != null && mStatusBar.isVisibleLw(),
- mNavigationBar != null && mNavigationBar.isVisibleLw(), mHandler);
+ mNavigationBar != null && mNavigationBar.isVisibleLw(),
+ mHandler, null /* completionConsumer */);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/GlobalActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/GlobalActionPerformerTest.java
index e72e460..c73be6f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/GlobalActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/GlobalActionPerformerTest.java
@@ -35,6 +35,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.function.Consumer;
+
/**
* Tests for GlobalActionPerformer
*/
@@ -84,6 +86,6 @@
AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
verify(mMockScreenshotHelper).takeScreenshot(
eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(),
- anyBoolean(), any(Handler.class));
+ anyBoolean(), any(Handler.class), any());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java
new file mode 100644
index 0000000..d27f1c7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.server.rollback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class RollbackUnitTest {
+
+ @Test
+ public void newEmptyStagedRollbackDefaults() {
+ int rollbackId = 123;
+ int sessionId = 567;
+ File file = new File("/test/testing");
+
+ Rollback rollback = new Rollback(rollbackId, file, sessionId);
+
+ assertThat(rollback.isEnabling()).isTrue();
+ assertThat(rollback.getBackupDir().getAbsolutePath()).isEqualTo("/test/testing");
+ assertThat(rollback.isStaged()).isTrue();
+ assertThat(rollback.getStagedSessionId()).isEqualTo(567);
+ }
+
+ @Test
+ public void newEmptyNonStagedRollbackDefaults() {
+ int rollbackId = 123;
+ File file = new File("/test/testing");
+
+ Rollback rollback = new Rollback(rollbackId, file, -1);
+
+ assertThat(rollback.isEnabling()).isTrue();
+ assertThat(rollback.getBackupDir().getAbsolutePath()).isEqualTo("/test/testing");
+ assertThat(rollback.isStaged()).isFalse();
+ }
+
+ @Test
+ public void rollbackStateChanges() {
+ Rollback rollback = new Rollback(123, new File("/test/testing"), -1);
+
+ assertThat(rollback.isEnabling()).isTrue();
+ assertThat(rollback.isAvailable()).isFalse();
+ assertThat(rollback.isCommitted()).isFalse();
+
+ rollback.setAvailable();
+
+ assertThat(rollback.isEnabling()).isFalse();
+ assertThat(rollback.isAvailable()).isTrue();
+ assertThat(rollback.isCommitted()).isFalse();
+
+ rollback.setCommitted();
+
+ assertThat(rollback.isEnabling()).isFalse();
+ assertThat(rollback.isAvailable()).isFalse();
+ assertThat(rollback.isCommitted()).isTrue();
+ }
+
+}
diff --git a/telephony/java/android/telephony/PhoneStateListener.java b/telephony/java/android/telephony/PhoneStateListener.java
index 271195b..6a3c06e 100644
--- a/telephony/java/android/telephony/PhoneStateListener.java
+++ b/telephony/java/android/telephony/PhoneStateListener.java
@@ -33,13 +33,13 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IPhoneStateListener;
+import dalvik.system.VMRuntime;
+
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
-import dalvik.system.VMRuntime;
-
/**
* A listener class for monitoring changes in specific telephony states
* on the device, including service state, signal strength, message
@@ -301,11 +301,6 @@
* it could be the current active opportunistic subscription in use, or the
* subscription user selected as default data subscription in DSDS mode.
*
- * Requires Permission: No permission is required to listen, but notification requires
- * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} or the calling
- * app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges})
- * on any active subscription.
- *
* @see #onActiveDataSubscriptionIdChanged
*/
public static final int LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE = 0x00400000;
diff --git a/tests/BootImageProfileTest/Android.bp b/tests/BootImageProfileTest/Android.bp
new file mode 100644
index 0000000..1b097a8
--- /dev/null
+++ b/tests/BootImageProfileTest/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+java_test_host {
+ name: "BootImageProfileTest",
+ srcs: ["src/**/*.java"],
+ libs: ["tradefed"],
+ test_suites: ["general-tests"],
+}
diff --git a/tests/BootImageProfileTest/AndroidTest.xml b/tests/BootImageProfileTest/AndroidTest.xml
new file mode 100644
index 0000000..c132007
--- /dev/null
+++ b/tests/BootImageProfileTest/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for BootImageProfileTest">
+ <!-- do not use DeviceSetup#set-property because it reboots the device b/136200738.
+ furthermore the changes in /data/local.prop don't actually seem to get picked up.
+ -->
+ <target_preparer
+ class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- we need this magic flag, otherwise it always reboots and breaks the selinux -->
+ <option name="force-skip-system-props" value="true" />
+
+ <option name="run-command" value="setprop dalvik.vm.profilesystemserver true" />
+ <option name="run-command" value="setprop dalvik.vm.profilebootclasspath true" />
+
+ <!-- Profiling does not pick up the above changes we restart the shell -->
+ <option name="run-command" value="stop" />
+ <option name="run-command" value="start" />
+
+ <!-- give it some time to restart the shell; otherwise the first unit test might fail -->
+ <option name="run-command" value="sleep 2" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="class" value="com.android.bootimageprofile.BootImageProfileTest" />
+ </test>
+</configuration>
diff --git a/tests/BootImageProfileTest/TEST_MAPPING b/tests/BootImageProfileTest/TEST_MAPPING
new file mode 100644
index 0000000..1b569f9
--- /dev/null
+++ b/tests/BootImageProfileTest/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "BootImageProfileTest"
+ }
+ ]
+}
diff --git a/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java b/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java
new file mode 100644
index 0000000..17986a3
--- /dev/null
+++ b/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.bootimageprofile;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.IDeviceTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class BootImageProfileTest implements IDeviceTest {
+ private ITestDevice mTestDevice;
+ private static final String SYSTEM_SERVER_PROFILE =
+ "/data/misc/profiles/cur/0/android/primary.prof";
+
+ @Override
+ public void setDevice(ITestDevice testDevice) {
+ mTestDevice = testDevice;
+ }
+
+ @Override
+ public ITestDevice getDevice() {
+ return mTestDevice;
+ }
+
+ /**
+ * Test that the boot image profile properties are set.
+ */
+ @Test
+ public void testProperties() throws Exception {
+ String res = mTestDevice.getProperty("dalvik.vm.profilebootclasspath");
+ assertTrue("profile boot class path not enabled", res != null && res.equals("true"));
+ res = mTestDevice.getProperty("dalvik.vm.profilesystemserver");
+ assertTrue("profile system server not enabled", res != null && res.equals("true"));
+ }
+
+ private void forceSaveProfile(String pkg) throws Exception {
+ String pid = mTestDevice.executeShellCommand("pidof " + pkg).trim();
+ assertTrue("Invalid pid " + pid, pid.length() > 0);
+ String res = mTestDevice.executeShellCommand("kill -s SIGUSR1 " + pid).trim();
+ assertTrue("kill SIGUSR1: " + res, res.length() == 0);
+ }
+
+ @Test
+ public void testSystemServerProfile() throws Exception {
+ // Trunacte the profile before force it to be saved to prevent previous profiles
+ // causing the test to pass.
+ String res;
+ res = mTestDevice.executeShellCommand("truncate -s 0 " + SYSTEM_SERVER_PROFILE).trim();
+ assertTrue(res, res.length() == 0);
+ // Force save profiles in case the system just started.
+ Thread.sleep(1000);
+ forceSaveProfile("system_server");
+ Thread.sleep(2000);
+ // Validate that the profile is non empty.
+ res = mTestDevice.executeShellCommand("profman --dump-only --profile-file="
+ + SYSTEM_SERVER_PROFILE);
+ boolean sawFramework = false;
+ boolean sawServices = false;
+ for (String line : res.split("\n")) {
+ if (line.contains("framework.jar")) {
+ sawFramework = true;
+ } else if (line.contains("services.jar")) {
+ sawServices = true;
+ }
+ }
+ assertTrue("Did not see framework.jar in " + res, sawFramework);
+ assertTrue("Did not see services.jar in " + res, sawServices);
+ }
+}
diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp
index 88d92c4..0b75039 100644
--- a/tests/PackageWatchdog/Android.bp
+++ b/tests/PackageWatchdog/Android.bp
@@ -23,6 +23,7 @@
"androidx.test.rules",
"services.core",
"services.net",
+ "truth-prebuilt",
],
libs: ["android.test.runner"],
jni_libs: [
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 2d867f9..9a60330 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -18,9 +18,8 @@
import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -43,6 +42,7 @@
import androidx.test.InstrumentationRegistry;
+import com.android.server.PackageWatchdog.HealthCheckState;
import com.android.server.PackageWatchdog.MonitoredPackage;
import com.android.server.PackageWatchdog.PackageHealthObserver;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
@@ -64,7 +64,6 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
-// TODO: Use Truth in tests.
/**
* Test PackageWatchdog.
*/
@@ -118,13 +117,12 @@
TestObserver observer = new TestObserver(OBSERVER_NAME_1);
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// The failed packages should be the same as the registered ones to ensure registration is
// done successfully
- assertEquals(1, observer.mHealthCheckFailedPackages.size());
- assertTrue(observer.mHealthCheckFailedPackages.contains(APP_A));
+ assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A);
}
@Test
@@ -135,17 +133,14 @@
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
- new VersionedPackage(APP_B, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+ new VersionedPackage(APP_B, VERSION_CODE)));
// The failed packages should be the same as the registered ones to ensure registration is
// done successfully
- assertEquals(1, observer1.mHealthCheckFailedPackages.size());
- assertEquals(2, observer2.mHealthCheckFailedPackages.size());
- assertTrue(observer1.mHealthCheckFailedPackages.contains(APP_A));
- assertTrue(observer2.mHealthCheckFailedPackages.contains(APP_A));
- assertTrue(observer2.mHealthCheckFailedPackages.contains(APP_B));
+ assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A);
+ assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B);
}
@Test
@@ -155,11 +150,11 @@
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
watchdog.unregisterHealthObserver(observer);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// We should have no failed packages to ensure unregistration is done successfully
- assertEquals(0, observer.mHealthCheckFailedPackages.size());
+ assertThat(observer.mHealthCheckFailedPackages).isEmpty();
}
@Test
@@ -171,13 +166,13 @@
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
watchdog.unregisterHealthObserver(observer2);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// observer1 should receive failed packages as intended.
- assertEquals(1, observer1.mHealthCheckFailedPackages.size());
+ assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A);
// observer2 should have no failed packages to ensure unregistration is done successfully
- assertEquals(0, observer2.mHealthCheckFailedPackages.size());
+ assertThat(observer2.mHealthCheckFailedPackages).isEmpty();
}
@Test
@@ -187,11 +182,11 @@
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
moveTimeForwardAndDispatch(SHORT_DURATION);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// We should have no failed packages for the fatal failure is raised after expiration
- assertEquals(0, observer.mHealthCheckFailedPackages.size());
+ assertThat(observer.mHealthCheckFailedPackages).isEmpty();
}
@Test
@@ -203,13 +198,13 @@
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), LONG_DURATION);
moveTimeForwardAndDispatch(SHORT_DURATION);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// We should have no failed packages for the fatal failure is raised after expiration
- assertEquals(0, observer1.mHealthCheckFailedPackages.size());
+ assertThat(observer1.mHealthCheckFailedPackages).isEmpty();
// We should have failed packages since observer2 hasn't expired
- assertEquals(1, observer2.mHealthCheckFailedPackages.size());
+ assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A);
}
/** Observing already observed package extends the observation time. */
@@ -230,12 +225,11 @@
// Then advance time such that it should have expired were it not for the second observation
moveTimeForwardAndDispatch((SHORT_DURATION / 2) + 1);
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify that we receive failed packages as expected for APP_A not expired
- assertEquals(1, observer.mHealthCheckFailedPackages.size());
- assertTrue(observer.mHealthCheckFailedPackages.contains(APP_A));
+ assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A);
}
/**
@@ -256,17 +250,14 @@
// Then resume observer1 and observer2
watchdog2.registerHealthObserver(observer1);
watchdog2.registerHealthObserver(observer2);
- raiseFatalFailure(watchdog2, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
- new VersionedPackage(APP_B, VERSION_CODE)));
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog2,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+ new VersionedPackage(APP_B, VERSION_CODE)));
// We should receive failed packages as expected to ensure observers are persisted and
// resumed correctly
- assertEquals(1, observer1.mHealthCheckFailedPackages.size());
- assertEquals(2, observer2.mHealthCheckFailedPackages.size());
- assertTrue(observer1.mHealthCheckFailedPackages.contains(APP_A));
- assertTrue(observer1.mHealthCheckFailedPackages.contains(APP_A));
- assertTrue(observer2.mHealthCheckFailedPackages.contains(APP_B));
+ assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A);
+ assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B);
}
/**
@@ -290,8 +281,8 @@
mTestLooper.dispatchAll();
// Verify that observers are not notified
- assertEquals(0, observer1.mMitigatedPackages.size());
- assertEquals(0, observer2.mMitigatedPackages.size());
+ assertThat(observer1.mHealthCheckFailedPackages).isEmpty();
+ assertThat(observer2.mHealthCheckFailedPackages).isEmpty();
}
/**
@@ -309,14 +300,12 @@
watchdog.startObservingHealth(observer1, Arrays.asList(APP_B), SHORT_DURATION);
// Then fail APP_C (not observed) above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE)));
-
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE)));
// Verify that observers are not notified
- assertEquals(0, observer1.mMitigatedPackages.size());
- assertEquals(0, observer2.mMitigatedPackages.size());
+ assertThat(observer1.mHealthCheckFailedPackages).isEmpty();
+ assertThat(observer2.mHealthCheckFailedPackages).isEmpty();
}
/**
@@ -341,14 +330,11 @@
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
// Then fail APP_A (different version) above the threshold
- raiseFatalFailure(watchdog,
+ raiseFatalFailureAndDispatch(watchdog,
Arrays.asList(new VersionedPackage(APP_A, differentVersionCode)));
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
-
// Verify that observers are not notified
- assertEquals(0, observer.mMitigatedPackages.size());
+ assertThat(observer.mHealthCheckFailedPackages).isEmpty();
}
@@ -378,13 +364,11 @@
SHORT_DURATION);
// Then fail all apps above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
- new VersionedPackage(APP_B, VERSION_CODE),
- new VersionedPackage(APP_C, VERSION_CODE),
- new VersionedPackage(APP_D, VERSION_CODE)));
-
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+ new VersionedPackage(APP_B, VERSION_CODE),
+ new VersionedPackage(APP_C, VERSION_CODE),
+ new VersionedPackage(APP_D, VERSION_CODE)));
// Verify least impact observers are notifed of package failures
List<String> observerNonePackages = observerNone.mMitigatedPackages;
@@ -393,16 +377,13 @@
List<String> observerLowPackages = observerLow.mMitigatedPackages;
// APP_D failure observed by only observerNone is not caught cos its impact is none
- assertEquals(0, observerNonePackages.size());
+ assertThat(observerNonePackages).isEmpty();
// APP_C failure is caught by observerHigh cos it's the lowest impact observer
- assertEquals(1, observerHighPackages.size());
- assertEquals(APP_C, observerHighPackages.get(0));
+ assertThat(observerHighPackages).containsExactly(APP_C);
// APP_B failure is caught by observerMid cos it's the lowest impact observer
- assertEquals(1, observerMidPackages.size());
- assertEquals(APP_B, observerMidPackages.get(0));
+ assertThat(observerMidPackages).containsExactly(APP_B);
// APP_A failure is caught by observerLow cos it's the lowest impact observer
- assertEquals(1, observerLowPackages.size());
- assertEquals(APP_A, observerLowPackages.get(0));
+ assertThat(observerLowPackages).containsExactly(APP_A);
}
/**
@@ -429,14 +410,12 @@
watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION);
// Then fail APP_A above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify only observerFirst is notifed
- assertEquals(1, observerFirst.mMitigatedPackages.size());
- assertEquals(APP_A, observerFirst.mMitigatedPackages.get(0));
- assertEquals(0, observerSecond.mMitigatedPackages.size());
+ assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
// After observerFirst handles failure, next action it has is high impact
observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_HIGH;
@@ -444,14 +423,12 @@
observerSecond.mMitigatedPackages.clear();
// Then fail APP_A again above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify only observerSecond is notifed cos it has least impact
- assertEquals(1, observerSecond.mMitigatedPackages.size());
- assertEquals(APP_A, observerSecond.mMitigatedPackages.get(0));
- assertEquals(0, observerFirst.mMitigatedPackages.size());
+ assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerFirst.mMitigatedPackages).isEmpty();
// After observerSecond handles failure, it has no further actions
observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE;
@@ -459,14 +436,12 @@
observerSecond.mMitigatedPackages.clear();
// Then fail APP_A again above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify only observerFirst is notifed cos it has the only action
- assertEquals(1, observerFirst.mMitigatedPackages.size());
- assertEquals(APP_A, observerFirst.mMitigatedPackages.get(0));
- assertEquals(0, observerSecond.mMitigatedPackages.size());
+ assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
// After observerFirst handles failure, it too has no further actions
observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE;
@@ -474,13 +449,12 @@
observerSecond.mMitigatedPackages.clear();
// Then fail APP_A again above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify no observer is notified cos no actions left
- assertEquals(0, observerFirst.mMitigatedPackages.size());
- assertEquals(0, observerSecond.mMitigatedPackages.size());
+ assertThat(observerFirst.mMitigatedPackages).isEmpty();
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
}
/**
@@ -499,15 +473,12 @@
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
// Then fail APP_A above the threshold
- raiseFatalFailure(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
-
- // Run handler so package failures are dispatched to observers
- mTestLooper.dispatchAll();
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
// Verify only one observer is notifed
- assertEquals(1, observer1.mMitigatedPackages.size());
- assertEquals(APP_A, observer1.mMitigatedPackages.get(0));
- assertEquals(0, observer2.mMitigatedPackages.size());
+ assertThat(observer1.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observer2.mMitigatedPackages).isEmpty();
}
/**
@@ -536,9 +507,7 @@
// Verify we requested health checks for APP_A and APP_B
List<String> requestedPackages = controller.getRequestedPackages();
- assertEquals(2, requestedPackages.size());
- assertEquals(APP_A, requestedPackages.get(0));
- assertEquals(APP_B, requestedPackages.get(1));
+ assertThat(requestedPackages).containsExactly(APP_A, APP_B);
// Then health check passed for APP_A (observer1 is aware)
controller.setPackagePassed(APP_A);
@@ -553,18 +522,16 @@
moveTimeForwardAndDispatch(SHORT_DURATION);
// Verify we cancelled all requests on expiry
- assertEquals(0, controller.getRequestedPackages().size());
+ assertThat(controller.getRequestedPackages()).isEmpty();
// Verify observer1 is not notified
- assertEquals(0, observer1.mMitigatedPackages.size());
+ assertThat(observer1.mMitigatedPackages).isEmpty();
// Verify observer2 is notifed because health checks for APP_B never passed
- assertEquals(1, observer2.mMitigatedPackages.size());
- assertEquals(APP_B, observer2.mMitigatedPackages.get(0));
+ assertThat(observer2.mMitigatedPackages).containsExactly(APP_B);
// Verify observer3 is notifed because health checks for APP_A did not pass before expiry
- assertEquals(1, observer3.mMitigatedPackages.size());
- assertEquals(APP_A, observer3.mMitigatedPackages.get(0));
+ assertThat(observer3.mMitigatedPackages).containsExactly(APP_A);
}
/**
@@ -591,9 +558,7 @@
// Verify we requested health checks for APP_A and APP_B
List<String> requestedPackages = controller.getRequestedPackages();
- assertEquals(2, requestedPackages.size());
- assertEquals(APP_A, requestedPackages.get(0));
- assertEquals(APP_B, requestedPackages.get(1));
+ assertThat(requestedPackages).containsExactly(APP_A, APP_B);
// Disable explicit health checks (marks APP_A and APP_B as passed)
setExplicitHealthCheckEnabled(false);
@@ -602,13 +567,13 @@
mTestLooper.dispatchAll();
// Verify all checks are cancelled
- assertEquals(0, controller.getRequestedPackages().size());
+ assertThat(controller.getRequestedPackages()).isEmpty();
// Then expire APP_A
moveTimeForwardAndDispatch(SHORT_DURATION);
// Verify APP_A is not failed (APP_B) is not expired yet
- assertEquals(0, observer.mMitigatedPackages.size());
+ assertThat(observer.mMitigatedPackages).isEmpty();
// Re-enable explicit health checks
setExplicitHealthCheckEnabled(true);
@@ -617,7 +582,7 @@
mTestLooper.dispatchAll();
// Verify no requests are made cos APP_A is expired and APP_B was marked as passed
- assertEquals(0, controller.getRequestedPackages().size());
+ assertThat(controller.getRequestedPackages()).isEmpty();
// Then set new supported packages
controller.setSupportedPackages(Arrays.asList(APP_C));
@@ -629,15 +594,13 @@
// Verify requests are only made for APP_C
requestedPackages = controller.getRequestedPackages();
- assertEquals(1, requestedPackages.size());
- assertEquals(APP_C, requestedPackages.get(0));
+ assertThat(requestedPackages).containsExactly(APP_C);
// Then expire APP_A and APP_C
moveTimeForwardAndDispatch(SHORT_DURATION);
// Verify only APP_C is failed because explicit health checks was not supported for APP_A
- assertEquals(1, observer.mMitigatedPackages.size());
- assertEquals(APP_C, observer.mMitigatedPackages.get(0));
+ assertThat(observer.mMitigatedPackages).containsExactly(APP_C);
}
/**
@@ -661,8 +624,7 @@
moveTimeForwardAndDispatch(SHORT_DURATION);
// Verify that health check is failed
- assertEquals(1, observer.mMitigatedPackages.size());
- assertEquals(APP_A, observer.mMitigatedPackages.get(0));
+ assertThat(observer.mMitigatedPackages).containsExactly(APP_A);
// Then clear failed packages and start observing a random package so requests are synced
// and PackageWatchdog#onSupportedPackages is called and APP_A has a chance to fail again
@@ -671,7 +633,7 @@
watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION);
// Verify that health check failure is not notified again
- assertTrue(observer.mMitigatedPackages.isEmpty());
+ assertThat(observer.mMitigatedPackages).isEmpty();
}
/** Tests {@link MonitoredPackage} health check state transitions. */
@@ -687,36 +649,38 @@
// Verify transition: inactive -> active -> passed
// Verify initially inactive
- assertEquals(MonitoredPackage.STATE_INACTIVE, m1.getHealthCheckStateLocked());
+ assertThat(m1.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE);
// Verify still inactive, until we #setHealthCheckActiveLocked
- assertEquals(MonitoredPackage.STATE_INACTIVE, m1.handleElapsedTimeLocked(SHORT_DURATION));
+ assertThat(m1.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.INACTIVE);
// Verify now active
- assertEquals(MonitoredPackage.STATE_ACTIVE, m1.setHealthCheckActiveLocked(SHORT_DURATION));
+ assertThat(m1.setHealthCheckActiveLocked(SHORT_DURATION)).isEqualTo(
+ HealthCheckState.ACTIVE);
// Verify now passed
- assertEquals(MonitoredPackage.STATE_PASSED, m1.tryPassHealthCheckLocked());
+ assertThat(m1.tryPassHealthCheckLocked()).isEqualTo(HealthCheckState.PASSED);
// Verify transition: inactive -> active -> failed
// Verify initially inactive
- assertEquals(MonitoredPackage.STATE_INACTIVE, m2.getHealthCheckStateLocked());
+ assertThat(m2.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE);
// Verify now active
- assertEquals(MonitoredPackage.STATE_ACTIVE, m2.setHealthCheckActiveLocked(SHORT_DURATION));
+ assertThat(m2.setHealthCheckActiveLocked(SHORT_DURATION)).isEqualTo(
+ HealthCheckState.ACTIVE);
// Verify now failed
- assertEquals(MonitoredPackage.STATE_FAILED, m2.handleElapsedTimeLocked(SHORT_DURATION));
+ assertThat(m2.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.FAILED);
// Verify transition: inactive -> failed
// Verify initially inactive
- assertEquals(MonitoredPackage.STATE_INACTIVE, m3.getHealthCheckStateLocked());
+ assertThat(m3.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE);
// Verify now failed because package expired
- assertEquals(MonitoredPackage.STATE_FAILED, m3.handleElapsedTimeLocked(LONG_DURATION));
+ assertThat(m3.handleElapsedTimeLocked(LONG_DURATION)).isEqualTo(HealthCheckState.FAILED);
// Verify remains failed even when asked to pass
- assertEquals(MonitoredPackage.STATE_FAILED, m3.tryPassHealthCheckLocked());
+ assertThat(m3.tryPassHealthCheckLocked()).isEqualTo(HealthCheckState.FAILED);
// Verify transition: passed
- assertEquals(MonitoredPackage.STATE_PASSED, m4.getHealthCheckStateLocked());
+ assertThat(m4.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.PASSED);
// Verify remains passed even if health check fails
- assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(SHORT_DURATION));
+ assertThat(m4.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.PASSED);
// Verify remains passed even if package expires
- assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION));
+ assertThat(m4.handleElapsedTimeLocked(LONG_DURATION)).isEqualTo(HealthCheckState.PASSED);
}
@Test
@@ -735,8 +699,7 @@
mTestLooper.dispatchAll();
// Verify the NetworkStack observer is notified
- assertEquals(1, observer.mMitigatedPackages.size());
- assertEquals(APP_A, observer.mMitigatedPackages.get(0));
+ assertThat(observer.mMitigatedPackages).containsExactly(APP_A);
}
private void adoptShellPermissions(String... permissions) {
@@ -772,10 +735,12 @@
}
/** Trigger package failures above the threshold. */
- private void raiseFatalFailure(PackageWatchdog watchdog, List<VersionedPackage> packages) {
+ private void raiseFatalFailureAndDispatch(PackageWatchdog watchdog,
+ List<VersionedPackage> packages) {
for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) {
watchdog.onPackageFailure(packages);
}
+ mTestLooper.dispatchAll();
}
private PackageWatchdog createWatchdog() {
@@ -790,13 +755,13 @@
new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller,
mConnectivityModuleConnector, mTestClock);
// Verify controller is not automatically started
- assertFalse(controller.mIsEnabled);
+ assertThat(controller.mIsEnabled).isFalse();
if (withPackagesReady) {
// Only capture the NetworkStack callback for the latest registered watchdog
reset(mConnectivityModuleConnector);
watchdog.onPackagesReady();
// Verify controller by default is started when packages are ready
- assertTrue(controller.mIsEnabled);
+ assertThat(controller.mIsEnabled).isTrue();
verify(mConnectivityModuleConnector).registerHealthListener(
mConnectivityModuleCallbackCaptor.capture());
diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp
index 2bd5931..231d045b 100644
--- a/tests/RollbackTest/Android.bp
+++ b/tests/RollbackTest/Android.bp
@@ -31,9 +31,9 @@
}
java_test_host {
- name: "SecondaryUserRollbackTest",
- srcs: ["SecondaryUserRollbackTest/src/**/*.java"],
+ name: "MultiUserRollbackTest",
+ srcs: ["MultiUserRollbackTest/src/**/*.java"],
libs: ["tradefed"],
test_suites: ["general-tests"],
- test_config: "SecondaryUserRollbackTest.xml",
+ test_config: "MultiUserRollbackTest.xml",
}
diff --git a/tests/RollbackTest/SecondaryUserRollbackTest.xml b/tests/RollbackTest/MultiUserRollbackTest.xml
similarity index 67%
rename from tests/RollbackTest/SecondaryUserRollbackTest.xml
rename to tests/RollbackTest/MultiUserRollbackTest.xml
index 6b3f05c..41cec46 100644
--- a/tests/RollbackTest/SecondaryUserRollbackTest.xml
+++ b/tests/RollbackTest/MultiUserRollbackTest.xml
@@ -13,17 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<configuration description="Runs the rollback test from a secondary user">
- <option name="test-suite-tag" value="SecondaryUserRollbackTest" />
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="RollbackTest.apk" />
- </target_preparer>
+<configuration description="Runs rollback tests for multiple users">
+ <option name="test-suite-tag" value="MultiUserRollbackTest" />
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
- <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
</target_preparer>
<test class="com.android.tradefed.testtype.HostTest" >
- <option name="class" value="com.android.tests.rollback.host.SecondaryUserRollbackTest" />
+ <option name="class" value="com.android.tests.rollback.host.MultiUserRollbackTest" />
</test>
</configuration>
diff --git a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
new file mode 100644
index 0000000..52f6eba
--- /dev/null
+++ b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.tests.rollback.host;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Runs rollback tests for multiple users.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class MultiUserRollbackTest extends BaseHostJUnit4Test {
+ // The user that was running originally when the test starts.
+ private int mOriginalUserId;
+ private int mSecondaryUserId = -1;
+ private static final long SWITCH_USER_COMPLETED_NUMBER_OF_POLLS = 60;
+ private static final long SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS = 1000;
+
+
+ @After
+ public void tearDown() throws Exception {
+ getDevice().switchUser(mOriginalUserId);
+ getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.A");
+ removeSecondaryUserIfNecessary();
+ }
+
+ @Before
+ public void setup() throws Exception {
+ mOriginalUserId = getDevice().getCurrentUser();
+ installPackageAsUser("RollbackTest.apk", true, mOriginalUserId);
+ createAndSwitchToSecondaryUserIfNecessary();
+ installPackageAsUser("RollbackTest.apk", true, mSecondaryUserId);
+ }
+
+ @Test
+ public void testBasicForSecondaryUser() throws Exception {
+ runPhaseForUsers("testBasic", mSecondaryUserId);
+ }
+
+ @Test
+ public void testMultipleUsers() throws Exception {
+ runPhaseForUsers("testMultipleUsersInstallV1", mOriginalUserId, mSecondaryUserId);
+ runPhaseForUsers("testMultipleUsersUpgradeToV2", mOriginalUserId);
+ runPhaseForUsers("testMultipleUsersUpdateUserData", mOriginalUserId, mSecondaryUserId);
+ switchToUser(mOriginalUserId);
+ getDevice().executeShellCommand("pm rollback-app com.android.cts.install.lib.testapp.A");
+ runPhaseForUsers("testMultipleUsersVerifyUserdataRollback", mOriginalUserId,
+ mSecondaryUserId);
+ }
+
+ /**
+ * Run the phase for the given user ids, in the order they are given.
+ */
+ private void runPhaseForUsers(String phase, int... userIds) throws Exception {
+ for (int userId: userIds) {
+ switchToUser(userId);
+ assertTrue(runDeviceTests("com.android.tests.rollback",
+ "com.android.tests.rollback.MultiUserRollbackTest",
+ phase));
+ }
+ }
+
+ private void removeSecondaryUserIfNecessary() throws Exception {
+ if (mSecondaryUserId != -1) {
+ getDevice().removeUser(mSecondaryUserId);
+ mSecondaryUserId = -1;
+ }
+ }
+
+ private void createAndSwitchToSecondaryUserIfNecessary() throws Exception {
+ if (mSecondaryUserId == -1) {
+ mOriginalUserId = getDevice().getCurrentUser();
+ mSecondaryUserId = getDevice().createUser("MultiUserRollbackTest_User"
+ + System.currentTimeMillis());
+ switchToUser(mSecondaryUserId);
+ }
+ }
+
+ private void switchToUser(int userId) throws Exception {
+ if (getDevice().getCurrentUser() == userId) {
+ return;
+ }
+
+ assertTrue(getDevice().switchUser(userId));
+ for (int i = 0; i < SWITCH_USER_COMPLETED_NUMBER_OF_POLLS; ++i) {
+ String userState = getDevice().executeShellCommand("am get-started-user-state "
+ + userId);
+ if (userState.contains("RUNNING_UNLOCKED")) {
+ return;
+ }
+ Thread.sleep(SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS);
+ }
+ fail("User switch to user " + userId + " timed out");
+ }
+}
diff --git a/tests/RollbackTest/RollbackTest.xml b/tests/RollbackTest/RollbackTest.xml
index 70cd867..a14b01c 100644
--- a/tests/RollbackTest/RollbackTest.xml
+++ b/tests/RollbackTest/RollbackTest.xml
@@ -22,8 +22,9 @@
<option name="package" value="com.android.tests.rollback" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
- <!-- Exclude the StagedRollbackTest tests, which needs to be specially
- driven from the StagedRollbackTest host test -->
+ <!-- Exclude the StagedRollbackTest and MultiUserRollbackTest tests, which need to be
+ specially driven from the StagedRollbackTest and MultiUserRollbackTest host test -->
<option name="exclude-filter" value="com.android.tests.rollback.StagedRollbackTest" />
+ <option name="exclude-filter" value="com.android.tests.rollback.MultiUserRollbackTest" />
</test>
</configuration>
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java
new file mode 100644
index 0000000..0ffe041
--- /dev/null
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.tests.rollback;
+
+import static com.android.cts.rollback.lib.RollbackInfoSubject.assertThat;
+import static com.android.cts.rollback.lib.RollbackUtils.getUniqueRollbackInfoForPackage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.rollback.lib.Rollback;
+import com.android.cts.rollback.lib.RollbackUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class MultiUserRollbackTest {
+
+ @Before
+ public void adoptShellPermissions() {
+ InstallUtils.adoptShellPermissionIdentity(
+ Manifest.permission.INSTALL_PACKAGES,
+ Manifest.permission.DELETE_PACKAGES,
+ Manifest.permission.TEST_MANAGE_ROLLBACKS,
+ Manifest.permission.MANAGE_ROLLBACKS);
+ }
+
+ @After
+ public void dropShellPermissions() {
+ InstallUtils.dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testBasic() throws Exception {
+ new RollbackTest().testBasic();
+ }
+
+ /**
+ * Install version 1 of the test app. This method is run for both users.
+ */
+ @Test
+ public void testMultipleUsersInstallV1() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
+ Install.single(TestApp.A1).commit();
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ InstallUtils.processUserData(TestApp.A);
+ }
+
+ /**
+ * Upgrade the test app to version 2. This method should only run once as the system user,
+ * and will update the app for both users.
+ */
+ @Test
+ public void testMultipleUsersUpgradeToV2() throws Exception {
+ RollbackManager rm = RollbackUtils.getRollbackManager();
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ Install.single(TestApp.A2).setEnableRollback().commit();
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ RollbackInfo rollback = getUniqueRollbackInfoForPackage(
+ rm.getAvailableRollbacks(), TestApp.A);
+ assertThat(rollback).isNotNull();
+ assertThat(rollback).packagesContainsExactly(
+ Rollback.from(TestApp.A2).to(TestApp.A1));
+ }
+
+ /**
+ * This method is run for both users. Assert that the test app has upgraded for both users, and
+ * update their userdata to reflect this new version.
+ */
+ @Test
+ public void testMultipleUsersUpdateUserData() {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ InstallUtils.processUserData(TestApp.A);
+ }
+
+ /**
+ * The system will have rolled back the test app at this stage. Verify that the rollback has
+ * taken place, and that the userdata has been correctly rolled back. This method is run for
+ * both users.
+ */
+ @Test
+ public void testMultipleUsersVerifyUserdataRollback() {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ InstallUtils.processUserData(TestApp.A);
+ }
+}
diff --git a/tests/RollbackTest/SecondaryUserRollbackTest/src/com/android/tests/rollback/host/SecondaryUserRollbackTest.java b/tests/RollbackTest/SecondaryUserRollbackTest/src/com/android/tests/rollback/host/SecondaryUserRollbackTest.java
deleted file mode 100644
index 11a0fbb..0000000
--- a/tests/RollbackTest/SecondaryUserRollbackTest/src/com/android/tests/rollback/host/SecondaryUserRollbackTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.tests.rollback.host;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Runs rollback tests from a secondary user.
- */
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class SecondaryUserRollbackTest extends BaseHostJUnit4Test {
- private static final int SYSTEM_USER_ID = 0;
- // The user that was running originally when the test starts.
- private int mOriginalUser = SYSTEM_USER_ID;
- private int mSecondaryUserId = -1;
- private static final long SWITCH_USER_COMPLETED_NUMBER_OF_POLLS = 60;
- private static final long SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS = 1000;
-
-
- @After
- public void tearDown() throws Exception {
- getDevice().switchUser(mOriginalUser);
- getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.A");
- getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.B");
- removeSecondaryUserIfNecessary();
- }
-
- @Before
- public void setup() throws Exception {
- createAndSwitchToSecondaryUserIfNecessary();
- installPackageAsUser("RollbackTest.apk", true, mSecondaryUserId, "--user current");
- }
-
- @Test
- public void testBasic() throws Exception {
- assertTrue(runDeviceTests("com.android.tests.rollback",
- "com.android.tests.rollback.RollbackTest",
- "testBasic"));
- }
-
- private void removeSecondaryUserIfNecessary() throws Exception {
- if (mSecondaryUserId != -1) {
- getDevice().removeUser(mSecondaryUserId);
- mSecondaryUserId = -1;
- }
- }
-
- private void createAndSwitchToSecondaryUserIfNecessary() throws Exception {
- if (mSecondaryUserId == -1) {
- mOriginalUser = getDevice().getCurrentUser();
- mSecondaryUserId = getDevice().createUser("SecondaryUserRollbackTest_User");
- assertTrue(getDevice().switchUser(mSecondaryUserId));
- // give time for user to be switched
- waitForSwitchUserCompleted(mSecondaryUserId);
- }
- }
-
- private void waitForSwitchUserCompleted(int userId) throws Exception {
- for (int i = 0; i < SWITCH_USER_COMPLETED_NUMBER_OF_POLLS; ++i) {
- String logs = getDevice().executeAdbCommand("logcat", "-v", "brief", "-d",
- "ActivityManager:D");
- if (logs.contains("Posting BOOT_COMPLETED user #" + userId)) {
- return;
- }
- Thread.sleep(SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS);
- }
- fail("User switch to user " + userId + " timed out");
- }
-}
diff --git a/tests/RollbackTest/TEST_MAPPING b/tests/RollbackTest/TEST_MAPPING
index 7ae03e6..fefde5b 100644
--- a/tests/RollbackTest/TEST_MAPPING
+++ b/tests/RollbackTest/TEST_MAPPING
@@ -7,7 +7,7 @@
"name": "StagedRollbackTest"
},
{
- "name": "SecondaryUserRollbackTest"
+ "name": "MultiUserRollbackTest"
}
]
}
diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp
new file mode 100644
index 0000000..a86c226
--- /dev/null
+++ b/tools/protologtool/Android.bp
@@ -0,0 +1,28 @@
+java_binary_host {
+ name: "protologtool",
+ manifest: "manifest.txt",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "javaparser",
+ "windowmanager-log-proto",
+ "jsonlib",
+ ],
+}
+
+java_test_host {
+ name: "protologtool-tests",
+ test_suites: ["general-tests"],
+ srcs: [
+ "src/**/*.kt",
+ "tests/**/*.kt",
+ ],
+ static_libs: [
+ "javaparser",
+ "windowmanager-log-proto",
+ "jsonlib",
+ "junit",
+ "mockito",
+ ],
+}
diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md
new file mode 100644
index 0000000..3439357
--- /dev/null
+++ b/tools/protologtool/README.md
@@ -0,0 +1,106 @@
+# ProtoLogTool
+
+Code transformation tool and viewer for ProtoLog.
+
+## What does it do?
+
+ProtoLogTool incorporates three different modes of operation:
+
+### Code transformation
+
+Command: `process <protolog class path> <protolog implementation class path>
+ <protolog groups class path> <config.jar> [<input.java>] <output.srcjar>`
+
+In this mode ProtoLogTool transforms every ProtoLog logging call in form of:
+```java
+ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2);
+```
+into:
+```java
+if (GROUP_NAME.isLogToAny()) {
+ ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 123456, "Format string %d %s or null", value1, value2);
+}
+```
+where `ProtoLog`, `ProtoLogImpl` and `ProtoLogGroup` are the classes provided as arguments
+ (can be imported, static imported or full path, wildcard imports are not allowed) and, `x` is the
+ logging method. The transformation is done on the source level. A hash is generated from the format
+ string and log level and inserted after the `ProtoLogGroup` argument. The format string is replaced
+ by `null` if `ProtoLogGroup.GROUP_NAME.isLogToLogcat()` returns false. If `ProtoLogGroup.GROUP_NAME.isEnabled()`
+ returns false the log statement is removed entirely from the resultant code.
+
+Input is provided as a list of java source file names. Transformed source is saved to a single
+source jar file. The ProtoLogGroup class with all dependencies should be provided as a compiled
+jar file (config.jar).
+
+### Viewer config generation
+
+Command: `viewerconf <protolog class path> <protolog implementation class path
+<protolog groups class path> <config.jar> [<input.java>] <output.json>`
+
+This command is similar in it's syntax to the previous one, only instead of creating a processed source jar
+it writes a viewer configuration file with following schema:
+```json
+{
+ "version": "1.0.0",
+ "messages": {
+ "123456": {
+ "message": "Format string %d %s",
+ "level": "ERROR",
+ "group": "GROUP_NAME"
+ },
+ },
+ "groups": {
+ "GROUP_NAME": {
+ "tag": "TestLog"
+ }
+ }
+}
+
+```
+
+### Binary log viewing
+
+Command: `read <viewer.json> <wm_log.pb>`
+
+Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log.
+
+## What is ProtoLog?
+
+ProtoLog is a logging system created for the WindowManager project. It allows both binary and text logging
+and is tunable in runtime. It consists of 3 different submodules:
+* logging system built-in the Android app,
+* log viewer for reading binary logs,
+* a code processing tool.
+
+ProtoLog is designed to reduce both application size (and by that memory usage) and amount of resources needed
+for logging. This is achieved by replacing log message strings with their hashes and only loading to memory/writing
+full log messages when necessary.
+
+### Text logging
+
+For text-based logs Android LogCat is used as a backend. Message strings are loaded from a viewer config
+located on the device when needed.
+
+### Binary logging
+
+Binary logs are saved as Protocol Buffers file. They can be read using the ProtoLog tool or specialised
+viewer like Winscope.
+
+## How to use ProtoLog?
+
+### Adding a new logging group or log statement
+
+To add a new ProtoLogGroup simple create a new enum ProtoLogGroup member with desired parameters.
+
+To add a new logging statement just add a new call to ProtoLog.x where x is a log level.
+
+After doing any changes to logging groups or statements you should run `make update-protolog` to update
+viewer configuration saved in the code repository.
+
+## How to change settings on device in runtime?
+Use the `adb shell su root cmd window logging` command. To get help just type
+`adb shell su root cmd window logging help`.
+
+
+
+
diff --git a/tools/protologtool/TEST_MAPPING b/tools/protologtool/TEST_MAPPING
new file mode 100644
index 0000000..52b12dc
--- /dev/null
+++ b/tools/protologtool/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "protologtool-tests"
+ }
+ ]
+}
diff --git a/tools/protologtool/manifest.txt b/tools/protologtool/manifest.txt
new file mode 100644
index 0000000..f5e53c4
--- /dev/null
+++ b/tools/protologtool/manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.protologtool.ProtoLogTool
diff --git a/tools/protologtool/src/com/android/protologtool/CodeUtils.kt b/tools/protologtool/src/com/android/protologtool/CodeUtils.kt
new file mode 100644
index 0000000..facca62
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/CodeUtils.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.ImportDeclaration
+import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.expr.BinaryExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.expr.TypeExpr
+import com.github.javaparser.ast.type.PrimitiveType
+import com.github.javaparser.ast.type.Type
+
+object CodeUtils {
+ /**
+ * Returns a stable hash of a string.
+ * We reimplement String::hashCode() for readability reasons.
+ */
+ fun hash(str: String, level: LogLevel): Int {
+ return (level.name + str).map { c -> c.toInt() }.reduce { h, c -> h * 31 + c }
+ }
+
+ fun isWildcardStaticImported(code: CompilationUnit, className: String): Boolean {
+ return code.findAll(ImportDeclaration::class.java)
+ .any { im -> im.isStatic && im.isAsterisk && im.name.toString() == className }
+ }
+
+ fun isClassImportedOrSamePackage(code: CompilationUnit, className: String): Boolean {
+ val packageName = className.substringBeforeLast('.')
+ return code.packageDeclaration.isPresent &&
+ code.packageDeclaration.get().nameAsString == packageName ||
+ code.findAll(ImportDeclaration::class.java)
+ .any { im ->
+ !im.isStatic &&
+ ((!im.isAsterisk && im.name.toString() == className) ||
+ (im.isAsterisk && im.name.toString() == packageName))
+ }
+ }
+
+ fun staticallyImportedMethods(code: CompilationUnit, className: String): Set<String> {
+ return code.findAll(ImportDeclaration::class.java)
+ .filter { im ->
+ im.isStatic &&
+ im.name.toString().substringBeforeLast('.') == className
+ }
+ .map { im -> im.name.toString().substringAfterLast('.') }.toSet()
+ }
+
+ fun concatMultilineString(expr: Expression): String {
+ return when (expr) {
+ is StringLiteralExpr -> expr.asString()
+ is BinaryExpr -> when {
+ expr.operator == BinaryExpr.Operator.PLUS ->
+ concatMultilineString(expr.left) + concatMultilineString(expr.right)
+ else -> throw InvalidProtoLogCallException(
+ "messageString must be a string literal " +
+ "or concatenation of string literals.", expr)
+ }
+ else -> throw InvalidProtoLogCallException("messageString must be a string literal " +
+ "or concatenation of string literals.", expr)
+ }
+ }
+
+ enum class LogDataTypes(
+ val type: Type,
+ val toType: (Expression) -> Expression = { expr -> expr }
+ ) {
+ // When adding new LogDataType make sure to update {@code logDataTypesToBitMask} accordingly
+ STRING(StaticJavaParser.parseClassOrInterfaceType("String"),
+ { expr ->
+ MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")),
+ SimpleName("valueOf"), NodeList(expr))
+ }),
+ LONG(PrimitiveType.longType()),
+ DOUBLE(PrimitiveType.doubleType()),
+ BOOLEAN(PrimitiveType.booleanType());
+ }
+
+ fun parseFormatString(messageString: String): List<LogDataTypes> {
+ val types = mutableListOf<LogDataTypes>()
+ var i = 0
+ while (i < messageString.length) {
+ if (messageString[i] == '%') {
+ if (i + 1 >= messageString.length) {
+ throw InvalidFormatStringException("Invalid format string in config")
+ }
+ when (messageString[i + 1]) {
+ 'b' -> types.add(CodeUtils.LogDataTypes.BOOLEAN)
+ 'd', 'o', 'x' -> types.add(CodeUtils.LogDataTypes.LONG)
+ 'f', 'e', 'g' -> types.add(CodeUtils.LogDataTypes.DOUBLE)
+ 's' -> types.add(CodeUtils.LogDataTypes.STRING)
+ '%' -> {
+ }
+ else -> throw InvalidFormatStringException("Invalid format string field" +
+ " %${messageString[i + 1]}")
+ }
+ i += 2
+ } else {
+ i += 1
+ }
+ }
+ return types
+ }
+
+ fun logDataTypesToBitMask(types: List<LogDataTypes>): Int {
+ if (types.size > 16) {
+ throw InvalidFormatStringException("Too many log call parameters " +
+ "- max 16 parameters supported")
+ }
+ var mask = 0
+ types.forEachIndexed { idx, type ->
+ val x = LogDataTypes.values().indexOf(type)
+ mask = mask or (x shl (idx * 2))
+ }
+ return mask
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/CommandOptions.kt b/tools/protologtool/src/com/android/protologtool/CommandOptions.kt
new file mode 100644
index 0000000..df49e15
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/CommandOptions.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.protologtool
+
+import java.util.regex.Pattern
+
+class CommandOptions(args: Array<String>) {
+ companion object {
+ const val TRANSFORM_CALLS_CMD = "transform-protolog-calls"
+ const val GENERATE_CONFIG_CMD = "generate-viewer-config"
+ const val READ_LOG_CMD = "read-log"
+ private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
+
+ private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
+ private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class"
+ private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
+ private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
+ private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf"
+ private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar"
+ private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM,
+ PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_JSON_PARAM,
+ OUTPUT_SOURCE_JAR_PARAM)
+
+ val USAGE = """
+ Usage: ${Constants.NAME} <command> [<args>]
+ Available commands:
+
+ $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM
+ <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM
+ <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
+ - processes java files replacing stub calls with logging code.
+
+ $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM
+ <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM
+ <viewer.json> [<input.java>]
+ - creates viewer config file from given java files.
+
+ $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb>
+ - translates a binary log to a readable format.
+ """.trimIndent()
+
+ private fun validateClassName(name: String): String {
+ if (!Pattern.matches("^([a-z]+[A-Za-z0-9]*\\.)+([A-Za-z0-9]+)$", name)) {
+ throw InvalidCommandException("Invalid class name $name")
+ }
+ return name
+ }
+
+ private fun getParam(paramName: String, params: Map<String, String>): String {
+ if (!params.containsKey(paramName)) {
+ throw InvalidCommandException("Param $paramName required")
+ }
+ return params.getValue(paramName)
+ }
+
+ private fun validateNotSpecified(paramName: String, params: Map<String, String>): String {
+ if (params.containsKey(paramName)) {
+ throw InvalidCommandException("Unsupported param $paramName")
+ }
+ return ""
+ }
+
+ private fun validateJarName(name: String): String {
+ if (!name.endsWith(".jar")) {
+ throw InvalidCommandException("Jar file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateSrcJarName(name: String): String {
+ if (!name.endsWith(".srcjar")) {
+ throw InvalidCommandException("Source jar file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateJSONName(name: String): String {
+ if (!name.endsWith(".json")) {
+ throw InvalidCommandException("Json file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateJavaInputList(list: List<String>): List<String> {
+ if (list.isEmpty()) {
+ throw InvalidCommandException("No java source input files")
+ }
+ list.forEach { name ->
+ if (!name.endsWith(".java")) {
+ throw InvalidCommandException("Not a java source file $name")
+ }
+ }
+ return list
+ }
+
+ private fun validateLogInputList(list: List<String>): String {
+ if (list.isEmpty()) {
+ throw InvalidCommandException("No log input file")
+ }
+ if (list.size > 1) {
+ throw InvalidCommandException("Only one log input file allowed")
+ }
+ return list[0]
+ }
+ }
+
+ val protoLogClassNameArg: String
+ val protoLogGroupsClassNameArg: String
+ val protoLogImplClassNameArg: String
+ val protoLogGroupsJarArg: String
+ val viewerConfigJsonArg: String
+ val outputSourceJarArg: String
+ val logProtofileArg: String
+ val javaSourceArgs: List<String>
+ val command: String
+
+ init {
+ if (args.isEmpty()) {
+ throw InvalidCommandException("No command specified.")
+ }
+ command = args[0]
+ if (command !in commands) {
+ throw InvalidCommandException("Unknown command.")
+ }
+
+ val params: MutableMap<String, String> = mutableMapOf()
+ val inputFiles: MutableList<String> = mutableListOf()
+
+ var idx = 1
+ while (idx < args.size) {
+ if (args[idx].startsWith("--")) {
+ if (idx + 1 >= args.size) {
+ throw InvalidCommandException("No value for ${args[idx]}")
+ }
+ if (args[idx] !in parameters) {
+ throw InvalidCommandException("Unknown parameter ${args[idx]}")
+ }
+ if (args[idx + 1].startsWith("--")) {
+ throw InvalidCommandException("No value for ${args[idx]}")
+ }
+ if (params.containsKey(args[idx])) {
+ throw InvalidCommandException("Duplicated parameter ${args[idx]}")
+ }
+ params[args[idx]] = args[idx + 1]
+ idx += 2
+ } else {
+ inputFiles.add(args[idx])
+ idx += 1
+ }
+ }
+
+ when (command) {
+ TRANSFORM_CALLS_CMD -> {
+ protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
+ protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
+ params))
+ protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM,
+ params))
+ protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
+ viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params)
+ outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params))
+ javaSourceArgs = validateJavaInputList(inputFiles)
+ logProtofileArg = ""
+ }
+ GENERATE_CONFIG_CMD -> {
+ protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
+ protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
+ params))
+ protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
+ protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
+ viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ javaSourceArgs = validateJavaInputList(inputFiles)
+ logProtofileArg = ""
+ }
+ READ_LOG_CMD -> {
+ protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params)
+ protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params)
+ protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
+ protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params)
+ viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ javaSourceArgs = listOf()
+ logProtofileArg = validateLogInputList(inputFiles)
+ }
+ else -> {
+ throw InvalidCommandException("Unknown command.")
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/Constants.kt b/tools/protologtool/src/com/android/protologtool/Constants.kt
new file mode 100644
index 0000000..2ccfc4d
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/Constants.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.protologtool
+
+object Constants {
+ const val NAME = "protologtool"
+ const val VERSION = "1.0.0"
+ const val IS_ENABLED_METHOD = "isEnabled"
+ const val IS_LOG_TO_LOGCAT_METHOD = "isLogToLogcat"
+ const val IS_LOG_TO_ANY_METHOD = "isLogToAny"
+ const val GET_TAG_METHOD = "getTag"
+ const val ENUM_VALUES_METHOD = "values"
+}
diff --git a/tools/protologtool/src/com/android/protologtool/LogGroup.kt b/tools/protologtool/src/com/android/protologtool/LogGroup.kt
new file mode 100644
index 0000000..42a37a2
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/LogGroup.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.protologtool
+
+data class LogGroup(
+ val name: String,
+ val enabled: Boolean,
+ val textEnabled: Boolean,
+ val tag: String
+)
diff --git a/tools/protologtool/src/com/android/protologtool/LogLevel.kt b/tools/protologtool/src/com/android/protologtool/LogLevel.kt
new file mode 100644
index 0000000..dc29557
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/LogLevel.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.ast.Node
+
+enum class LogLevel {
+ DEBUG, VERBOSE, INFO, WARN, ERROR, WTF;
+
+ companion object {
+ fun getLevelForMethodName(name: String, node: Node): LogLevel {
+ return when (name) {
+ "d" -> DEBUG
+ "v" -> VERBOSE
+ "i" -> INFO
+ "w" -> WARN
+ "e" -> ERROR
+ "wtf" -> WTF
+ else -> throw InvalidProtoLogCallException("Unknown log level $name", node)
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/LogParser.kt b/tools/protologtool/src/com/android/protologtool/LogParser.kt
new file mode 100644
index 0000000..4d0eb0e
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/LogParser.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonReader
+import com.android.server.wm.ProtoLogMessage
+import com.android.server.wm.WindowManagerLogFileProto
+import java.io.BufferedReader
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.PrintStream
+import java.lang.Exception
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Implements a simple parser/viewer for binary ProtoLog logs.
+ * A binary log is translated into Android "LogCat"-like text log.
+ */
+class LogParser(private val configParser: ViewerConfigParser) {
+ companion object {
+ private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+ private val magicNumber =
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ }
+
+ private fun printTime(time: Long, offset: Long, ps: PrintStream) {
+ ps.print(dateFormat.format(Date(time / 1000000 + offset)) + " ")
+ }
+
+ private fun printFormatted(
+ protoLogMessage: ProtoLogMessage,
+ configEntry: ViewerConfigParser.ConfigEntry,
+ ps: PrintStream
+ ) {
+ val strParmIt = protoLogMessage.strParamsList.iterator()
+ val longParamsIt = protoLogMessage.sint64ParamsList.iterator()
+ val doubleParamsIt = protoLogMessage.doubleParamsList.iterator()
+ val boolParamsIt = protoLogMessage.booleanParamsList.iterator()
+ val args = mutableListOf<Any>()
+ val format = configEntry.messageString
+ val argTypes = CodeUtils.parseFormatString(format)
+ try {
+ argTypes.forEach {
+ when (it) {
+ CodeUtils.LogDataTypes.BOOLEAN -> args.add(boolParamsIt.next())
+ CodeUtils.LogDataTypes.LONG -> args.add(longParamsIt.next())
+ CodeUtils.LogDataTypes.DOUBLE -> args.add(doubleParamsIt.next())
+ CodeUtils.LogDataTypes.STRING -> args.add(strParmIt.next())
+ }
+ }
+ } catch (ex: NoSuchElementException) {
+ throw InvalidFormatStringException("Invalid format string in config", ex)
+ }
+ if (strParmIt.hasNext() || longParamsIt.hasNext() ||
+ doubleParamsIt.hasNext() || boolParamsIt.hasNext()) {
+ throw RuntimeException("Invalid format string in config - no enough matchers")
+ }
+ val formatted = format.format(*(args.toTypedArray()))
+ ps.print("${configEntry.level} ${configEntry.tag}: $formatted\n")
+ }
+
+ private fun printUnformatted(protoLogMessage: ProtoLogMessage, ps: PrintStream, tag: String) {
+ ps.println("$tag: ${protoLogMessage.messageHash} - ${protoLogMessage.strParamsList}" +
+ " ${protoLogMessage.sint64ParamsList} ${protoLogMessage.doubleParamsList}" +
+ " ${protoLogMessage.booleanParamsList}")
+ }
+
+ fun parse(protoLogInput: InputStream, jsonConfigInput: InputStream, ps: PrintStream) {
+ val jsonReader = JsonReader(BufferedReader(InputStreamReader(jsonConfigInput)))
+ val config = configParser.parseConfig(jsonReader)
+ val protoLog = WindowManagerLogFileProto.parseFrom(protoLogInput)
+
+ if (protoLog.magicNumber != magicNumber) {
+ throw InvalidInputException("ProtoLog file magic number is invalid.")
+ }
+ if (protoLog.version != Constants.VERSION) {
+ throw InvalidInputException("ProtoLog file version not supported by this tool," +
+ " log version ${protoLog.version}, viewer version ${Constants.VERSION}")
+ }
+
+ protoLog.logList.forEach { log ->
+ printTime(log.elapsedRealtimeNanos, protoLog.realTimeToElapsedTimeOffsetMillis, ps)
+ if (log.messageHash !in config) {
+ printUnformatted(log, ps, "UNKNOWN")
+ } else {
+ val conf = config.getValue(log.messageHash)
+ try {
+ printFormatted(log, conf, ps)
+ } catch (ex: Exception) {
+ printUnformatted(log, ps, "INVALID")
+ }
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogCallProcessor.kt
new file mode 100644
index 0000000..29d8ae5
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ProtoLogCallProcessor.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+
+/**
+ * Helper class for visiting all ProtoLog calls.
+ * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback
+ * is executed.
+ */
+open class ProtoLogCallProcessor(
+ private val protoLogClassName: String,
+ private val protoLogGroupClassName: String,
+ private val groupMap: Map<String, LogGroup>
+) {
+ private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
+ private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.')
+
+ private fun getLogGroupName(
+ expr: Expression,
+ isClassImported: Boolean,
+ staticImports: Set<String>
+ ): String {
+ return when (expr) {
+ is NameExpr -> when {
+ expr.nameAsString in staticImports -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup", expr)
+ }
+ is FieldAccessExpr -> when {
+ expr.scope.toString() == protoLogGroupClassName
+ || isClassImported &&
+ expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup", expr)
+ }
+ else -> throw InvalidProtoLogCallException("Invalid group argument " +
+ "- must be ProtoLogGroup enum member reference", expr)
+ }
+ }
+
+ private fun isProtoCall(
+ call: MethodCallExpr,
+ isLogClassImported: Boolean,
+ staticLogImports: Collection<String>
+ ): Boolean {
+ return call.scope.isPresent && call.scope.get().toString() == protoLogClassName ||
+ isLogClassImported && call.scope.isPresent &&
+ call.scope.get().toString() == protoLogSimpleClassName ||
+ !call.scope.isPresent && staticLogImports.contains(call.name.toString())
+ }
+
+ open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?): CompilationUnit {
+ if (CodeUtils.isWildcardStaticImported(code, protoLogClassName) ||
+ CodeUtils.isWildcardStaticImported(code, protoLogGroupClassName)) {
+ throw IllegalImportException("Wildcard static imports of $protoLogClassName " +
+ "and $protoLogGroupClassName methods are not supported.")
+ }
+
+ val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName)
+ val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName)
+ val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code,
+ protoLogGroupClassName)
+ val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName)
+
+ code.findAll(MethodCallExpr::class.java)
+ .filter { call ->
+ isProtoCall(call, isLogClassImported, staticLogImports)
+ }.forEach { call ->
+ if (call.arguments.size < 2) {
+ throw InvalidProtoLogCallException("Method signature does not match " +
+ "any ProtoLog method.", call)
+ }
+
+ val messageString = CodeUtils.concatMultilineString(call.getArgument(1))
+ val groupNameArg = call.getArgument(0)
+ val groupName =
+ getLogGroupName(groupNameArg, isGroupClassImported, staticGroupImports)
+ if (groupName !in groupMap) {
+ throw InvalidProtoLogCallException("Unknown group argument " +
+ "- not a ProtoLogGroup enum member", call)
+ }
+
+ callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName(
+ call.name.toString(), call), groupMap.getValue(groupName))
+ }
+ return code
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogCallVisitor.kt
new file mode 100644
index 0000000..42a75f8
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ProtoLogCallVisitor.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.ast.expr.MethodCallExpr
+
+interface ProtoLogCallVisitor {
+ fun processCall(call: MethodCallExpr, messageString: String, level: LogLevel, group: LogGroup)
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogGroupReader.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogGroupReader.kt
new file mode 100644
index 0000000..664c8a6
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ProtoLogGroupReader.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.protologtool
+
+import com.android.protologtool.Constants.ENUM_VALUES_METHOD
+import com.android.protologtool.Constants.GET_TAG_METHOD
+import com.android.protologtool.Constants.IS_ENABLED_METHOD
+import com.android.protologtool.Constants.IS_LOG_TO_LOGCAT_METHOD
+import java.io.File
+import java.lang.RuntimeException
+import java.net.URLClassLoader
+
+class ProtoLogGroupReader {
+ private fun getClassloaderForJar(jarPath: String): ClassLoader {
+ val jarFile = File(jarPath)
+ val url = jarFile.toURI().toURL()
+ return URLClassLoader(arrayOf(url), ProtoLogGroupReader::class.java.classLoader)
+ }
+
+ private fun getEnumValues(clazz: Class<*>): List<Enum<*>> {
+ val valuesMethod = clazz.getMethod(ENUM_VALUES_METHOD)
+ @Suppress("UNCHECKED_CAST")
+ return (valuesMethod.invoke(null) as Array<Enum<*>>).toList()
+ }
+
+ private fun getLogGroupFromEnumValue(group: Any, clazz: Class<*>): LogGroup {
+ val enabled = clazz.getMethod(IS_ENABLED_METHOD).invoke(group) as Boolean
+ val textEnabled = clazz.getMethod(IS_LOG_TO_LOGCAT_METHOD).invoke(group) as Boolean
+ val tag = clazz.getMethod(GET_TAG_METHOD).invoke(group) as String
+ val name = (group as Enum<*>).name
+ return LogGroup(name, enabled, textEnabled, tag)
+ }
+
+ fun loadFromJar(jarPath: String, className: String): Map<String, LogGroup> {
+ try {
+ val classLoader = getClassloaderForJar(jarPath)
+ val clazz = classLoader.loadClass(className)
+ val values = getEnumValues(clazz)
+ return values.map { group ->
+ group.name to getLogGroupFromEnumValue(group, clazz)
+ }.toMap()
+ } catch (ex: ReflectiveOperationException) {
+ throw RuntimeException("Unable to load ProtoLogGroup enum class", ex)
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt
new file mode 100644
index 0000000..485a047
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.protologtool
+
+import com.android.protologtool.CommandOptions.Companion.USAGE
+import com.github.javaparser.StaticJavaParser
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.util.jar.JarOutputStream
+import java.util.zip.ZipEntry
+import kotlin.system.exitProcess
+
+object ProtoLogTool {
+ private fun showHelpAndExit() {
+ println(USAGE)
+ exitProcess(-1)
+ }
+
+ private fun processClasses(command: CommandOptions) {
+ val groups = ProtoLogGroupReader()
+ .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
+ val out = FileOutputStream(command.outputSourceJarArg)
+ val outJar = JarOutputStream(out)
+ val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+ command.protoLogGroupsClassNameArg, groups)
+ val transformer = SourceTransformer(command.protoLogImplClassNameArg, processor)
+
+ command.javaSourceArgs.forEach { path ->
+ val file = File(path)
+ val code = StaticJavaParser.parse(file)
+ val outSrc = transformer.processClass(code)
+ val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration
+ .get().nameAsString else ""
+ val newPath = pack.replace('.', '/') + '/' + file.name
+ outJar.putNextEntry(ZipEntry(newPath))
+ outJar.write(outSrc.toByteArray())
+ outJar.closeEntry()
+ }
+
+ outJar.close()
+ out.close()
+ }
+
+ private fun viewerConf(command: CommandOptions) {
+ val groups = ProtoLogGroupReader()
+ .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
+ val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+ command.protoLogGroupsClassNameArg, groups)
+ val builder = ViewerConfigBuilder(processor)
+ command.javaSourceArgs.forEach { path ->
+ val file = File(path)
+ builder.processClass(StaticJavaParser.parse(file))
+ }
+ val out = FileOutputStream(command.viewerConfigJsonArg)
+ out.write(builder.build().toByteArray())
+ out.close()
+ }
+
+ fun read(command: CommandOptions) {
+ LogParser(ViewerConfigParser())
+ .parse(FileInputStream(command.logProtofileArg),
+ FileInputStream(command.viewerConfigJsonArg), System.out)
+ }
+
+ @JvmStatic
+ fun main(args: Array<String>) {
+ try {
+ val command = CommandOptions(args)
+ when (command.command) {
+ CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command)
+ CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command)
+ CommandOptions.READ_LOG_CMD -> read(command)
+ }
+ } catch (ex: InvalidCommandException) {
+ println(ex.message)
+ showHelpAndExit()
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt b/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt
new file mode 100644
index 0000000..319a8170
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.protologtool
+
+import com.android.protologtool.Constants.IS_LOG_TO_ANY_METHOD
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.body.VariableDeclarator
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.CastExpr
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.VariableDeclarationExpr
+import com.github.javaparser.ast.stmt.BlockStmt
+import com.github.javaparser.ast.stmt.ExpressionStmt
+import com.github.javaparser.ast.stmt.IfStmt
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.printer.PrettyPrinter
+import com.github.javaparser.printer.PrettyPrinterConfiguration
+import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter
+
+class SourceTransformer(
+ protoLogImplClassName: String,
+ private val protoLogCallProcessor: ProtoLogCallProcessor
+) : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ // Input format: ProtoLog.e(GROUP, "msg %d", arg)
+ if (!call.parentNode.isPresent) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- no parent node in AST")
+ }
+ if (call.parentNode.get() !is ExpressionStmt) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- parent node in AST is not an ExpressionStmt")
+ }
+ val parentStmt = call.parentNode.get() as ExpressionStmt
+ if (!parentStmt.parentNode.isPresent) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- no grandparent node in AST")
+ }
+ val ifStmt: IfStmt
+ if (group.enabled) {
+ val hash = CodeUtils.hash(messageString, level)
+ val newCall = call.clone()
+ if (!group.textEnabled) {
+ // Remove message string if text logging is not enabled by default.
+ // Out: ProtoLog.e(GROUP, null, arg)
+ newCall.arguments[1].replace(NameExpr("null"))
+ }
+ // Insert message string hash as a second argument.
+ // Out: ProtoLog.e(GROUP, 1234, null, arg)
+ newCall.arguments.add(1, IntegerLiteralExpr(hash))
+ val argTypes = CodeUtils.parseFormatString(messageString)
+ val typeMask = CodeUtils.logDataTypesToBitMask(argTypes)
+ // Insert bitmap representing which Number parameters are to be considered as
+ // floating point numbers.
+ // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
+ newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
+ // Replace call to a stub method with an actual implementation.
+ // Out: com.android.server.wm.ProtoLogImpl.e(GROUP, 1234, null, arg)
+ newCall.setScope(protoLogImplClassNode)
+ // Create a call to GROUP.isLogAny()
+ // Out: GROUP.isLogAny()
+ val isLogAnyExpr = MethodCallExpr(newCall.arguments[0].clone(),
+ SimpleName(IS_LOG_TO_ANY_METHOD))
+ if (argTypes.size != call.arguments.size - 2) {
+ throw InvalidProtoLogCallException(
+ "Number of arguments does not mach format string", call)
+ }
+ val blockStmt = BlockStmt()
+ if (argTypes.isNotEmpty()) {
+ // Assign every argument to a variable to check its type in compile time
+ // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
+ // Out: long protoLogParam0 = arg
+ argTypes.forEachIndexed { idx, type ->
+ val varName = "protoLogParam$idx"
+ val declaration = VariableDeclarator(type.type, varName,
+ type.toType(newCall.arguments[idx + 4].clone()))
+ blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
+ newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
+ }
+ } else {
+ // Assign (Object[])null as the vararg parameter to prevent allocating an empty
+ // object array.
+ val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
+ newCall.addArgument(nullArray)
+ }
+ blockStmt.addStatement(ExpressionStmt(newCall))
+ // Create an IF-statement with the previously created condition.
+ // Out: if (GROUP.isLogAny()) {
+ // long protoLogParam0 = arg;
+ // com.android.server.wm.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0);
+ // }
+ ifStmt = IfStmt(isLogAnyExpr, blockStmt, null)
+ } else {
+ // Surround with if (false).
+ val newCall = parentStmt.clone()
+ ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null)
+ newCall.setBlockComment(" ${group.name} is disabled ")
+ }
+ // Inline the new statement.
+ val printedIfStmt = inlinePrinter.print(ifStmt)
+ // Append blank lines to preserve line numbering in file (to allow debugging)
+ val newLines = LexicalPreservingPrinter.print(parentStmt).count { c -> c == '\n' }
+ val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
+ val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt)
+ LexicalPreservingPrinter.setup(inlinedIfStmt)
+ // Replace the original call.
+ if (!parentStmt.replace(inlinedIfStmt)) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- unable to replace the call.")
+ }
+ }
+
+ private val inlinePrinter: PrettyPrinter
+ private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object")
+
+ init {
+ val config = PrettyPrinterConfiguration()
+ config.endOfLineCharacter = " "
+ config.indentSize = 0
+ config.tabWidth = 1
+ inlinePrinter = PrettyPrinter(config)
+ }
+
+ private val protoLogImplClassNode =
+ StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
+
+ fun processClass(compilationUnit: CompilationUnit): String {
+ LexicalPreservingPrinter.setup(compilationUnit)
+ protoLogCallProcessor.process(compilationUnit, this)
+ return LexicalPreservingPrinter.print(compilationUnit)
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt
new file mode 100644
index 0000000..8ce9a49
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonWriter
+import com.github.javaparser.ast.CompilationUnit
+import com.android.protologtool.Constants.VERSION
+import com.github.javaparser.ast.expr.MethodCallExpr
+import java.io.StringWriter
+
+class ViewerConfigBuilder(
+ private val protoLogCallVisitor: ProtoLogCallProcessor
+) : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ if (group.enabled) {
+ val key = CodeUtils.hash(messageString, level)
+ if (statements.containsKey(key)) {
+ if (statements[key] != Triple(messageString, level, group)) {
+ throw HashCollisionException(
+ "Please modify the log message \"$messageString\" " +
+ "or \"${statements[key]}\" - their hashes are equal.")
+ }
+ } else {
+ groups.add(group)
+ statements[key] = Triple(messageString, level, group)
+ }
+ }
+ }
+
+ private val statements: MutableMap<Int, Triple<String, LogLevel, LogGroup>> = mutableMapOf()
+ private val groups: MutableSet<LogGroup> = mutableSetOf()
+
+ fun processClass(unit: CompilationUnit) {
+ protoLogCallVisitor.process(unit, this)
+ }
+
+ fun build(): String {
+ val stringWriter = StringWriter()
+ val writer = JsonWriter(stringWriter)
+ writer.setIndent(" ")
+ writer.beginObject()
+ writer.name("version")
+ writer.value(VERSION)
+ writer.name("messages")
+ writer.beginObject()
+ statements.toSortedMap().forEach { (key, value) ->
+ writer.name(key.toString())
+ writer.beginObject()
+ writer.name("message")
+ writer.value(value.first)
+ writer.name("level")
+ writer.value(value.second.name)
+ writer.name("group")
+ writer.value(value.third.name)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.name("groups")
+ writer.beginObject()
+ groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group ->
+ writer.name(group.name)
+ writer.beginObject()
+ writer.name("tag")
+ writer.value(group.tag)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.endObject()
+ stringWriter.buffer.append('\n')
+ return stringWriter.toString()
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt
new file mode 100644
index 0000000..69cf92d
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonReader
+
+open class ViewerConfigParser {
+ data class MessageEntry(
+ val messageString: String,
+ val level: String,
+ val groupName: String
+ )
+
+ fun parseMessage(jsonReader: JsonReader): MessageEntry {
+ jsonReader.beginObject()
+ var message: String? = null
+ var level: String? = null
+ var groupName: String? = null
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ when (key) {
+ "message" -> message = jsonReader.nextString()
+ "level" -> level = jsonReader.nextString()
+ "group" -> groupName = jsonReader.nextString()
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (message.isNullOrBlank() || level.isNullOrBlank() || groupName.isNullOrBlank()) {
+ throw InvalidViewerConfigException("Invalid message entry in viewer config")
+ }
+ return MessageEntry(message, level, groupName)
+ }
+
+ data class GroupEntry(val tag: String)
+
+ fun parseGroup(jsonReader: JsonReader): GroupEntry {
+ jsonReader.beginObject()
+ var tag: String? = null
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ when (key) {
+ "tag" -> tag = jsonReader.nextString()
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (tag.isNullOrBlank()) {
+ throw InvalidViewerConfigException("Invalid group entry in viewer config")
+ }
+ return GroupEntry(tag)
+ }
+
+ fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> {
+ val config: MutableMap<Int, MessageEntry> = mutableMapOf()
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ val hash = key.toIntOrNull()
+ ?: throw InvalidViewerConfigException("Invalid key in messages viewer config")
+ config[hash] = parseMessage(jsonReader)
+ }
+ jsonReader.endObject()
+ return config
+ }
+
+ fun parseGroups(jsonReader: JsonReader): Map<String, GroupEntry> {
+ val config: MutableMap<String, GroupEntry> = mutableMapOf()
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ config[key] = parseGroup(jsonReader)
+ }
+ jsonReader.endObject()
+ return config
+ }
+
+ data class ConfigEntry(val messageString: String, val level: String, val tag: String)
+
+ open fun parseConfig(jsonReader: JsonReader): Map<Int, ConfigEntry> {
+ var messages: Map<Int, MessageEntry>? = null
+ var groups: Map<String, GroupEntry>? = null
+ var version: String? = null
+
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ when (key) {
+ "messages" -> messages = parseMessages(jsonReader)
+ "groups" -> groups = parseGroups(jsonReader)
+ "version" -> version = jsonReader.nextString()
+
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (messages == null || groups == null || version == null) {
+ throw InvalidViewerConfigException("Invalid config - definitions missing")
+ }
+ if (version != Constants.VERSION) {
+ throw InvalidViewerConfigException("Viewer config version not supported by this tool," +
+ " config version $version, viewer version ${Constants.VERSION}")
+ }
+ return messages.map { msg ->
+ msg.key to ConfigEntry(
+ msg.value.messageString, msg.value.level, groups[msg.value.groupName]?.tag
+ ?: throw InvalidViewerConfigException(
+ "Group definition missing for ${msg.value.groupName}"))
+ }.toMap()
+ }
+}
diff --git a/tools/protologtool/src/com/android/protologtool/exceptions.kt b/tools/protologtool/src/com/android/protologtool/exceptions.kt
new file mode 100644
index 0000000..2199785
--- /dev/null
+++ b/tools/protologtool/src/com/android/protologtool/exceptions.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.ast.Node
+import java.lang.Exception
+import java.lang.RuntimeException
+
+class HashCollisionException(message: String) : RuntimeException(message)
+
+class IllegalImportException(message: String) : Exception(message)
+
+class InvalidProtoLogCallException(message: String, node: Node)
+ : RuntimeException("$message\nAt: $node")
+
+class InvalidViewerConfigException : Exception {
+ constructor(message: String) : super(message)
+
+ constructor(message: String, ex: Exception) : super(message, ex)
+}
+
+class InvalidFormatStringException : Exception {
+ constructor(message: String) : super(message)
+
+ constructor(message: String, ex: Exception) : super(message, ex)
+}
+
+class InvalidInputException(message: String) : Exception(message)
+
+class InvalidCommandException(message: String) : Exception(message)
diff --git a/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt
new file mode 100644
index 0000000..82daa73
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt
@@ -0,0 +1,206 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.expr.BinaryExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CodeUtilsTest {
+ @Test
+ fun hash() {
+ assertEquals(-1704685243, CodeUtils.hash("test", LogLevel.DEBUG))
+ }
+
+ @Test
+ fun hash_changeLevel() {
+ assertEquals(-1176900998, CodeUtils.hash("test", LogLevel.ERROR))
+ }
+
+ @Test
+ fun hash_changeMessage() {
+ assertEquals(-1305634931, CodeUtils.hash("test2", LogLevel.DEBUG))
+ }
+
+ @Test
+ fun isWildcardStaticImported_true() {
+ val code = """package org.example.test;
+ import static org.example.Test.*;
+ """
+ assertTrue(CodeUtils.isWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isWildcardStaticImported_notStatic() {
+ val code = """package org.example.test;
+ import org.example.Test.*;
+ """
+ assertFalse(CodeUtils.isWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isWildcardStaticImported_differentClass() {
+ val code = """package org.example.test;
+ import static org.example.Test2.*;
+ """
+ assertFalse(CodeUtils.isWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isWildcardStaticImported_notWildcard() {
+ val code = """package org.example.test;
+ import org.example.Test.test;
+ """
+ assertFalse(CodeUtils.isWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_imported() {
+ val code = """package org.example.test;
+ import org.example.Test;
+ """
+ assertTrue(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_samePackage() {
+ val code = """package org.example.test;
+ """
+ assertTrue(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.test.Test"))
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_false() {
+ val code = """package org.example.test;
+ import org.example.Test;
+ """
+ assertFalse(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.Test2"))
+ }
+
+ @Test
+ fun staticallyImportedMethods_ab() {
+ val code = """
+ import static org.example.Test.a;
+ import static org.example.Test.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a", "b")))
+ assertEquals(2, imported.size)
+ }
+
+ @Test
+ fun staticallyImportedMethods_differentClass() {
+ val code = """
+ import static org.example.Test.a;
+ import static org.example.Test2.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a")))
+ assertEquals(1, imported.size)
+ }
+
+ @Test
+ fun staticallyImportedMethods_notStatic() {
+ val code = """
+ import static org.example.Test.a;
+ import org.example.Test.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a")))
+ assertEquals(1, imported.size)
+ }
+
+ @Test
+ fun concatMultilineString_single() {
+ val str = StringLiteralExpr("test")
+ val out = CodeUtils.concatMultilineString(str)
+ assertEquals("test", out)
+ }
+
+ @Test
+ fun concatMultilineString_double() {
+ val str = """
+ "test" + "abc"
+ """
+ val code = StaticJavaParser.parseExpression<BinaryExpr>(str)
+ val out = CodeUtils.concatMultilineString(code)
+ assertEquals("testabc", out)
+ }
+
+ @Test
+ fun concatMultilineString_multiple() {
+ val str = """
+ "test" + "abc" + "1234" + "test"
+ """
+ val code = StaticJavaParser.parseExpression<BinaryExpr>(str)
+ val out = CodeUtils.concatMultilineString(code)
+ assertEquals("testabc1234test", out)
+ }
+
+ @Test
+ fun parseFormatString() {
+ val str = "%b %d %o %x %f %e %g %s %%"
+ val out = CodeUtils.parseFormatString(str)
+ assertEquals(listOf(
+ CodeUtils.LogDataTypes.BOOLEAN,
+ CodeUtils.LogDataTypes.LONG,
+ CodeUtils.LogDataTypes.LONG,
+ CodeUtils.LogDataTypes.LONG,
+ CodeUtils.LogDataTypes.DOUBLE,
+ CodeUtils.LogDataTypes.DOUBLE,
+ CodeUtils.LogDataTypes.DOUBLE,
+ CodeUtils.LogDataTypes.STRING
+ ), out)
+ }
+
+ @Test(expected = InvalidFormatStringException::class)
+ fun parseFormatString_invalid() {
+ val str = "%q"
+ CodeUtils.parseFormatString(str)
+ }
+
+ @Test
+ fun logDataTypesToBitMask() {
+ val types = listOf(CodeUtils.LogDataTypes.STRING, CodeUtils.LogDataTypes.DOUBLE,
+ CodeUtils.LogDataTypes.LONG, CodeUtils.LogDataTypes.BOOLEAN)
+ val mask = CodeUtils.logDataTypesToBitMask(types)
+ assertEquals(0b11011000, mask)
+ }
+
+ @Test(expected = InvalidFormatStringException::class)
+ fun logDataTypesToBitMask_toManyParams() {
+ val types = mutableListOf<CodeUtils.LogDataTypes>()
+ for (i in 0..16) {
+ types.add(CodeUtils.LogDataTypes.STRING)
+ }
+ CodeUtils.logDataTypesToBitMask(types)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protologtool/CommandOptionsTest.kt
new file mode 100644
index 0000000..c1cd473
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/CommandOptionsTest.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.protologtool
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CommandOptionsTest {
+ companion object {
+ val TEST_JAVA_SRC = listOf(
+ "frameworks/base/services/core/java/com/android/server/wm/" +
+ "AccessibilityController.java",
+ "frameworks/base/services/core/java/com/android/server/wm/ActivityDisplay.java",
+ "frameworks/base/services/core/java/com/android/server/wm/" +
+ "ActivityMetricsLaunchObserver.java"
+ )
+ private const val TEST_PROTOLOG_CLASS = "com.android.server.wm.ProtoLog"
+ private const val TEST_PROTOLOGIMPL_CLASS = "com.android.server.wm.ProtoLogImpl"
+ private const val TEST_PROTOLOGGROUP_CLASS = "com.android.server.wm.ProtoLogGroup"
+ private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" +
+ "services/core/services.core.wm.protologgroups/android_common/javac/" +
+ "services.core.wm.protologgroups.jar"
+ private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" +
+ "services.core.wm.protolog.srcjar"
+ private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" +
+ "services.core.wm.protolog.json"
+ private const val TEST_LOG = "./test_log.pb"
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun noCommand() {
+ CommandOptions(arrayOf())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun invalidCommand() {
+ val testLine = "invalid"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun transformClasses() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogClass() {
+ val testLine = "transform-protolog-calls " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogImplClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogGroupClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogGroupJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noOutJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ TEST_JAVA_SRC.joinToString(" ")
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noJavaInput() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogClass() {
+ val testLine = "transform-protolog-calls --protolog-class invalid " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogImplClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class invalid " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogGroupClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class invalid " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogGroupJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar invalid.txt " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidOutJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidJavaInput() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR invalid.py"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_unknownParam() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noValue() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun generateConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun generateConfig_noViewerConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ TEST_JAVA_SRC.joinToString(" ")
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun generateConfig_invalidViewerConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun readLog() {
+ val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.READ_LOG_CMD, cmd.command)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_LOG, cmd.logProtofileArg)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/LogParserTest.kt b/tools/protologtool/tests/com/android/protologtool/LogParserTest.kt
new file mode 100644
index 0000000..7106ea6
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/LogParserTest.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonReader
+import com.android.server.wm.ProtoLogMessage
+import com.android.server.wm.WindowManagerLogFileProto
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PrintStream
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class LogParserTest {
+ private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java)
+ private val parser = LogParser(configParser)
+ private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf()
+ private var outStream: OutputStream = ByteArrayOutputStream()
+ private var printStream: PrintStream = PrintStream(outStream)
+ private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+
+ @Before
+ fun init() {
+ Mockito.`when`(configParser.parseConfig(any(JsonReader::class.java))).thenReturn(config)
+ }
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ private fun getConfigDummyStream(): InputStream {
+ return "".byteInputStream()
+ }
+
+ private fun buildProtoInput(logBuilder: WindowManagerLogFileProto.Builder): InputStream {
+ logBuilder.setVersion(Constants.VERSION)
+ logBuilder.magicNumber =
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ return logBuilder.build().toByteArray().inputStream()
+ }
+
+ private fun testDate(timeMS: Long): String {
+ return dateFormat.format(Date(timeMS))
+ }
+
+ @Test
+ fun parse() {
+ config[70933285] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b",
+ "ERROR", "WindowManager")
+
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(70933285)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: true\n",
+ outStream.toString())
+ }
+
+ @Test
+ fun parse_formatting() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" +
+ " %x %e %g %s %f", "ERROR", "WindowManager")
+
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addAllSint64Params(listOf(1000, 20000, 300000))
+ .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1))
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: " +
+ "true 1000 % 47040 493e0 1.000000e-01 1.00000e-05 test 1000.100000\n",
+ outStream.toString())
+ }
+
+ @Test
+ fun parse_invalidParamsTooMany() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o",
+ "ERROR", "WindowManager")
+
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addAllSint64Params(listOf(1000, 20000, 300000))
+ .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1))
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} INVALID: 123 - [test] [1000, 20000, 300000] " +
+ "[0.1, 1.0E-5, 1000.1] [true]\n", outStream.toString())
+ }
+
+ @Test
+ fun parse_invalidParamsNotEnough() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" +
+ " %x %e %g %s %f", "ERROR", "WindowManager")
+
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} INVALID: 123 - [test] [] [] [true]\n",
+ outStream.toString())
+ }
+
+ @Test(expected = InvalidInputException::class)
+ fun parse_invalidMagicNumber() {
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ logBuilder.setVersion(Constants.VERSION)
+ logBuilder.magicNumber = 0
+ val stream = logBuilder.build().toByteArray().inputStream()
+
+ parser.parse(stream, getConfigDummyStream(), printStream)
+ }
+
+ @Test(expected = InvalidInputException::class)
+ fun parse_invalidVersion() {
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ logBuilder.setVersion("invalid")
+ logBuilder.magicNumber =
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ val stream = logBuilder.build().toByteArray().inputStream()
+
+ parser.parse(stream, getConfigDummyStream(), printStream)
+ }
+
+ @Test
+ fun parse_noConfig() {
+ val logBuilder = WindowManagerLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(70933285)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} UNKNOWN: 70933285 - [] [] [] [true]\n",
+ outStream.toString())
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protologtool/ProtoLogCallProcessorTest.kt
new file mode 100644
index 0000000..dcb1f7f
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/ProtoLogCallProcessorTest.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.expr.MethodCallExpr
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ProtoLogCallProcessorTest {
+ private data class LogCall(
+ val call: MethodCallExpr,
+ val messageString: String,
+ val level: LogLevel,
+ val group: LogGroup
+ )
+
+ private val groupMap: MutableMap<String, LogGroup> = mutableMapOf()
+ private val calls: MutableList<LogCall> = mutableListOf()
+ private val visitor = ProtoLogCallProcessor("org.example.ProtoLog", "org.example.ProtoLogGroup",
+ groupMap)
+ private val processor = object : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ calls.add(LogCall(call, messageString, level, group))
+ }
+ }
+
+ private fun checkCalls() {
+ assertEquals(1, calls.size)
+ val c = calls[0]
+ assertEquals("test %b", c.messageString)
+ assertEquals(groupMap["TEST"], c.group)
+ assertEquals(LogLevel.DEBUG, c.level)
+ }
+
+ @Test
+ fun process_samePackage() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ ProtoLog.e(ProtoLogGroup.ERROR, "error %d", 1);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, false, "WindowManager")
+ groupMap["ERROR"] = LogGroup("ERROR", true, true, "WindowManagerERROR")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ assertEquals(2, calls.size)
+ var c = calls[0]
+ assertEquals("test %b", c.messageString)
+ assertEquals(groupMap["TEST"], c.group)
+ assertEquals(LogLevel.DEBUG, c.level)
+ c = calls[1]
+ assertEquals("error %d", c.messageString)
+ assertEquals(groupMap["ERROR"], c.group)
+ assertEquals(LogLevel.ERROR, c.level)
+ }
+
+ @Test
+ fun process_imported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLog;
+ import org.example.ProtoLogGroup;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ checkCalls()
+ }
+
+ @Test
+ fun process_importedStatic() {
+ val code = """
+ package org.example2;
+
+ import static org.example.ProtoLog.d;
+ import static org.example.ProtoLogGroup.TEST;
+
+ class Test {
+ void test() {
+ d(TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ checkCalls()
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_groupNotImported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLog;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ }
+
+ @Test
+ fun process_protoLogNotImported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLogGroup;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ assertEquals(0, calls.size)
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_unknownGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor)
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_staticGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(TEST, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor)
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_badGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(0, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor)
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_invalidSignature() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d("test");
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor)
+ }
+
+ @Test
+ fun process_disabled() {
+ // Disabled groups are also processed.
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", false, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor)
+ checkCalls()
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protologtool/SourceTransformerTest.kt
new file mode 100644
index 0000000..7b8dd9a
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/SourceTransformerTest.kt
@@ -0,0 +1,373 @@
+/*
+ * 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.protologtool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.stmt.IfStmt
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+import org.mockito.Mockito
+
+class SourceTransformerTest {
+ companion object {
+ private const val PROTO_LOG_IMPL_PATH = "org.example.ProtoLogImpl"
+ private val TEST_CODE = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1);
+ }
+ }
+ """.trimIndent()
+
+ private val TEST_CODE_MULTILINE = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test %d %f " +
+ "abc %s\n test", 100,
+ 0.1, "test");
+ }
+ }
+ """.trimIndent()
+
+ private val TEST_CODE_NO_PARAMS = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test");
+ }
+ }
+ """.trimIndent()
+
+ /* ktlint-disable max-line-length */
+ private val TRANSFORMED_CODE_TEXT_ENABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 835524026, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -986393606, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2);
+
+ }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_NO_PARAMS = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (TEST_GROUP.isLogToAny()) { org.example.ProtoLogImpl.w(TEST_GROUP, 1282022424, 0, "test", (Object[]) null); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_TEXT_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 835524026, 9, null, protoLogParam0, protoLogParam1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -986393606, 9, null, protoLogParam0, protoLogParam1, protoLogParam2);
+
+ }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test");
+
+ }
+ }
+ }
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+ }
+
+ private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
+ private val sourceJarWriter = SourceTransformer("org.example.ProtoLogImpl", processor)
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ @Test
+ fun processClass_textEnabled() {
+ val code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(3, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(6, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("835524026", methodCall.arguments[1].toString())
+ assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
+ assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out)
+ }
+
+ @Test
+ fun processClass_textEnabledMultiline() {
+ val code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(4, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(7, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("-986393606", methodCall.arguments[1].toString())
+ assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals("protoLogParam2", methodCall.arguments[6].toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out)
+ }
+
+ @Test
+ fun processClass_noParams() {
+ val code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(1, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(5, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1282022424", methodCall.arguments[1].toString())
+ assertEquals(0.toString(), methodCall.arguments[2].toString())
+ assertEquals(TRANSFORMED_CODE_NO_PARAMS, out)
+ }
+
+ @Test
+ fun processClass_textDisabled() {
+ val code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, false, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(3, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(6, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("835524026", methodCall.arguments[1].toString())
+ assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
+ assertEquals("null", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_textDisabledMultiline() {
+ val code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ true, false, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(4, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(7, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("-986393606", methodCall.arguments[1].toString())
+ assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
+ assertEquals("null", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals("protoLogParam2", methodCall.arguments[6].toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_disabled() {
+ val code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("false", ifStmt.condition.toString())
+ assertEquals(TRANSFORMED_CODE_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_disabledMultiline() {
+ val code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ false, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(code)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("false", ifStmt.condition.toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protologtool/ViewerConfigBuilderTest.kt
new file mode 100644
index 0000000..53d2e8b
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/ViewerConfigBuilderTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonReader
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.MethodCallExpr
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+import java.io.StringReader
+
+class ViewerConfigBuilderTest {
+ companion object {
+ private val TAG1 = "WM_TEST"
+ private val TAG2 = "WM_DEBUG"
+ private val TEST1 = ViewerConfigParser.ConfigEntry("test1", LogLevel.INFO.name, TAG1)
+ private val TEST2 = ViewerConfigParser.ConfigEntry("test2", LogLevel.DEBUG.name, TAG2)
+ private val TEST3 = ViewerConfigParser.ConfigEntry("test3", LogLevel.ERROR.name, TAG2)
+ }
+
+ private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
+ private val configBuilder = ViewerConfigBuilder(processor)
+ private val dummyCompilationUnit = CompilationUnit()
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> {
+ return ViewerConfigParser().parseConfig(JsonReader(StringReader(json)))
+ }
+
+ @Test
+ fun processClass() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ LogGroup("TEST_GROUP", true, true, TAG1))
+ visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG,
+ LogGroup("DEBUG_GROUP", true, true, TAG2))
+ visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR,
+ LogGroup("DEBUG_GROUP", true, true, TAG2))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(3, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString,
+ LogLevel.INFO)])
+ assertEquals(TEST2, parsedConfig[CodeUtils.hash(TEST2.messageString,
+ LogLevel.DEBUG)])
+ assertEquals(TEST3, parsedConfig[CodeUtils.hash(TEST3.messageString,
+ LogLevel.ERROR)])
+ }
+
+ @Test
+ fun processClass_nonUnique() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ LogGroup("TEST_GROUP", true, true, TAG1))
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ LogGroup("TEST_GROUP", true, true, TAG1))
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ LogGroup("TEST_GROUP", true, true, TAG1))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(1, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString, LogLevel.INFO)])
+ }
+
+ @Test
+ fun processClass_disabled() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ LogGroup("TEST_GROUP", true, true, TAG1))
+ visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG,
+ LogGroup("DEBUG_GROUP", false, true, TAG2))
+ visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR,
+ LogGroup("DEBUG_GROUP", true, false, TAG2))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(2, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString, LogLevel.INFO)])
+ assertEquals(TEST3, parsedConfig[CodeUtils.hash(TEST3.messageString, LogLevel.ERROR)])
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protologtool/ViewerConfigParserTest.kt b/tools/protologtool/tests/com/android/protologtool/ViewerConfigParserTest.kt
new file mode 100644
index 0000000..c0cea73
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protologtool/ViewerConfigParserTest.kt
@@ -0,0 +1,327 @@
+/*
+ * 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.protologtool
+
+import com.android.json.stream.JsonReader
+import org.junit.Test
+import java.io.StringReader
+import org.junit.Assert.assertEquals
+
+class ViewerConfigParserTest {
+ private val parser = ViewerConfigParser()
+
+ private fun getJSONReader(str: String): JsonReader {
+ return JsonReader(StringReader(str))
+ }
+
+ @Test
+ fun parseMessage() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test
+ fun parseMessage_reorder() {
+ val json = """
+ {
+ "group": "GENERIC_WM",
+ "level": "ERROR",
+ "message": "Test completed successfully: %b"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test
+ fun parseMessage_unknownEntry() {
+ val json = """
+ {
+ "unknown": "unknown entries should not block parsing",
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noMessage() {
+ val json = """
+ {
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noLevel() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "group": "GENERIC_WM"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noGroup() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test
+ fun parseGroup() {
+ val json = """
+ {
+ "tag": "WindowManager"
+ }
+ """
+ val group = parser.parseGroup(getJSONReader(json))
+ assertEquals("WindowManager", group.tag)
+ }
+
+ @Test
+ fun parseGroup_unknownEntry() {
+ val json = """
+ {
+ "unknown": "unknown entries should not block parsing",
+ "tag": "WindowManager"
+ }
+ """
+ val group = parser.parseGroup(getJSONReader(json))
+ assertEquals("WindowManager", group.tag)
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseGroup_noTag() {
+ val json = """
+ {
+ }
+ """
+ parser.parseGroup(getJSONReader(json))
+ }
+
+ @Test
+ fun parseMessages() {
+ val json = """
+ {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ },
+ "1792430067": {
+ "message": "Attempted to add window to a display that does not exist: %d. Aborting.",
+ "level": "WARN",
+ "group": "ERROR_WM"
+ }
+ }
+ """
+ val messages = parser.parseMessages(getJSONReader(json))
+ assertEquals(2, messages.size)
+ val msg1 =
+ ViewerConfigParser.MessageEntry("Test completed successfully: %b",
+ "ERROR", "GENERIC_WM")
+ val msg2 =
+ ViewerConfigParser.MessageEntry("Attempted to add window to a display that " +
+ "does not exist: %d. Aborting.", "WARN", "ERROR_WM")
+
+ assertEquals(msg1, messages[70933285])
+ assertEquals(msg2, messages[1792430067])
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessages_invalidHash() {
+ val json = """
+ {
+ "invalid": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ }
+ """
+ parser.parseMessages(getJSONReader(json))
+ }
+
+ @Test
+ fun parseGroups() {
+ val json = """
+ {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ },
+ "ERROR_WM": {
+ "tag": "WindowManagerError"
+ }
+ }
+ """
+ val groups = parser.parseGroups(getJSONReader(json))
+ assertEquals(2, groups.size)
+ val grp1 = ViewerConfigParser.GroupEntry("WindowManager")
+ val grp2 = ViewerConfigParser.GroupEntry("WindowManagerError")
+ assertEquals(grp1, groups["GENERIC_WM"])
+ assertEquals(grp2, groups["ERROR_WM"])
+ }
+
+ @Test
+ fun parseConfig() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ val config = parser.parseConfig(getJSONReader(json))
+ assertEquals(1, config.size)
+ val cfg1 = ViewerConfigParser.ConfigEntry("Test completed successfully: %b",
+ "ERROR", "WindowManager")
+ assertEquals(cfg1, config[70933285])
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_invalidVersion() {
+ val json = """
+ {
+ "version": "invalid",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noVersion() {
+ val json = """
+ {
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noMessages() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noGroups() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_missingGroup() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "ERROR_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ val config = parser.parseConfig(getJSONReader(json))
+ }
+}