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.
*
* TODO(b/270115511) do something with these metrics.
*/
void retrieveAndClearMetrics(Collection 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 writeOffloadData(
String networkInterface, Collection offloadIntents) {
List orderedOffloadIntents = offloadIntents
.stream()
.sorted(Comparator.comparingInt(offloadIntent -> offloadIntent.mPriority))
.toList();
Set 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 deleteOffloadData(Set offloadKeys) {
Set 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 writePassthroughData(
String networkInterface,
List 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 orderedPtIntents = ptIntents
.stream()
.sorted(Comparator.comparingInt(pt -> pt.mPriority))
.toList();
Set 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 deletePassthroughData(String networkInterface, Collection qnames) {
Set 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();
}
}