summaryrefslogtreecommitdiff
path: root/MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java
diff options
context:
space:
mode:
Diffstat (limited to 'MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java')
-rw-r--r--MdnsOffloadManagerService/src/com/android/tv/mdnsoffloadmanager/MdnsOffloadManagerService.java482
1 files changed, 482 insertions, 0 deletions
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();
+ });
+ }
+ }
+}