/* * Copyright (C) 2025 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.flagging; import android.annotation.NonNull; import android.annotation.Nullable; import android.internal.configinfra.aconfigd.x.Aconfigd.StorageRequestMessage; import android.internal.configinfra.aconfigd.x.Aconfigd.StorageRequestMessages; import android.internal.configinfra.aconfigd.x.Aconfigd.StorageReturnMessage; import android.internal.configinfra.aconfigd.x.Aconfigd.StorageReturnMessages; import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.util.Slog; import android.util.configinfrastructure.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Writes messages to aconfigd, and parses responses. * *

Uses ProtoInputStream, rather than a proto lite lib. * * @hide */ public final class AconfigdProtoStreamer { private static final String SYSTEM_SOCKET_ADDRESS = "aconfigd_system"; private static final String MAINLINE_SOCKET_ADDRESS = "aconfigd_mainline"; private static final String TAG = "FlagManager"; /** * Create a new AconfigdProtoStreamer. * * @hide */ public AconfigdProtoStreamer() {} /** * Send override removal requests to aconfigd. * * @param flags the set of flag names to remove local overrides for * @param removeType the type of removal: immediately, or on reboot * @hide */ public void sendClearFlagOverrideRequests(@NonNull Set flags, long removeType) throws IOException { ProtoOutputStream requestOutputStream = new ProtoOutputStream(); for (Flag flag : Flag.buildFlagsWithoutValues(flags)) { long msgsToken = requestOutputStream.start(StorageRequestMessages.MSGS); long msgToken = requestOutputStream.start(StorageRequestMessage.REMOVE_LOCAL_OVERRIDE_MESSAGE); requestOutputStream.write( StorageRequestMessage.RemoveLocalOverrideMessage.PACKAGE_NAME, flag.packageName); requestOutputStream.write( StorageRequestMessage.RemoveLocalOverrideMessage.FLAG_NAME, flag.flagName); requestOutputStream.write( StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_ALL, false); requestOutputStream.write( StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_OVERRIDE_TYPE, removeType); requestOutputStream.end(msgToken); requestOutputStream.end(msgsToken); } sendBytesAndParseResponse( requestOutputStream.getBytes(), StorageReturnMessage.REMOVE_LOCAL_OVERRIDE_MESSAGE); } /** * Send OTA flag-staging requests to aconfigd. * * @param flags a map from flag names to the values that will be staged * @param buildFingerprint the build fingerprint on which the values will be un-staged * @hide */ public void sendOtaFlagOverrideRequests( @NonNull Map flags, @NonNull String buildFingerprint) throws IOException { ProtoOutputStream requestOutputStream = new ProtoOutputStream(); long msgsToken = requestOutputStream.start(StorageRequestMessages.MSGS); long msgToken = requestOutputStream.start(StorageRequestMessage.OTA_STAGING_MESSAGE); requestOutputStream.write( StorageRequestMessage.OTAFlagStagingMessage.BUILD_ID, buildFingerprint); for (Flag flag : Flag.buildFlags(flags)) { long flagOverrideMsgToken = requestOutputStream.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, flag.packageName); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flag.flagName); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flag.value); requestOutputStream.end(flagOverrideMsgToken); } requestOutputStream.end(msgToken); requestOutputStream.end(msgsToken); sendBytesAndParseResponse( requestOutputStream.getBytes(), StorageReturnMessage.OTA_STAGING_MESSAGE); } /** * Send flag override requests to aconfigd, and parse the response. * * @hide */ public void sendFlagOverrideRequests(@NonNull Map flags, long overrideType) throws IOException { ProtoOutputStream requestOutputStream = new ProtoOutputStream(); for (Flag flag : Flag.buildFlags(flags)) { long msgsToken = requestOutputStream.start(StorageRequestMessages.MSGS); long msgToken = requestOutputStream.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, flag.packageName); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flag.flagName); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flag.value); requestOutputStream.write( StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, overrideType); requestOutputStream.end(msgToken); requestOutputStream.end(msgsToken); } sendBytesAndParseResponse( requestOutputStream.getBytes(), StorageReturnMessage.FLAG_OVERRIDE_MESSAGE); } private void sendBytesAndParseResponse(byte[] requestBytes, long responseMessageToken) throws IOException { try { LocalSocket systemSocket = new LocalSocket(); LocalSocketAddress systemAddress = new LocalSocketAddress( SYSTEM_SOCKET_ADDRESS, LocalSocketAddress.Namespace.RESERVED); if (!systemSocket.isConnected()) { systemSocket.connect(systemAddress); } InputStream inputStream = sendBytesOverSocket(requestBytes, systemSocket); parseAconfigdResponse(inputStream, responseMessageToken); systemSocket.shutdownInput(); systemSocket.shutdownOutput(); systemSocket.close(); } catch (IOException systemException) { Slog.i( TAG, "failed to send request to system socket; trying mainline socket", systemException); LocalSocket mainlineSocket = new LocalSocket(); LocalSocketAddress mainlineAddress = new LocalSocketAddress( MAINLINE_SOCKET_ADDRESS, LocalSocketAddress.Namespace.RESERVED); if (!mainlineSocket.isConnected()) { mainlineSocket.connect(mainlineAddress); } InputStream inputStream = sendBytesOverSocket(requestBytes, mainlineSocket); parseAconfigdResponse(inputStream, responseMessageToken); mainlineSocket.shutdownInput(); mainlineSocket.shutdownOutput(); mainlineSocket.close(); } } private InputStream sendBytesOverSocket(byte[] requestBytes, LocalSocket socket) throws IOException { InputStream responseInputStream = null; DataInputStream inputStream = new DataInputStream(socket.getInputStream()); DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); outputStream.writeInt(requestBytes.length); outputStream.write(requestBytes); responseInputStream = socket.getInputStream(); return responseInputStream; } private void parseAconfigdResponse(InputStream inputStream, long responseMessageToken) throws IOException { ProtoInputStream proto = new ProtoInputStream(inputStream); while (true) { long currentToken = proto.nextField(); if (currentToken == ProtoInputStream.NO_MORE_FIELDS) { return; } if (currentToken != ((int) StorageReturnMessages.MSGS)) { continue; } long msgsToken = proto.start(StorageReturnMessages.MSGS); long nextToken = proto.nextField(); if (nextToken == ((int) responseMessageToken)) { long msgToken = proto.start(responseMessageToken); proto.end(msgToken); } else if (nextToken == ((int) StorageReturnMessage.ERROR_MESSAGE)) { String errmsg = proto.readString(StorageReturnMessage.ERROR_MESSAGE); throw new IOException("override request failed: " + errmsg); } else if (nextToken == ProtoInputStream.NO_MORE_FIELDS) { // Do nothing. } else { throw new IOException( "invalid message type, expecting only return message" + " or error message"); } proto.end(msgsToken); } } private static class Flag { public final String packageName; public final String flagName; public final String value; public Flag( @NonNull String packageName, @NonNull String flagName, @Nullable Boolean value) { this.packageName = packageName; this.flagName = flagName; if (value != null) { this.value = Boolean.toString(value); } else { this.value = null; } } public static Set buildFlags(@NonNull Map flags) { HashSet flagSet = new HashSet(); for (Map.Entry flagAndValue : flags.entrySet()) { String packageName = ""; String flagName = flagAndValue.getKey(); int periodIndex = flagName.lastIndexOf("."); if (periodIndex != -1) { packageName = flagName.substring(0, flagName.lastIndexOf(".")); flagName = flagName.substring(flagName.lastIndexOf(".") + 1); } flagSet.add(new Flag(packageName, flagName, flagAndValue.getValue())); } return flagSet; } public static Set buildFlagsWithoutValues(@NonNull Set flags) { HashSet flagSet = new HashSet(); for (String qualifiedFlagName : flags) { String packageName = ""; String flagName = qualifiedFlagName; int periodIndex = qualifiedFlagName.lastIndexOf("."); if (periodIndex != -1) { packageName = qualifiedFlagName.substring(0, qualifiedFlagName.lastIndexOf(".")); flagName = qualifiedFlagName.substring(qualifiedFlagName.lastIndexOf(".") + 1); } flagSet.add(new Flag(packageName, flagName, null)); } return flagSet; } } }