diff options
Diffstat (limited to 'MdnsOffloadManagerService/src/com/android')
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(); + } +} |