summaryrefslogtreecommitdiff
path: root/MdnsOffloadManagerService/src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'MdnsOffloadManagerService/src/com/android')
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/InterfaceOffloadManager.java129
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java482
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsPacketParser.java220
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadIntentStore.java310
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadWriter.java283
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/PriorityListManager.java55
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/util/WakeLockWrapper.java28
7 files changed, 1507 insertions, 0 deletions
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/InterfaceOffloadManager.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/InterfaceOffloadManager.java
new file mode 100644
index 0000000..8759883
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/InterfaceOffloadManager.java
@@ -0,0 +1,129 @@
+package com.android.tv.mdnsoffloadmanager;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@WorkerThread
+public class InterfaceOffloadManager {
+
+ private static final String TAG = InterfaceOffloadManager.class.getSimpleName();
+
+ private final String mNetworkInterface;
+ private final OffloadIntentStore mOffloadIntentStore;
+ private final OffloadWriter mOffloadWriter;
+ private final Set<Integer> mCurrentOffloadKeys = new HashSet<>();
+ private final Set<String> mCurrentPassthroughQNames = new HashSet<>();
+ private boolean mIsNetworkAvailable = false;
+
+ InterfaceOffloadManager(
+ @NonNull String networkInterface,
+ @NonNull OffloadIntentStore offloadIntentStore,
+ @NonNull OffloadWriter offloadWriter) {
+ mNetworkInterface = networkInterface;
+ mOffloadIntentStore = offloadIntentStore;
+ mOffloadWriter = offloadWriter;
+ }
+
+ void onVendorServiceConnected() {
+ refreshProtocolResponses();
+ refreshPassthroughList();
+ }
+
+ void onAppIdAllowlistUpdated() {
+ refreshProtocolResponses();
+ refreshPassthroughList();
+ }
+
+ void onNetworkAvailable() {
+ String msg = "Network interface {" + mNetworkInterface + "} is connected." +
+ " Offloading all stored data.";
+ Log.d(TAG, msg);
+ mIsNetworkAvailable = true;
+ refreshProtocolResponses();
+ refreshPassthroughList();
+ }
+
+ void onNetworkLost() {
+ String msg = "Network interface {" + mNetworkInterface + "} was disconnected."
+ + " Clearing all associated data.";
+ Log.d(TAG, msg);
+ mIsNetworkAvailable = false;
+ clearProtocolResponses();
+ clearPassthroughList();
+ }
+
+ void onVendorServiceDisconnected() {
+ mCurrentOffloadKeys.clear();
+ mCurrentPassthroughQNames.clear();
+ }
+
+ void refreshProtocolResponses() {
+ if (!mIsNetworkAvailable) {
+ return;
+ }
+ applyOffloadIntents(mOffloadIntentStore.getOffloadIntentsForInterface(mNetworkInterface));
+ }
+
+ void refreshPassthroughList() {
+ if (!mIsNetworkAvailable) {
+ return;
+ }
+ applyPassthroughIntents(
+ mOffloadIntentStore.getPassthroughIntentsForInterface(mNetworkInterface));
+ }
+
+ private void clearProtocolResponses() {
+ applyOffloadIntents(Collections.emptySet());
+ }
+
+ private void clearPassthroughList() {
+ applyPassthroughIntents(Collections.emptyList());
+ }
+
+ private void applyOffloadIntents(Collection<OffloadIntentStore.OffloadIntent> offloadIntents) {
+ if (!mOffloadWriter.isVendorServiceConnected()) {
+ Log.e(TAG, "Vendor service disconnected, cannot apply mDNS offload state");
+ return;
+ }
+ Collection<Integer> deleted = mOffloadWriter.deleteOffloadData(mCurrentOffloadKeys);
+ mCurrentOffloadKeys.removeAll(deleted);
+ Collection<Integer> offloaded = mOffloadWriter.writeOffloadData(
+ mNetworkInterface, offloadIntents);
+ mCurrentOffloadKeys.addAll(offloaded);
+ }
+
+ private void applyPassthroughIntents(
+ List<OffloadIntentStore.PassthroughIntent> passthroughIntents) {
+ if (!mOffloadWriter.isVendorServiceConnected()){
+ Log.e(TAG, "Vendor service disconnected, cannot apply mDNS passthrough state");
+ return;
+ }
+ Collection<String> deleted = mOffloadWriter.deletePassthroughData(
+ mNetworkInterface, mCurrentPassthroughQNames);
+ mCurrentPassthroughQNames.removeAll(deleted);
+ Collection<String> added = mOffloadWriter.writePassthroughData(
+ mNetworkInterface, passthroughIntents);
+ mCurrentPassthroughQNames.addAll(added);
+ }
+
+ @WorkerThread
+ void dump(PrintWriter writer) {
+ writer.println("InterfaceOffloadManager[%s]:".formatted(mNetworkInterface));
+ writer.println("mIsNetworkAvailable=%b".formatted(mIsNetworkAvailable));
+ writer.println("current offload keys:");
+ mCurrentOffloadKeys.forEach(key -> writer.println("* %d".formatted(key)));
+ writer.println("current passthrough qnames:");
+ mCurrentPassthroughQNames.forEach(qname -> writer.println("* %s".formatted(qname)));
+ writer.println();
+ }
+
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java
new file mode 100644
index 0000000..c554cf8
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java
@@ -0,0 +1,482 @@
+/*
+ * 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 com.android.tv.mdnsoffloadmanager;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.tv.mdnsoffloadmanager.util.WakeLockWrapper;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import device.google.atv.mdns_offload.IMdnsOffload;
+import device.google.atv.mdns_offload.IMdnsOffloadManager;
+
+
+public class MdnsOffloadManagerService extends Service {
+
+ private static final String TAG = MdnsOffloadManagerService.class.getSimpleName();
+ private static final int VENDOR_SERVICE_COMPONENT_ID =
+ R.string.config_mdnsOffloadVendorServiceComponent;
+ private static final int AWAIT_DUMP_SECONDS = 5;
+
+ private final ConnectivityManager.NetworkCallback mNetworkCallback =
+ new ConnectivityManagerNetworkCallback();
+ private final Map<String, InterfaceOffloadManager> mInterfaceOffloadManagers = new HashMap<>();
+ private final Injector mInjector;
+ private Handler mHandler;
+ private PriorityListManager mPriorityListManager;
+ private OffloadIntentStore mOffloadIntentStore;
+ private OffloadWriter mOffloadWriter;
+ private ConnectivityManager mConnectivityManager;
+ private PackageManager mPackageManager;
+ private WakeLockWrapper mWakeLock;
+
+ public MdnsOffloadManagerService() {
+ this(new Injector());
+ }
+
+ @VisibleForTesting
+ MdnsOffloadManagerService(@NonNull Injector injector) {
+ super();
+ injector.setContext(this);
+ mInjector = injector;
+ }
+
+ @VisibleForTesting
+ static class Injector {
+
+ private Context mContext = null;
+ private Looper mLooper = null;
+
+ void setContext(Context context) {
+ mContext = context;
+ }
+
+ synchronized Looper getLooper() {
+ if (mLooper == null) {
+ HandlerThread ht = new HandlerThread("MdnsOffloadManager");
+ ht.start();
+ mLooper = ht.getLooper();
+ }
+ return mLooper;
+ }
+
+ Resources getResources() {
+ return mContext.getResources();
+ }
+
+ ConnectivityManager getConnectivityManager() {
+ return mContext.getSystemService(ConnectivityManager.class);
+ }
+
+
+ PowerManager.LowPowerStandbyPolicy getLowPowerStandbyPolicy() {
+ return mContext.getSystemService(PowerManager.class).getLowPowerStandbyPolicy();
+ }
+
+ WakeLockWrapper newWakeLock() {
+ return new WakeLockWrapper(
+ mContext.getSystemService(PowerManager.class)
+ .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG));
+ }
+
+ PackageManager getPackageManager() {
+ return mContext.getPackageManager();
+ }
+
+ boolean isInteractive() {
+ return mContext.getSystemService(PowerManager.class).isInteractive();
+ }
+
+ boolean bindService(Intent intent, ServiceConnection connection, int flags) {
+ return mContext.bindService(intent, connection, flags);
+ }
+
+ void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+ mContext.registerReceiver(receiver, filter, flags);
+ }
+
+ int getCallingUid() {
+ return Binder.getCallingUid();
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(mInjector.getLooper());
+ mPriorityListManager = new PriorityListManager(mInjector.getResources());
+ mOffloadIntentStore = new OffloadIntentStore(mPriorityListManager);
+ mOffloadWriter = new OffloadWriter();
+ mConnectivityManager = mInjector.getConnectivityManager();
+ mPackageManager = mInjector.getPackageManager();
+ mWakeLock = mInjector.newWakeLock();
+ bindVendorService();
+ setupScreenBroadcastReceiver();
+ setupConnectivityListener();
+ setupStandbyPolicyListener();
+ }
+
+ private void bindVendorService() {
+ String vendorServicePath = mInjector.getResources().getString(VENDOR_SERVICE_COMPONENT_ID);
+
+ if (vendorServicePath.isEmpty()) {
+ String msg = "vendorServicePath is empty. Bind cannot proceed.";
+ Log.e(TAG, msg);
+ throw new IllegalArgumentException(msg);
+ }
+ ComponentName componentName = ComponentName.unflattenFromString(vendorServicePath);
+ if (componentName == null) {
+ String msg = "componentName cannot be extracted from vendorServicePath."
+ + " Bind cannot proceed.";
+ Log.e(TAG, msg);
+ throw new IllegalArgumentException(msg);
+ }
+
+ Log.d(TAG, "IMdnsOffloadManager is binding to: " + componentName);
+
+ Intent explicitIntent = new Intent();
+ explicitIntent.setComponent(componentName);
+ boolean bindingSuccessful = mInjector.bindService(
+ explicitIntent, mVendorServiceConnection, Context.BIND_AUTO_CREATE);
+ if (!bindingSuccessful) {
+ String msg = "Failed to bind to vendor service at {" + vendorServicePath + "}.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+ }
+
+ private void setupScreenBroadcastReceiver() {
+ BroadcastReceiver receiver = new ScreenBroadcastReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ mInjector.registerReceiver(receiver, filter, 0);
+ mHandler.post(() -> mOffloadWriter.setOffloadState(!mInjector.isInteractive()));
+ }
+
+ private void setupConnectivityListener() {
+ NetworkRequest networkRequest = new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .build();
+ mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkCallback);
+ }
+
+ private void setupStandbyPolicyListener() {
+ BroadcastReceiver receiver = new LowPowerStandbyPolicyReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(PowerManager.ACTION_LOW_POWER_STANDBY_POLICY_CHANGED);
+ mInjector.registerReceiver(receiver, filter, 0);
+ refreshAppIdAllowlist();
+ }
+
+ private void refreshAppIdAllowlist() {
+ PowerManager.LowPowerStandbyPolicy standbyPolicy = mInjector.getLowPowerStandbyPolicy();
+ Set<Integer> allowedAppIds = standbyPolicy.getExemptPackages()
+ .stream()
+ .map(pkg -> {
+ try {
+ return mPackageManager.getPackageUid(pkg, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Unable to get UID of package {" + pkg + "}.");
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .map(UserHandle::getAppId)
+ .collect(Collectors.toSet());
+ mHandler.post(() -> {
+ mOffloadIntentStore.setAppIdAllowlist(allowedAppIds);
+ mInterfaceOffloadManagers.values()
+ .forEach(InterfaceOffloadManager::onAppIdAllowlistUpdated);
+ });
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mOffloadManagerBinder;
+ }
+
+ @Override
+ protected void dump(FileDescriptor fileDescriptor, PrintWriter printWriter, String[] strings) {
+ CountDownLatch doneSignal = new CountDownLatch(1);
+ mHandler.post(() -> {
+ dump(printWriter);
+ doneSignal.countDown();
+ });
+ boolean success = false;
+ try {
+ success = doneSignal.await(AWAIT_DUMP_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException ignored) {
+ }
+ if (!success) {
+ Log.e(TAG, "Failed to dump state on handler thread");
+ }
+ }
+
+ @WorkerThread
+ private void dump(PrintWriter writer) {
+ mOffloadIntentStore.dump(writer);
+ mInterfaceOffloadManagers.values().forEach(manager -> manager.dump(writer));
+ mOffloadWriter.dump(writer);
+ mOffloadIntentStore.dumpProtocolData(writer);
+ }
+
+ private final IMdnsOffloadManager.Stub mOffloadManagerBinder = new IMdnsOffloadManager.Stub() {
+ @Override
+ public int addProtocolResponses(@NonNull String networkInterface,
+ @NonNull OffloadServiceInfo serviceOffloadData,
+ @NonNull IBinder clientToken) {
+ Objects.requireNonNull(networkInterface);
+ Objects.requireNonNull(serviceOffloadData);
+ Objects.requireNonNull(clientToken);
+ int callerUid = mInjector.getCallingUid();
+ OffloadIntentStore.OffloadIntent offloadIntent =
+ mOffloadIntentStore.registerOffloadIntent(
+ networkInterface, serviceOffloadData, clientToken, callerUid);
+ try {
+ offloadIntent.mClientToken.linkToDeath(
+ () -> removeProtocolResponses(offloadIntent.mRecordKey, clientToken), 0);
+ } catch (RemoteException e) {
+ String msg = "Error while setting a callback for linkToDeath binder" +
+ " {" + offloadIntent.mClientToken + "} in addProtocolResponses.";
+ Log.e(TAG, msg, e);
+ return offloadIntent.mRecordKey;
+ }
+ mHandler.post(() -> {
+ getInterfaceOffloadManager(networkInterface).refreshProtocolResponses();
+ });
+ return offloadIntent.mRecordKey;
+ }
+
+ @Override
+ public void removeProtocolResponses(int recordKey, @NonNull IBinder clientToken) {
+ if (recordKey <= 0) {
+ throw new IllegalArgumentException("recordKey must be positive");
+ }
+ Objects.requireNonNull(clientToken);
+ mHandler.post(() -> {
+ OffloadIntentStore.OffloadIntent offloadIntent =
+ mOffloadIntentStore.getAndRemoveOffloadIntent(recordKey, clientToken);
+ if (offloadIntent == null) {
+ return;
+ }
+ getInterfaceOffloadManager(offloadIntent.mNetworkInterface)
+ .refreshProtocolResponses();
+ });
+ }
+
+ @Override
+ public void addToPassthroughList(
+ @NonNull String networkInterface,
+ @NonNull String qname,
+ @NonNull IBinder clientToken) {
+ Objects.requireNonNull(networkInterface);
+ Objects.requireNonNull(qname);
+ Objects.requireNonNull(clientToken);
+ int callerUid = mInjector.getCallingUid();
+ mHandler.post(() -> {
+ OffloadIntentStore.PassthroughIntent ptIntent =
+ mOffloadIntentStore.registerPassthroughIntent(
+ networkInterface, qname, clientToken, callerUid);
+ IBinder token = ptIntent.mClientToken;
+ try {
+ token.linkToDeath(
+ () -> removeFromPassthroughList(
+ networkInterface, ptIntent.mCanonicalQName, token), 0);
+ } catch (RemoteException e) {
+ String msg = "Error while setting a callback for linkToDeath binder {"
+ + token + "} in addToPassthroughList.";
+ Log.e(TAG, msg, e);
+ return;
+ }
+ getInterfaceOffloadManager(networkInterface).refreshPassthroughList();
+ });
+ }
+
+ @Override
+ public void removeFromPassthroughList(
+ @NonNull String networkInterface,
+ @NonNull String qname,
+ @NonNull IBinder clientToken) {
+ Objects.requireNonNull(networkInterface);
+ Objects.requireNonNull(qname);
+ Objects.requireNonNull(clientToken);
+ mHandler.post(() -> {
+ boolean removed = mOffloadIntentStore.removePassthroughIntent(qname, clientToken);
+ if (removed) {
+ getInterfaceOffloadManager(networkInterface).refreshPassthroughList();
+ }
+ });
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return super.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return super.HASH;
+ }
+ };
+
+ private InterfaceOffloadManager getInterfaceOffloadManager(String networkInterface) {
+ return mInterfaceOffloadManagers.computeIfAbsent(
+ networkInterface,
+ iface -> new InterfaceOffloadManager(iface, mOffloadIntentStore, mOffloadWriter));
+ }
+
+ private final ServiceConnection mVendorServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.i(TAG, "IMdnsOffload service bound successfully.");
+ IMdnsOffload vendorService = IMdnsOffload.Stub.asInterface(service);
+ mHandler.post(() -> {
+ mOffloadWriter.setVendorService(vendorService);
+ mOffloadWriter.resetAll();
+ mInterfaceOffloadManagers.values()
+ .forEach(InterfaceOffloadManager::onVendorServiceConnected);
+ mOffloadWriter.applyOffloadState();
+ });
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ Log.e(TAG, "IMdnsOffload service has unexpectedly disconnected.");
+ mHandler.post(() -> {
+ mOffloadWriter.setVendorService(null);
+ mInterfaceOffloadManagers.values()
+ .forEach(InterfaceOffloadManager::onVendorServiceDisconnected);
+ });
+ }
+ };
+
+ private class ScreenBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Note: Screen on/off here is actually historical naming for the overall interactive
+ // state of the device:
+ // https://developer.android.com/reference/android/os/PowerManager#isInteractive()
+ String action = intent.getAction();
+ mHandler.post(() -> {
+ if (Intent.ACTION_SCREEN_ON.equals(action)) {
+ mOffloadWriter.setOffloadState(false);
+ mOffloadWriter.retrieveAndClearMetrics(mOffloadIntentStore.getRecordKeys());
+ } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ try {
+ mWakeLock.acquire(5000);
+ mOffloadWriter.setOffloadState(true);
+ } finally {
+ mWakeLock.release();
+ }
+ }
+ });
+ }
+ }
+
+ private class LowPowerStandbyPolicyReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!PowerManager.ACTION_LOW_POWER_STANDBY_POLICY_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ refreshAppIdAllowlist();
+ }
+ }
+
+ private class ConnectivityManagerNetworkCallback extends ConnectivityManager.NetworkCallback {
+ private final Map<Network, LinkProperties> mLinkProperties = new HashMap<>();
+
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+ // We only want to know the interface name of a network. This method is
+ // called right after onAvailable() or any other important change during the lifecycle
+ // of the network.
+ mHandler.post(() -> {
+ LinkProperties previousProperties = mLinkProperties.put(network, linkProperties);
+ if (previousProperties != null &&
+ !previousProperties.getInterfaceName().equals(
+ linkProperties.getInterfaceName())) {
+ // This means that the interface changed names, which may happen
+ // but very rarely.
+ InterfaceOffloadManager offloadManager =
+ getInterfaceOffloadManager(previousProperties.getInterfaceName());
+ offloadManager.onNetworkLost();
+ }
+
+ // We trigger an onNetworkAvailable even if the existing is the same in case
+ // anything needs to be refreshed due to the LinkProperties change.
+ InterfaceOffloadManager offloadManager =
+ getInterfaceOffloadManager(linkProperties.getInterfaceName());
+ offloadManager.onNetworkAvailable();
+ });
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ mHandler.post(() -> {
+ // Network object is guaranteed to match a network object from a previous
+ // onLinkPropertiesChanged() so the LinkProperties must be available to retrieve
+ // the associated iface.
+ LinkProperties previousProperties = mLinkProperties.remove(network);
+ if (previousProperties == null){
+ Log.w(TAG,"Network "+ network + " lost before being available.");
+ return;
+ }
+ InterfaceOffloadManager offloadManager =
+ getInterfaceOffloadManager(previousProperties.getInterfaceName());
+ offloadManager.onNetworkLost();
+ });
+ }
+ }
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsPacketParser.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsPacketParser.java
new file mode 100644
index 0000000..cd930dc
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsPacketParser.java
@@ -0,0 +1,220 @@
+/*
+ * 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 com.android.tv.mdnsoffloadmanager;
+
+import androidx.annotation.NonNull;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import device.google.atv.mdns_offload.IMdnsOffload.MdnsProtocolData.MatchCriteria;
+
+/**
+ * Tool class to help read mdns data from a fully formed mDNS response packet.
+ */
+public final class MdnsPacketParser {
+
+ private static final int OFFSET_QUERIES_COUNT = 4;
+ private static final int OFFSET_ANSWERS_COUNT = 6;
+ private static final int OFFSET_AUTHORITY_COUNT = 8;
+ private static final int OFFSET_ADDITIONAL_COUNT = 10;
+ private static final int OFFSET_DATA_SECTION_START = 12;
+
+ private final byte[] mMdnsData;
+ private int mCursorIndex;
+
+ private MdnsPacketParser(@NonNull byte[] mDNSData) {
+ this.mMdnsData = mDNSData;
+ }
+
+ /**
+ * Extracts a label starting at offset and then follows RFC1035-4.1.4 The offset should start
+ * either at a data length value or at a pointer value.
+ */
+ public static String extractFullName(@NonNull byte[] array, int offset) {
+ MdnsPacketParser parser = new MdnsPacketParser(array);
+ parser.setCursor(offset);
+ StringBuilder builder = new StringBuilder();
+
+ while (!parser.isCursorOnRootLabel()) {
+ if (parser.isCursorOnPointer()) {
+ parser.setCursor(parser.pollPointerOffset());
+ } else if (parser.isCursorOnLabel()) {
+ builder.append(parser.pollLabel());
+ builder.append('.');
+ } else {
+ throw new IllegalArgumentException("mDNS response packet is badly formed.");
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Finds all the RRNAMEs ans RRTYPEs in the mdns response packet provided. Expects a packet only
+ * with responses.
+ */
+ public static List<MatchCriteria> extractMatchCriteria(@NonNull byte[] mdnsResponsePacket) {
+ Objects.requireNonNull(mdnsResponsePacket);
+
+ // Parse MdnsPacket and read labels and find
+ List<MatchCriteria> criteriaList = new ArrayList<>();
+ MdnsPacketParser parser = new MdnsPacketParser(mdnsResponsePacket);
+
+ if (parser.getQueriesCount() != 0
+ || parser.getAuthorityCount() != 0
+ || parser.getAdditionalCount() != 0) {
+ throw new IllegalArgumentException(
+ "mDNS response packet contains data that is not answers");
+ }
+ int answersToRead = parser.getAnswersCount();
+
+ parser.moveToDataSection();
+ while (answersToRead > 0) {
+ // Each record starts with the RRNAME, so the offset is correct for the criteria.
+ MatchCriteria criteria = new MatchCriteria();
+ criteria.nameOffset = parser.getCursorOffset();
+
+ /// Skip labels first
+ while (parser.isCursorOnLabel()) {
+ parser.pollLabel();
+ }
+ // We can be on a root label or on a pointer. Skip both.
+ if (parser.isCursorOnRootLabel()) {
+ parser.skipBytes(1);
+ } else if (parser.isCursorOnPointer()) {
+ parser.pollPointerOffset();
+ }
+
+ // The cursor must be on the RRTYPE.
+ criteria.type = parser.pollUint16();
+
+ // The next 6 bytes point to cache flush, rrclass, and ttl
+ parser.skipBytes(6);
+
+ // Now the index points to the data length on 2 bytes
+ int dataLength = parser.pollUint16();
+
+ // Then we can skip those data bytes.
+ parser.skipBytes(dataLength);
+
+ // Criteria is complete, it can be added.
+ criteriaList.add(criteria);
+ answersToRead--;
+ }
+ if (parser.hasContent()) {
+ // The packet is badly formed. All answers where read successfully, but data remains
+ // available.
+ throw new IllegalArgumentException(
+ "mDNS response packet is badly formed. Too much data.");
+ }
+
+ return criteriaList;
+ }
+
+ private boolean hasContent() {
+ return mCursorIndex < mMdnsData.length;
+ }
+
+ private void setCursor(int position) {
+ if (position < 0) {
+ throw new IllegalArgumentException("Setting cursor on negative offset is not allowed.");
+ }
+ mCursorIndex = position;
+ }
+
+ private int getCursorOffset() {
+ return mCursorIndex;
+ }
+
+ private void skipBytes(int bytesToSkip) {
+ mCursorIndex += bytesToSkip;
+ }
+
+ private int getQueriesCount() {
+ return readUint16(OFFSET_QUERIES_COUNT);
+ }
+
+ private int getAnswersCount() {
+ return readUint16(OFFSET_ANSWERS_COUNT);
+ }
+
+ private int getAuthorityCount() {
+ return readUint16(OFFSET_AUTHORITY_COUNT);
+ }
+
+ private int getAdditionalCount() {
+ return readUint16(OFFSET_ADDITIONAL_COUNT);
+ }
+
+ private void moveToDataSection() {
+ mCursorIndex = OFFSET_DATA_SECTION_START;
+ }
+
+ private String pollLabel() {
+ int labelSize = readUint8(mCursorIndex);
+ mCursorIndex++;
+ if (mCursorIndex + labelSize > mMdnsData.length) {
+ throw new IllegalArgumentException(
+ "mDNS response packet is badly formed. Not enough data.");
+ }
+ String value = new String(mMdnsData, mCursorIndex, labelSize, StandardCharsets.UTF_8);
+ mCursorIndex += labelSize;
+ return value;
+ }
+
+ private boolean isCursorOnLabel() {
+ return !isCursorOnRootLabel() && (readUint8(mCursorIndex) & 0b11000000) == 0b00000000;
+ }
+
+ private boolean isCursorOnPointer() {
+ return (readUint8(mCursorIndex) & 0b11000000) == 0b11000000;
+ }
+
+ private boolean isCursorOnRootLabel() {
+ return readUint8(mCursorIndex) == 0;
+ }
+
+ private int pollPointerOffset() {
+ int value = readUint16(mCursorIndex) & 0b0011111111111111;
+ mCursorIndex += 2;
+ return value;
+ }
+
+ private int readUint8(int offset) {
+ if (offset >= mMdnsData.length) {
+ throw new IllegalArgumentException(
+ "mDNS response packet is badly formed. Not enough data.");
+ }
+ return ((int) mMdnsData[offset]) & 0xff;
+ }
+
+ private int readUint16(int offset) {
+ if (offset + 1 >= mMdnsData.length) {
+ throw new IllegalArgumentException(
+ "mDNS response packet is badly formed. Not enough data.");
+ }
+ return (readUint8(offset) << 8) + readUint8(offset + 1);
+ }
+
+ private int pollUint16() {
+ int value = readUint16(mCursorIndex);
+ mCursorIndex += 2;
+ return value;
+ }
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadIntentStore.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadIntentStore.java
new file mode 100644
index 0000000..bd408c7
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadIntentStore.java
@@ -0,0 +1,310 @@
+package com.android.tv.mdnsoffloadmanager;
+
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import device.google.atv.mdns_offload.IMdnsOffload;
+import device.google.atv.mdns_offload.IMdnsOffloadManager;
+
+/**
+ * Class to store OffloadIntents made by clients and assign record keys.
+ */
+public class OffloadIntentStore {
+
+ private static final String TAG = OffloadIntentStore.class.getSimpleName();
+
+ private final AtomicInteger mNextKey = new AtomicInteger(1);
+ private final ConcurrentMap<Integer, OffloadIntent> mOffloadIntentsByRecordKey =
+ new ConcurrentHashMap<>();
+ // Note that we need to preserve the order of passthrough intents.
+ private final List<PassthroughIntent> mPassthroughIntents = new ArrayList<>();
+
+ private final PriorityListManager mPriorityListManager;
+
+ /**
+ * Only listed packages may offload data or manage the passthrough list, requests from any other
+ * packages are dropped.
+ */
+ private final Set<Integer> mAppIdAllowlist = new HashSet<>();
+
+ OffloadIntentStore(@NonNull PriorityListManager priorityListManager) {
+ mPriorityListManager = priorityListManager;
+ }
+
+ @WorkerThread
+ void setAppIdAllowlist(Set<Integer> appIds) {
+ mAppIdAllowlist.clear();
+ mAppIdAllowlist.addAll(appIds);
+ }
+
+ /**
+ * Register the intention to offload an mDNS service. The system will do its best to offload it
+ * when possible (considering dependencies, network conditions etc.).
+ * <p>
+ * The offload intent will be associated with the caller via the clientToken, stored in the
+ * internal memory store, and be assigned a unique record key.
+ */
+ OffloadIntent registerOffloadIntent(
+ String networkInterface,
+ IMdnsOffloadManager.OffloadServiceInfo serviceInfo,
+ IBinder clientToken,
+ int callerUid) {
+ int recordKey = mNextKey.getAndIncrement();
+ IMdnsOffload.MdnsProtocolData mdnsProtocolData = convertToMdnsProtocolData(serviceInfo);
+ int priority = mPriorityListManager.getPriority(mdnsProtocolData, recordKey);
+ int appId = UserHandle.getAppId(callerUid);
+ OffloadIntent offloadIntent = new OffloadIntent(
+ networkInterface, recordKey, mdnsProtocolData, clientToken, priority, appId);
+ mOffloadIntentsByRecordKey.put(recordKey, offloadIntent);
+ return offloadIntent;
+ }
+
+ /**
+ * Retrieve all offload intents for a given interface.
+ */
+ @WorkerThread
+ Collection<OffloadIntent> getOffloadIntentsForInterface(String networkInterface) {
+ return mOffloadIntentsByRecordKey
+ .values()
+ .stream()
+ .filter(intent -> intent.mNetworkInterface.equals(networkInterface)
+ && mAppIdAllowlist.contains(intent.mOwnerAppId))
+ .toList();
+ }
+
+ /**
+ * Retrieve an offload intent by its record key and remove from internal database.
+ * <p>
+ * Only permitted if the offload intent was registered by the same caller.
+ */
+ @WorkerThread
+ OffloadIntent getAndRemoveOffloadIntent(int recordKey, IBinder clientToken) {
+ OffloadIntent offloadIntent = mOffloadIntentsByRecordKey.get(recordKey);
+ if (offloadIntent == null) {
+ Log.e(TAG, "Failed to remove protocol responses, bad record key {"
+ + recordKey + "}.");
+ return null;
+ }
+ if (!offloadIntent.mClientToken.equals(clientToken)) {
+ Log.e(TAG, "Failed to remove protocol messages, bad client token {"
+ + clientToken + "}.");
+ return null;
+ }
+ mOffloadIntentsByRecordKey.remove(recordKey);
+ return offloadIntent;
+ }
+
+ @WorkerThread
+ Collection<Integer> getRecordKeys() {
+ return mOffloadIntentsByRecordKey.keySet();
+ }
+
+ /**
+ * Create a passthrough intent, representing the intention to add a DNS query name to the
+ * passthrough list. The system will do its best to configure the passthrough when possible.
+ * <p>
+ * The passthrough intent will be associated with the caller via the clientToken, stored in the
+ * internal memory store, and identified by the passthrough QNAME.
+ */
+ @WorkerThread
+ PassthroughIntent registerPassthroughIntent(
+ String networkInterface,
+ String qname,
+ IBinder clientToken,
+ int callerUid) {
+ String canonicalQName = mPriorityListManager.canonicalQName(qname);
+ int priority = mPriorityListManager.getPriority(canonicalQName, 0);
+ int appId = UserHandle.getAppId(callerUid);
+ PassthroughIntent passthroughIntent = new PassthroughIntent(
+ networkInterface, qname, canonicalQName, clientToken, priority, appId);
+ mPassthroughIntents.add(passthroughIntent);
+ return passthroughIntent;
+ }
+
+ /**
+ * Retrieve all passthrough intents for a given interface.
+ */
+ @WorkerThread
+ List<PassthroughIntent> getPassthroughIntentsForInterface(String networkInterface) {
+ return mPassthroughIntents
+ .stream()
+ .filter(intent -> intent.mNetworkInterface.equals(networkInterface)
+ && mAppIdAllowlist.contains(intent.mOwnerAppId))
+ .toList();
+ }
+
+ /**
+ * Retrieve a passthrough intent by its QNAME remove from internal database.
+ * <p>
+ * Only permitted if the passthrough intent was registered by the same caller.
+ */
+ @WorkerThread
+ boolean removePassthroughIntent(String qname, IBinder clientToken) {
+ String canonicalQName = mPriorityListManager.canonicalQName(qname);
+ boolean removed = mPassthroughIntents.removeIf(
+ pt -> pt.mCanonicalQName.equals(canonicalQName)
+ && pt.mClientToken.equals(clientToken));
+ if (!removed) {
+ Log.e(TAG, "Failed to remove passthrough intent, bad QNAME or client token.");
+ return false;
+ }
+ return true;
+ }
+
+ private static IMdnsOffload.MdnsProtocolData convertToMdnsProtocolData(
+ IMdnsOffloadManager.OffloadServiceInfo serviceData) {
+ IMdnsOffload.MdnsProtocolData data = new IMdnsOffload.MdnsProtocolData();
+ data.rawOffloadPacket = serviceData.rawOffloadPacket;
+ data.matchCriteriaList = MdnsPacketParser.extractMatchCriteria(
+ serviceData.rawOffloadPacket);
+ return data;
+ }
+
+ @WorkerThread
+ void dump(PrintWriter writer) {
+ writer.println("OffloadIntentStore:");
+ writer.println("offload intents:");
+ mOffloadIntentsByRecordKey.values()
+ .forEach(intent -> writer.println("* %s".formatted(intent)));
+ writer.println("passthrough intents:");
+ mPassthroughIntents.forEach(intent -> writer.println("* %s".formatted(intent)));
+ writer.println();
+ }
+
+ /**
+ * Create a detailed dump of the OffloadIntents, including a hexdump of the raw packets.
+ */
+ @WorkerThread
+ void dumpProtocolData(PrintWriter writer) {
+ writer.println("Protocol data dump:");
+ mOffloadIntentsByRecordKey.values().forEach(intent -> {
+ writer.println("mRecordKey=%d".formatted(intent.mRecordKey));
+ IMdnsOffload.MdnsProtocolData data = intent.mProtocolData;
+ writer.println("match criteria:");
+ data.matchCriteriaList.forEach(criteria ->
+ writer.println("* %s".formatted(formatMatchCriteria(criteria))));
+ writer.println("raw offload packet:");
+ hexDump(writer, data.rawOffloadPacket);
+ });
+ writer.println();
+ }
+
+ /**
+ * Class representing the intention to offload mDNS protocol data.
+ */
+ static class OffloadIntent {
+ final String mNetworkInterface;
+ final int mRecordKey;
+ final IMdnsOffload.MdnsProtocolData mProtocolData;
+ final IBinder mClientToken;
+ final int mPriority; // Lower values take precedence.
+ final int mOwnerAppId;
+
+ private OffloadIntent(
+ String networkInterface,
+ int recordKey,
+ IMdnsOffload.MdnsProtocolData protocolData,
+ IBinder clientToken,
+ int priority,
+ int ownerAppId
+ ) {
+ mNetworkInterface = networkInterface;
+ mRecordKey = recordKey;
+ mProtocolData = protocolData;
+ mClientToken = clientToken;
+ mPriority = priority;
+ mOwnerAppId = ownerAppId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("OffloadIntent{");
+ sb.append("mNetworkInterface='").append(mNetworkInterface).append('\'');
+ sb.append(", mRecordKey=").append(mRecordKey);
+ sb.append(", mPriority=").append(mPriority);
+ sb.append(", mOwnerAppId=").append(mOwnerAppId);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Class representing the intention to configure mDNS passthrough for a given query name.
+ */
+ static class PassthroughIntent {
+ final String mNetworkInterface;
+ // Preserving the original upper/lowercase format.
+ final String mOriginalQName;
+ final String mCanonicalQName;
+ final IBinder mClientToken;
+ final int mPriority;
+ final int mOwnerAppId;
+
+ PassthroughIntent(
+ String networkInterface,
+ String originalQName,
+ String canonicalQName,
+ IBinder clientToken,
+ int priority,
+ int ownerAppId) {
+ mNetworkInterface = networkInterface;
+ mOriginalQName = originalQName;
+ mCanonicalQName = canonicalQName;
+ mClientToken = clientToken;
+ mPriority = priority;
+ mOwnerAppId = ownerAppId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PassthroughIntent{");
+ sb.append("mNetworkInterface='").append(mNetworkInterface).append('\'');
+ sb.append(", mOriginalQName='").append(mOriginalQName).append('\'');
+ sb.append(", mCanonicalQName='").append(mCanonicalQName).append('\'');
+ sb.append(", mPriority=").append(mPriority);
+ sb.append(", mOwnerAppId=").append(mOwnerAppId);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+
+ private String formatMatchCriteria(IMdnsOffload.MdnsProtocolData.MatchCriteria matchCriteria) {
+ return "MatchCriteria{type=%d, nameOffset=%d}"
+ .formatted(matchCriteria.type, matchCriteria.nameOffset);
+ }
+
+ private void hexDump(PrintWriter writer, byte[] data) {
+ final int width = 16;
+ for (int rowOffset = 0; rowOffset < data.length; rowOffset += width) {
+ writer.printf("%06d: ", rowOffset);
+
+ for (int index = 0; index < width; index++) {
+ if (rowOffset + index < data.length) {
+ writer.printf("%02x ", data[rowOffset + index]);
+ } else {
+ writer.print(" ");
+ }
+ }
+
+ int asciiWidth = Math.min(width, data.length - rowOffset);
+ writer.print(" | ");
+ writer.println(new String(data, rowOffset, asciiWidth, StandardCharsets.US_ASCII)
+ .replaceAll("[^\\x20-\\x7E]", "."));
+ }
+ }
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadWriter.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadWriter.java
new file mode 100644
index 0000000..9d874d7
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/OffloadWriter.java
@@ -0,0 +1,283 @@
+package com.android.tv.mdnsoffloadmanager;
+
+import static device.google.atv.mdns_offload.IMdnsOffload.PassthroughBehavior.DROP_ALL;
+import static device.google.atv.mdns_offload.IMdnsOffload.PassthroughBehavior.PASSTHROUGH_LIST;
+
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import device.google.atv.mdns_offload.IMdnsOffload;
+
+@WorkerThread
+public class OffloadWriter {
+
+ private static final String TAG = OffloadWriter.class.getSimpleName();
+ private static final int INVALID_OFFLOAD_KEY = -1;
+
+ private boolean mOffloadState = false;
+ private IMdnsOffload mVendorService;
+
+ @NonNull
+ private static String convertQNameForVendorService(String qname) {
+ // We strip the trailing '.' when we provide QNames to the vendor service.
+ if (qname.endsWith(".")) {
+ return qname.substring(0, qname.length() - 1);
+ }
+ return qname;
+ }
+
+ private static String passthroughBehaviorToString(
+ @IMdnsOffload.PassthroughBehavior byte passthroughBehavior) {
+ switch (passthroughBehavior) {
+ case IMdnsOffload.PassthroughBehavior.FORWARD_ALL:
+ return "FORWARD_ALL";
+ case IMdnsOffload.PassthroughBehavior.DROP_ALL:
+ return "DROP_ALL";
+ case IMdnsOffload.PassthroughBehavior.PASSTHROUGH_LIST:
+ return "PASSTHROUGH_LIST";
+ }
+ throw new IllegalArgumentException("No such passthrough behavior " + passthroughBehavior);
+ }
+
+ void setVendorService(@Nullable IMdnsOffload vendorService) {
+ mVendorService = vendorService;
+ }
+
+ boolean isVendorServiceConnected() {
+ return mVendorService != null;
+ }
+
+ void resetAll() {
+ if (!isVendorServiceConnected()) {
+ Log.e(TAG, "Cannot reset vendor service, service is not connected.");
+ return;
+ }
+ try {
+ mVendorService.resetAll();
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Failed to reset vendor service.", e);
+ }
+ }
+
+ /**
+ * Apply the desired offload state on the vendor service. It may be necessary to refresh it,
+ * after we bind to the vendor service to set the initial state, or restore the previous state.
+ */
+ void applyOffloadState() {
+ setOffloadState(mOffloadState);
+ }
+
+ /**
+ * Set the desired offload state and propagate to the vendor service.
+ */
+ void setOffloadState(boolean enabled) {
+ if (!isVendorServiceConnected()) {
+ Log.e(TAG, "Cannot set offload state, vendor service is not connected.");
+ return;
+ }
+ try {
+ mVendorService.setOffloadState(enabled);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Failed to set offload state to {" + enabled + "}.", e);
+ }
+ mOffloadState = enabled;
+ }
+
+ /**
+ * Retrieve and clear all metric counters.
+ * <p>
+ * TODO(b/270115511) do something with these metrics.
+ */
+ void retrieveAndClearMetrics(Collection<Integer> recordKeys) {
+ try {
+ int missCounter = mVendorService.getAndResetMissCounter();
+ Log.d(TAG, "Missed queries:" + missCounter);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "getAndResetMissCounter failure", e);
+ }
+ for (int recordKey : recordKeys) {
+ try {
+ int hitCounter = mVendorService.getAndResetHitCounter(recordKey);
+ Log.d(TAG, "Hits for record " + recordKey + " : " + hitCounter);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "getAndResetHitCounter failure for recordKey {" + recordKey + "}", e);
+ }
+ }
+ }
+
+ /**
+ * Offload a list of records. Records are prioritized by their priority value, and lower
+ * priority records may be dropped if not all fit in memory.
+ *
+ * @return The offload keys of successfully offloaded protocol responses.
+ */
+ Collection<Integer> writeOffloadData(
+ String networkInterface, Collection<OffloadIntentStore.OffloadIntent> offloadIntents) {
+ List<OffloadIntentStore.OffloadIntent> orderedOffloadIntents = offloadIntents
+ .stream()
+ .sorted(Comparator.comparingInt(offloadIntent -> offloadIntent.mPriority))
+ .toList();
+ Set<Integer> offloaded = new HashSet<>();
+ for (OffloadIntentStore.OffloadIntent offloadIntent : orderedOffloadIntents) {
+ Integer offloadKey = tryAddProtocolResponses(networkInterface, offloadIntent);
+ if (offloadKey != null) {
+ offloaded.add(offloadKey);
+ }
+ }
+ return offloaded;
+ }
+
+ /**
+ * Remove a set of protocol responses.
+ *
+ * @return The offload keys of deleted protocol responses.
+ */
+ Collection<Integer> deleteOffloadData(Set<Integer> offloadKeys) {
+ Set<Integer> deleted = new HashSet<>();
+ for (Integer offloadKey : offloadKeys) {
+ if (tryRemoveProtocolResponses(offloadKey)) {
+ deleted.add(offloadKey);
+ }
+ }
+ return deleted;
+ }
+
+ /**
+ * Add a list of entries to the passthrough list. Entries will be prioritized based on the
+ * supplied priority value, where the supplied order will be maintained for equal values. Lower
+ * priority records may be dropped if not all fit in memory.
+ *
+ * @return The set of successfully added passthrough entries.
+ */
+ Collection<String> writePassthroughData(
+ String networkInterface,
+ List<OffloadIntentStore.PassthroughIntent> ptIntents) {
+ byte passthroughMode = ptIntents.isEmpty() ? DROP_ALL : PASSTHROUGH_LIST;
+ trySetPassthroughBehavior(networkInterface, passthroughMode);
+
+ // Note that this is a stable sort, therefore the provided order will be preserved for
+ // entries that are not on the priority list.
+ List<OffloadIntentStore.PassthroughIntent> orderedPtIntents = ptIntents
+ .stream()
+ .sorted(Comparator.comparingInt(pt -> pt.mPriority))
+ .toList();
+ Set<String> added = new HashSet<>();
+ for (OffloadIntentStore.PassthroughIntent ptIntent : orderedPtIntents) {
+ if (tryAddToPassthroughList(networkInterface, ptIntent)) {
+ added.add(ptIntent.mOriginalQName);
+ }
+ }
+ return added;
+ }
+
+ /**
+ * Delete a set of entries on the passthrough list.
+ *
+ * @return The set of entries that were deleted.
+ */
+ Collection<String> deletePassthroughData(String networkInterface, Collection<String> qnames) {
+ Set<String> deleted = new HashSet<>();
+ for (String qname : qnames) {
+ if (tryRemoveFromPassthroughList(networkInterface, qname)) {
+ deleted.add(qname);
+ }
+ }
+ return deleted;
+ }
+
+ @Nullable
+ private Integer tryAddProtocolResponses(
+ String networkInterface, OffloadIntentStore.OffloadIntent offloadIntent) {
+ int offloadKey;
+ try {
+ offloadKey = mVendorService.addProtocolResponses(
+ networkInterface, offloadIntent.mProtocolData);
+ } catch (RemoteException | ServiceSpecificException e) {
+ String msg = "Failed to offload mDNS protocol response for record key {" +
+ offloadIntent.mRecordKey + "} on iface {" + networkInterface + "}";
+ Log.e(TAG, msg, e);
+ return null;
+ }
+ if (offloadKey == INVALID_OFFLOAD_KEY) {
+ Log.e(TAG, "Failed to offload mDNS protocol data, vendor service returned error.");
+ return null;
+ }
+ return offloadKey;
+ }
+
+ private boolean tryRemoveProtocolResponses(Integer offloadKey) {
+ try {
+ mVendorService.removeProtocolResponses(offloadKey);
+ return true;
+ } catch (RemoteException | ServiceSpecificException e) {
+ String msg = "Failed to remove offloaded mDNS protocol response for offload key {"
+ + offloadKey + "}";
+ Log.e(TAG, msg, e);
+ }
+ return false;
+ }
+
+ private void trySetPassthroughBehavior(String networkInterface, byte passthroughMode) {
+ try {
+ mVendorService.setPassthroughBehavior(networkInterface, passthroughMode);
+ } catch (RemoteException | ServiceSpecificException e) {
+ String msg = "Failed to set passthrough mode {"
+ + passthroughBehaviorToString(passthroughMode) + "}"
+ + " on iface {" + networkInterface + "}";
+ Log.e(TAG, msg, e);
+ }
+ }
+
+ private boolean tryAddToPassthroughList(
+ String networkInterface,
+ OffloadIntentStore.PassthroughIntent ptIntent) {
+ String simpleQName = convertQNameForVendorService(ptIntent.mOriginalQName);
+ boolean addedEntry;
+ try {
+ addedEntry = mVendorService.addToPassthroughList(networkInterface, simpleQName);
+ } catch (RemoteException | ServiceSpecificException e) {
+ String msg = "Failed to add passthrough list entry for qname {"
+ + ptIntent.mOriginalQName + "} on iface {" + networkInterface + "}";
+ Log.e(TAG, msg, e);
+ return false;
+ }
+ if (!addedEntry) {
+ String msg = "Failed to add passthrough list entry for qname {"
+ + ptIntent.mOriginalQName + "} on iface {" + networkInterface + "}.";
+ Log.e(TAG, msg);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean tryRemoveFromPassthroughList(String networkInterface, String qname) {
+ String simpleQName = convertQNameForVendorService(qname);
+ try {
+ mVendorService.removeFromPassthroughList(networkInterface, simpleQName);
+ return true;
+ } catch (RemoteException | ServiceSpecificException e) {
+ String msg = "Failed to remove passthrough for qname {" + qname + "}.";
+ Log.e(TAG, msg, e);
+ }
+ return false;
+ }
+
+ void dump(PrintWriter writer) {
+ writer.println("OffloadWriter:");
+ writer.println("mOffloadState=%b".formatted(mOffloadState));
+ writer.println("isVendorServiceConnected=%b".formatted(isVendorServiceConnected()));
+ writer.println();
+ }
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/PriorityListManager.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/PriorityListManager.java
new file mode 100644
index 0000000..dfa733f
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/PriorityListManager.java
@@ -0,0 +1,55 @@
+package com.android.tv.mdnsoffloadmanager;
+
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import device.google.atv.mdns_offload.IMdnsOffload;
+
+public class PriorityListManager {
+
+ public static final int PRIORITIZED_QNAMES_ID = R.array.config_mdnsOffloadPriorityQnames;
+ private final Map<String, Integer> mPriorityMap;
+
+ PriorityListManager(@NonNull Resources resources) {
+ String[] priorityList = resources.getStringArray(PRIORITIZED_QNAMES_ID);
+ // We assign negative sorting keys to qNames on the priority list to ensure they are
+ // prioritized. Priorities for all other records are assigned on a first come first served
+ // order based on their record key.
+ int priorityListSize = priorityList.length;
+ mPriorityMap = IntStream.range(0, priorityListSize)
+ .boxed()
+ .collect(Collectors.toUnmodifiableMap(
+ index -> canonicalQName(priorityList[index]),
+ index -> -priorityListSize + index));
+ }
+
+ String canonicalQName(String qName) {
+ String upper = qName.toUpperCase(Locale.ROOT);
+ if (upper.endsWith(".")) {
+ return upper;
+ }
+ return upper + ".";
+ }
+
+ int getPriority(String qname, int defaultPriority) {
+ return mPriorityMap.getOrDefault(canonicalQName(qname), defaultPriority);
+ }
+
+ int getPriority(IMdnsOffload.MdnsProtocolData protocolData, int recordKey) {
+ return protocolData.matchCriteriaList.stream()
+ .mapToInt(mc -> {
+ String qname = MdnsPacketParser.extractFullName(
+ protocolData.rawOffloadPacket, mc.nameOffset);
+ return getPriority(qname, recordKey);
+ })
+ .min()
+ .orElse(recordKey);
+ }
+
+}
diff --git a/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/util/WakeLockWrapper.java b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/util/WakeLockWrapper.java
new file mode 100644
index 0000000..150d801
--- /dev/null
+++ b/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/util/WakeLockWrapper.java
@@ -0,0 +1,28 @@
+package com.android.tv.mdnsoffloadmanager.util;
+
+import android.os.PowerManager;
+
+/**
+ * Wrapper around {@link android.os.PowerManager.WakeLock} for testing purposes.
+ */
+public class WakeLockWrapper {
+ private final PowerManager.WakeLock mLock;
+
+ public WakeLockWrapper(PowerManager.WakeLock lock) {
+ this.mLock = lock;
+ }
+
+ /**
+ * @see PowerManager.WakeLock#acquire()
+ */
+ public void acquire(long timeout) {
+ mLock.acquire(timeout);
+ }
+
+ /**
+ * @see PowerManager.WakeLock#release()
+ */
+ public void release() {
+ mLock.release();
+ }
+}