DDM: Introduce STAG packets
This is a new mecanism to help DDM client synchronize with app
state.
Design doc: go/ddm-stag
Test: Android Studio
Bug: NA
Change-Id: I4699c62756f50106962ec8481bf67f103fa41b8f
diff --git a/Android.bp b/Android.bp
index effd7ce..3709d75 100644
--- a/Android.bp
+++ b/Android.bp
@@ -503,6 +503,13 @@
}
filegroup {
+ name: "framework-android-os-unit-testable-src",
+ srcs: [
+ "core/java/android/os/DdmSyncState.java",
+ ],
+}
+
+filegroup {
name: "framework-networkstack-shared-srcs",
srcs: [
// TODO: remove these annotations as soon as we can use andoid.support.annotations.*
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 5496191..6e9d055 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -117,6 +117,8 @@
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.DdmSyncStageUpdater;
+import android.os.DdmSyncState.Stage;
import android.os.Debug;
import android.os.Environment;
import android.os.FileUtils;
@@ -258,6 +260,9 @@
*/
public final class ActivityThread extends ClientTransactionHandler
implements ActivityThreadInternal {
+
+ private final DdmSyncStageUpdater mDdmSyncStageUpdater = new DdmSyncStageUpdater();
+
/** @hide */
public static final String TAG = "ActivityThread";
private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565;
@@ -6656,6 +6661,8 @@
@UnsupportedAppUsage
private void handleBindApplication(AppBindData data) {
+ mDdmSyncStageUpdater.next(Stage.Bind);
+
// Register the UI Thread as a sensitive thread to the runtime.
VMRuntime.registerSensitiveThread();
// In the case the stack depth property exists, pass it down to the runtime.
@@ -6699,6 +6706,7 @@
data.appInfo.packageName,
UserHandle.myUserId());
VMRuntime.setProcessPackageName(data.appInfo.packageName);
+ mDdmSyncStageUpdater.next(Stage.Named);
// Pass data directory path to ART. This is used for caching information and
// should be set before any application code is loaded.
@@ -6903,6 +6911,7 @@
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) {
+ mDdmSyncStageUpdater.next(Stage.Debugger);
if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) {
waitForDebugger(data);
} else if (data.debugMode == ApplicationThreadConstants.DEBUG_SUSPEND) {
@@ -6910,6 +6919,7 @@
}
// Nothing special to do in case of DEBUG_ON.
}
+ mDdmSyncStageUpdater.next(Stage.Running);
try {
// If the app is being launched for full backup or restore, bring it up in
@@ -7801,6 +7811,7 @@
mConfigurationController = new ConfigurationController(this);
mSystemThread = system;
mStartSeq = startSeq;
+ mDdmSyncStageUpdater.next(Stage.Attach);
if (!system) {
android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java
index 4160029..a51a740 100644
--- a/core/java/android/ddm/DdmHandleHello.java
+++ b/core/java/android/ddm/DdmHandleHello.java
@@ -16,6 +16,7 @@
package android.ddm;
+import android.os.DdmSyncState;
import android.os.Debug;
import android.os.UserHandle;
import android.util.Log;
@@ -44,6 +45,7 @@
private static final String[] FRAMEWORK_FEATURES = new String[] {
"opengl-tracing",
"view-hierarchy",
+ "support_boot_stages"
};
/* singleton, do not instantiate */
@@ -145,7 +147,9 @@
+ instructionSetDescription.length() * 2
+ vmFlags.length() * 2
+ 1
- + pkgName.length() * 2);
+ + pkgName.length() * 2
+ // STAG id (int)
+ + Integer.BYTES);
out.order(ChunkHandler.CHUNK_ORDER);
out.putInt(CLIENT_PROTOCOL_VERSION);
out.putInt(android.os.Process.myPid());
@@ -162,6 +166,10 @@
out.putInt(pkgName.length());
putString(out, pkgName);
+ // Added API 34 (and advertised via FEAT ddm packet)
+ // Send the current boot stage in ActivityThread
+ out.putInt(DdmSyncState.getStage().toInt());
+
Chunk reply = new Chunk(CHUNK_HELO, out);
/*
diff --git a/core/java/android/os/DdmSyncStageUpdater.java b/core/java/android/os/DdmSyncStageUpdater.java
new file mode 100644
index 0000000..90f7076
--- /dev/null
+++ b/core/java/android/os/DdmSyncStageUpdater.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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 android.os;
+
+import android.os.DdmSyncState.Stage;
+import android.util.Slog;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @hide
+ */
+// Making it public so ActivityThread can access it.
+public class DdmSyncStageUpdater {
+
+ private static final String TAG = "DdmSyncStageUpdater";
+
+ private static final int CHUNK_STAGE = ChunkHandler.type("STAG");
+
+ /**
+ * @hide
+ */
+ public DdmSyncStageUpdater() {
+ }
+
+ /**
+ * @hide
+ */
+ // Making it public so ActivityThread can access it.
+ public synchronized void next(Stage stage) {
+ try {
+ DdmSyncState.next(stage);
+
+ // Request DDMServer to send a STAG chunk
+ ByteBuffer data = ByteBuffer.allocate(Integer.BYTES);
+ data.putInt(stage.toInt());
+ Chunk stagChunk = new Chunk(CHUNK_STAGE, data);
+ DdmServer.sendChunk(stagChunk);
+ } catch (Exception e) {
+ // Catch everything to make sure we don't impact ActivityThread
+ Slog.w(TAG, "Unable to go to next stage" + stage, e);
+ }
+ }
+
+}
diff --git a/core/java/android/os/DdmSyncState.java b/core/java/android/os/DdmSyncState.java
new file mode 100644
index 0000000..09c9ef2
--- /dev/null
+++ b/core/java/android/os/DdmSyncState.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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 android.os;
+
+import java.util.Arrays;
+
+/**
+ * Keep track of an app boot state. The main purpose is to stream back DDM packet so a DDM client
+ * can synchronize with the app state.
+ *
+ * The state is static so it can be accessed from HELO handler.
+ *
+ * @hide
+ **/
+public final class DdmSyncState {
+
+ /**
+ * @hide
+ */
+ public enum Stage {
+ // From zygote to attach
+ Boot("BOOT"),
+
+ // From attach to handleBindApplication
+ Attach("ATCH"),
+
+ // When handleBindApplication is finally reached
+ Bind("BIND"),
+
+ // When the actual package name is known (not the early "<preinitalized>" value).
+ Named("NAMD"),
+
+ // Can be skipped if the app is not debugged.
+ Debugger("DEBG"),
+
+ // App is in RunLoop
+ Running("A_GO");
+
+ final String mLabel;
+
+ Stage(String label) {
+ if (label.length() != 4) {
+ throw new IllegalStateException(
+ "Bad stage id '" + label + "'. Must be four letters");
+ }
+ this.mLabel = label;
+ }
+
+ /**
+ * To be included in a DDM packet payload, the stage is encoded in a big-endian int
+ * @hide
+ */
+ public int toInt() {
+ int result = 0;
+ for (int i = 0; i < 4; ++i) {
+ result = ((result << 8) | (mLabel.charAt(i) & 0xff));
+ }
+ return result;
+ }
+ }
+
+ private static int sCurrentStageIndex = 0;
+
+ /**
+ * @hide
+ */
+ public static synchronized Stage getStage() {
+ return Stage.values()[sCurrentStageIndex];
+ }
+
+ /**
+ * @hide
+ */
+ public static void reset() {
+ sCurrentStageIndex = 0;
+ }
+
+ /**
+ * Search for the next level down the list of Stage. Only succeed if the next stage
+ * if a later stage (no cycling allowed).
+ *
+ * @hide
+ */
+ public static synchronized void next(Stage nextStage) {
+ Stage[] stages = Stage.values();
+ // Search for the requested next stage
+ int rover = sCurrentStageIndex;
+ while (rover < stages.length && stages[rover] != nextStage) {
+ rover++;
+ }
+
+ if (rover == stages.length || stages[rover] != nextStage) {
+ throw new IllegalStateException(
+ "Cannot go to " + nextStage + " from:" + getInternalState());
+ }
+
+ sCurrentStageIndex = rover;
+ }
+
+ /**
+ * Use to build error messages
+ * @hide
+ */
+ private static String getInternalState() {
+ StringBuilder sb = new StringBuilder("\n");
+ sb.append("level = ").append(sCurrentStageIndex);
+ sb.append("\n");
+ sb.append("stages = ");
+ sb.append(Arrays.toString(Arrays.stream(Stage.values()).map(Enum::name).toArray()));
+ sb.append("\n");
+ return sb.toString();
+ }
+}
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index e9a3254..69889c5 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -79,3 +79,7 @@
# ART
per-file ArtModuleServiceManager.java = file:platform/art:/OWNERS
+
+# DDM Protocol
+per-file DdmSyncState.java = sanglardf@google.com, rpaquay@google.com
+per-file DdmSyncStageUpdater.java = sanglardf@google.com, rpaquay@google.com
diff --git a/tests/utils/testutils/Android.bp b/tests/utils/testutils/Android.bp
index deff42a..82953ac 100644
--- a/tests/utils/testutils/Android.bp
+++ b/tests/utils/testutils/Android.bp
@@ -41,3 +41,17 @@
"mockito-target-extended-minus-junit4",
],
}
+
+java_test_host {
+ name: "frameworks-base-android-os-unittests",
+ srcs: [
+ ":framework-android-os-unit-testable-src",
+ "java/android/os/test/DdmSyncStateTest.java",
+ ],
+ static_libs: [
+ "junit",
+ ],
+ test_options: {
+ unit_test: true,
+ },
+}
diff --git a/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java b/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java
new file mode 100644
index 0000000..8274ce4
--- /dev/null
+++ b/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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 android.os.test;
+
+import android.os.DdmSyncState;
+import android.os.DdmSyncState.Stage;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test DdmSyncState, the Android app stage boot sync system for DDM Client.
+ */
+
+public class DdmSyncStateTest {
+
+ @Test
+ public void testNoCycle() {
+ DdmSyncState.reset();
+ try {
+ DdmSyncState.next(Stage.Attach);
+ DdmSyncState.next(Stage.Bind);
+ DdmSyncState.next(Stage.Named);
+ DdmSyncState.next(Stage.Debugger);
+ DdmSyncState.next(Stage.Running);
+
+ // Cycling back here which is not allowed
+ DdmSyncState.next(Stage.Attach);
+ Assert.fail("Going back to attach should have failed");
+ } catch (IllegalStateException ignored) {
+
+ }
+ }
+
+ @Test
+ public void testDebuggerFlow() {
+ DdmSyncState.reset();
+ DdmSyncState.next(Stage.Attach);
+ DdmSyncState.next(Stage.Bind);
+ DdmSyncState.next(Stage.Named);
+ DdmSyncState.next(Stage.Debugger);
+ DdmSyncState.next(Stage.Running);
+ Assert.assertEquals(Stage.Running, DdmSyncState.getStage());
+
+ }
+
+ @Test
+ public void testNoDebugFlow() {
+ DdmSyncState.reset();
+ DdmSyncState.next(Stage.Attach);
+ DdmSyncState.next(Stage.Bind);
+ DdmSyncState.next(Stage.Named);
+ // Notice how Stage.Debugger stage is skipped
+ DdmSyncState.next(Stage.Running);
+ Assert.assertEquals(Stage.Running, DdmSyncState.getStage());
+ }
+}