diff options
author | Soonil Nagarkar <sooniln@google.com> | 2021-03-01 16:26:21 -0800 |
---|---|---|
committer | Soonil Nagarkar <sooniln@google.com> | 2021-03-22 20:27:31 -0700 |
commit | c257baefb5758f895c52b520d9d772225c067e14 (patch) | |
tree | 530938cb055065355888f6c3a6e44ce4b129cc6b | |
parent | 74882cf536a560d23a1cf422faee8c15e1cda763 (diff) |
Refactor ServiceWatcher
Split it up into more generic and useable components that are not as
location specific.
Bug: 182491144
Bug: 181665909
Test: presubmits
Change-Id: I2d6d5e70328657a7430e170531402cb3acbed028
13 files changed, 899 insertions, 597 deletions
diff --git a/location/java/android/location/provider/ILocationProviderManager.aidl b/location/java/android/location/provider/ILocationProviderManager.aidl index 50ed046f09cd..092ec67ff146 100644 --- a/location/java/android/location/provider/ILocationProviderManager.aidl +++ b/location/java/android/location/provider/ILocationProviderManager.aidl @@ -24,7 +24,7 @@ import android.location.provider.ProviderProperties; * @hide */ interface ILocationProviderManager { - void onInitialize(boolean allowed, in ProviderProperties properties, @nullable String packageName, @nullable String attributionTag); + void onInitialize(boolean allowed, in ProviderProperties properties, @nullable String attributionTag); void onSetAllowed(boolean allowed); void onSetProperties(in ProviderProperties properties); diff --git a/location/java/android/location/provider/LocationProviderBase.java b/location/java/android/location/provider/LocationProviderBase.java index ae6395d5d12d..eada22cd94dc 100644 --- a/location/java/android/location/provider/LocationProviderBase.java +++ b/location/java/android/location/provider/LocationProviderBase.java @@ -96,7 +96,6 @@ public abstract class LocationProviderBase { "com.android.location.service.FusedLocationProvider"; private final String mTag; - private final @Nullable String mPackageName; private final @Nullable String mAttributionTag; private final IBinder mBinder; @@ -108,7 +107,6 @@ public abstract class LocationProviderBase { public LocationProviderBase(@NonNull Context context, @NonNull String tag, @NonNull ProviderProperties properties) { mTag = tag; - mPackageName = context.getPackageName(); mAttributionTag = context.getAttributionTag(); mBinder = new Service(); @@ -305,7 +303,7 @@ public abstract class LocationProviderBase { public void setLocationProviderManager(ILocationProviderManager manager) { synchronized (mBinder) { try { - manager.onInitialize(mAllowed, mProperties, mPackageName, mAttributionTag); + manager.onInitialize(mAllowed, mProperties, mAttributionTag); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (RuntimeException e) { diff --git a/location/lib/java/com/android/location/provider/LocationProviderBase.java b/location/lib/java/com/android/location/provider/LocationProviderBase.java index 7f1cf6dc3459..95f6c2f38ecb 100644 --- a/location/lib/java/com/android/location/provider/LocationProviderBase.java +++ b/location/lib/java/com/android/location/provider/LocationProviderBase.java @@ -96,7 +96,6 @@ public abstract class LocationProviderBase { public static final String FUSED_PROVIDER = LocationManager.FUSED_PROVIDER; final String mTag; - @Nullable final String mPackageName; @Nullable final String mAttributionTag; final IBinder mBinder; @@ -133,7 +132,6 @@ public abstract class LocationProviderBase { public LocationProviderBase(Context context, String tag, ProviderPropertiesUnbundled properties) { mTag = tag; - mPackageName = context != null ? context.getPackageName() : null; mAttributionTag = context != null ? context.getAttributionTag() : null; mBinder = new Service(); @@ -370,7 +368,7 @@ public abstract class LocationProviderBase { public void setLocationProviderManager(ILocationProviderManager manager) { synchronized (mBinder) { try { - manager.onInitialize(mAllowed, mProperties, mPackageName, mAttributionTag); + manager.onInitialize(mAllowed, mProperties, mAttributionTag); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (RuntimeException e) { diff --git a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java index 7cc599499ca1..c4c60ea5aaa8 100644 --- a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java +++ b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java @@ -151,20 +151,14 @@ public class FusedLocationServiceTest { } @Override - public void onInitialize(boolean allowed, ProviderProperties properties, String packageName, - String attributionTag) { - - } + public void onInitialize(boolean allowed, ProviderProperties properties, + String attributionTag) {} @Override - public void onSetAllowed(boolean allowed) { - - } + public void onSetAllowed(boolean allowed) {} @Override - public void onSetProperties(ProviderProperties properties) { - - } + public void onSetProperties(ProviderProperties properties) {} @Override public void onReportLocation(Location location) { @@ -177,9 +171,7 @@ public class FusedLocationServiceTest { } @Override - public void onFlushComplete() { - - } + public void onFlushComplete() {} public Location getNextLocation(long timeoutMs) throws InterruptedException { return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS); diff --git a/services/core/java/com/android/server/location/GeocoderProxy.java b/services/core/java/com/android/server/location/GeocoderProxy.java index c93c4b1f21b7..dc3596b6c2a7 100644 --- a/services/core/java/com/android/server/location/GeocoderProxy.java +++ b/services/core/java/com/android/server/location/GeocoderProxy.java @@ -24,6 +24,7 @@ import android.location.IGeocodeProvider; import android.os.IBinder; import android.os.RemoteException; +import com.android.server.servicewatcher.CurrentUserServiceSupplier; import com.android.server.servicewatcher.ServiceWatcher; import java.util.Collections; @@ -54,9 +55,11 @@ public class GeocoderProxy { private final ServiceWatcher mServiceWatcher; private GeocoderProxy(Context context) { - mServiceWatcher = new ServiceWatcher(context, SERVICE_ACTION, null, null, - com.android.internal.R.bool.config_enableGeocoderOverlay, - com.android.internal.R.string.config_geocoderProviderPackageName); + mServiceWatcher = ServiceWatcher.create(context, "GeocoderProxy", + new CurrentUserServiceSupplier(context, SERVICE_ACTION, + com.android.internal.R.bool.config_enableGeocoderOverlay, + com.android.internal.R.string.config_geocoderProviderPackageName), + null); } private boolean register() { @@ -72,7 +75,7 @@ public class GeocoderProxy { */ public void getFromLocation(double latitude, double longitude, int maxResults, GeocoderParams params, IGeocodeListener listener) { - mServiceWatcher.runOnBinder(new ServiceWatcher.BinderRunner() { + mServiceWatcher.runOnBinder(new ServiceWatcher.BinderOperation() { @Override public void run(IBinder binder) throws RemoteException { IGeocodeProvider provider = IGeocodeProvider.Stub.asInterface(binder); @@ -97,7 +100,7 @@ public class GeocoderProxy { double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, int maxResults, GeocoderParams params, IGeocodeListener listener) { - mServiceWatcher.runOnBinder(new ServiceWatcher.BinderRunner() { + mServiceWatcher.runOnBinder(new ServiceWatcher.BinderOperation() { @Override public void run(IBinder binder) throws RemoteException { IGeocodeProvider provider = IGeocodeProvider.Stub.asInterface(binder); diff --git a/services/core/java/com/android/server/location/HardwareActivityRecognitionProxy.java b/services/core/java/com/android/server/location/HardwareActivityRecognitionProxy.java index e1c87000ea89..6ac6e77342be 100644 --- a/services/core/java/com/android/server/location/HardwareActivityRecognitionProxy.java +++ b/services/core/java/com/android/server/location/HardwareActivityRecognitionProxy.java @@ -25,15 +25,15 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.CurrentUserServiceSupplier.BoundServiceInfo; import com.android.server.servicewatcher.ServiceWatcher; -import com.android.server.servicewatcher.ServiceWatcher.BoundService; +import com.android.server.servicewatcher.ServiceWatcher.ServiceListener; /** * Proxy class to bind GmsCore to the ActivityRecognitionHardware. - * - * @hide */ -public class HardwareActivityRecognitionProxy { +public class HardwareActivityRecognitionProxy implements ServiceListener<BoundServiceInfo> { private static final String TAG = "ARProxy"; private static final String SERVICE_ACTION = @@ -66,12 +66,16 @@ public class HardwareActivityRecognitionProxy { mInstance = null; } - mServiceWatcher = new ServiceWatcher(context, - SERVICE_ACTION, - this::onBind, - null, - com.android.internal.R.bool.config_enableActivityRecognitionHardwareOverlay, - com.android.internal.R.string.config_activityRecognitionHardwarePackageName); + int useOverlayResId = + com.android.internal.R.bool.config_enableActivityRecognitionHardwareOverlay; + int nonOverlayPackageResId = + com.android.internal.R.string.config_activityRecognitionHardwarePackageName; + + mServiceWatcher = ServiceWatcher.create(context, + "HardwareActivityRecognitionProxy", + new CurrentUserServiceSupplier(context, SERVICE_ACTION, useOverlayResId, + nonOverlayPackageResId), + this); } private boolean register() { @@ -82,7 +86,8 @@ public class HardwareActivityRecognitionProxy { return resolves; } - private void onBind(IBinder binder, BoundService service) throws RemoteException { + @Override + public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException { String descriptor = binder.getInterfaceDescriptor(); if (IActivityRecognitionHardwareWatcher.class.getCanonicalName().equals(descriptor)) { @@ -99,4 +104,7 @@ public class HardwareActivityRecognitionProxy { Log.e(TAG, "Unknown descriptor: " + descriptor); } } + + @Override + public void onUnbind() {} } diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 6d1606defa04..864aa33a58d0 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -378,6 +378,7 @@ public class LocationManagerService extends ILocationManager.Stub { // provider has unfortunate hard dependencies on the network provider ProxyLocationProvider networkProvider = ProxyLocationProvider.create( mContext, + NETWORK_PROVIDER, ACTION_NETWORK_PROVIDER, com.android.internal.R.bool.config_enableNetworkLocationOverlay, com.android.internal.R.string.config_networkLocationProviderPackageName); @@ -397,6 +398,7 @@ public class LocationManagerService extends ILocationManager.Stub { ProxyLocationProvider fusedProvider = ProxyLocationProvider.create( mContext, + FUSED_PROVIDER, ACTION_FUSED_PROVIDER, com.android.internal.R.bool.config_enableFusedLocationOverlay, com.android.internal.R.string.config_fusedLocationProviderPackageName); diff --git a/services/core/java/com/android/server/location/geofence/GeofenceProxy.java b/services/core/java/com/android/server/location/geofence/GeofenceProxy.java index c70714932792..90b446ecb15d 100644 --- a/services/core/java/com/android/server/location/geofence/GeofenceProxy.java +++ b/services/core/java/com/android/server/location/geofence/GeofenceProxy.java @@ -29,14 +29,17 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.CurrentUserServiceSupplier.BoundServiceInfo; import com.android.server.servicewatcher.ServiceWatcher; +import com.android.server.servicewatcher.ServiceWatcher.ServiceListener; import java.util.Objects; /** * @hide */ -public final class GeofenceProxy { +public final class GeofenceProxy implements ServiceListener<BoundServiceInfo> { private static final String TAG = "GeofenceProxy"; private static final String SERVICE_ACTION = "com.android.location.service.GeofenceProvider"; @@ -62,10 +65,12 @@ public final class GeofenceProxy { private GeofenceProxy(Context context, IGpsGeofenceHardware gpsGeofence) { mGpsGeofenceHardware = Objects.requireNonNull(gpsGeofence); - mServiceWatcher = new ServiceWatcher(context, SERVICE_ACTION, - (binder, service) -> updateGeofenceHardware(binder), null, - com.android.internal.R.bool.config_enableGeofenceOverlay, - com.android.internal.R.string.config_geofenceProviderPackageName); + mServiceWatcher = ServiceWatcher.create(context, + "GeofenceProxy", + new CurrentUserServiceSupplier(context, SERVICE_ACTION, + com.android.internal.R.bool.config_enableGeofenceOverlay, + com.android.internal.R.string.config_geofenceProviderPackageName), + this); mGeofenceHardware = null; } @@ -87,6 +92,14 @@ public final class GeofenceProxy { return resolves; } + @Override + public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException { + updateGeofenceHardware(binder); + } + + @Override + public void onUnbind() {} + private class GeofenceProxyServiceConnection implements ServiceConnection { GeofenceProxyServiceConnection() {} diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java index 317e61b44e46..5df78704d002 100644 --- a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java +++ b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java @@ -19,7 +19,6 @@ package com.android.server.location.provider.proxy; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; import android.annotation.Nullable; -import android.content.ComponentName; import android.content.Context; import android.location.Location; import android.location.LocationResult; @@ -28,33 +27,34 @@ import android.location.provider.ILocationProviderManager; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; import android.location.util.identity.CallerIdentity; -import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.text.TextUtils; import android.util.ArraySet; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.ArrayUtils; import com.android.server.FgThread; import com.android.server.location.provider.AbstractLocationProvider; +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.CurrentUserServiceSupplier.BoundServiceInfo; import com.android.server.servicewatcher.ServiceWatcher; -import com.android.server.servicewatcher.ServiceWatcher.BoundService; +import com.android.server.servicewatcher.ServiceWatcher.ServiceListener; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; /** * Proxy for ILocationProvider implementations. */ -public class ProxyLocationProvider extends AbstractLocationProvider { +public class ProxyLocationProvider extends AbstractLocationProvider implements + ServiceListener<BoundServiceInfo> { - private static final String KEY_EXTRA_ATTRIBUTION_TAGS = "android:location_allow_listed_tags"; - private static final String EXTRA_ATTRIBUTION_TAGS_SEPARATOR = ";"; + private static final String EXTRA_LOCATION_TAGS = "android:location_allow_listed_tags"; + private static final String LOCATION_TAGS_SEPARATOR = ";"; private static final long RESET_DELAY_MS = 1000; @@ -63,10 +63,10 @@ public class ProxyLocationProvider extends AbstractLocationProvider { * null. */ @Nullable - public static ProxyLocationProvider create(Context context, String action, + public static ProxyLocationProvider create(Context context, String provider, String action, int enableOverlayResId, int nonOverlayPackageResId) { - ProxyLocationProvider proxy = new ProxyLocationProvider(context, action, enableOverlayResId, - nonOverlayPackageResId); + ProxyLocationProvider proxy = new ProxyLocationProvider(context, provider, action, + enableOverlayResId, nonOverlayPackageResId); if (proxy.checkServiceResolves()) { return proxy; } else { @@ -84,23 +84,23 @@ public class ProxyLocationProvider extends AbstractLocationProvider { @GuardedBy("mLock") @Nullable Runnable mResetter; - @GuardedBy("mLock") - Proxy mProxy; + @Nullable Proxy mProxy; @GuardedBy("mLock") - @Nullable ComponentName mService; + @Nullable BoundServiceInfo mBoundServiceInfo; private volatile ProviderRequest mRequest; - private ProxyLocationProvider(Context context, String action, int enableOverlayResId, - int nonOverlayPackageResId) { + private ProxyLocationProvider(Context context, String provider, String action, + int enableOverlayResId, int nonOverlayPackageResId) { // safe to use direct executor since our locks are not acquired in a code path invoked by // our owning provider super(DIRECT_EXECUTOR, null, null, Collections.emptySet()); mContext = context; - mServiceWatcher = new ServiceWatcher(context, action, this::onBind, - this::onUnbind, enableOverlayResId, nonOverlayPackageResId); + mServiceWatcher = ServiceWatcher.create(context, provider, + new CurrentUserServiceSupplier(context, action, enableOverlayResId, + nonOverlayPackageResId), this); mProxy = null; mRequest = ProviderRequest.EMPTY_REQUEST; @@ -110,26 +110,13 @@ public class ProxyLocationProvider extends AbstractLocationProvider { return mServiceWatcher.checkServiceResolves(); } - private void onBind(IBinder binder, BoundService boundService) throws RemoteException { + @Override + public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException { ILocationProvider provider = ILocationProvider.Stub.asInterface(binder); synchronized (mLock) { mProxy = new Proxy(); - mService = boundService.component; - - if (mResetter != null) { - FgThread.getHandler().removeCallbacks(mResetter); - mResetter = null; - } - - // update extra attribution tag info from manifest - if (boundService.metadata != null) { - String tagsList = boundService.metadata.getString(KEY_EXTRA_ATTRIBUTION_TAGS); - if (tagsList != null) { - setExtraAttributionTags( - new ArraySet<>(tagsList.split(EXTRA_ATTRIBUTION_TAGS_SEPARATOR))); - } - } + mBoundServiceInfo = boundServiceInfo; provider.setLocationProviderManager(mProxy); @@ -140,26 +127,29 @@ public class ProxyLocationProvider extends AbstractLocationProvider { } } - private void onUnbind() { + @Override + public void onUnbind() { Runnable[] flushListeners; synchronized (mLock) { mProxy = null; - mService = null; + mBoundServiceInfo = null; // we need to clear the state - but most disconnections are very temporary. we give a // grace period where we don't clear the state immediately so that transient - // interruptions are not visible to clients - mResetter = new Runnable() { - @Override - public void run() { - synchronized (mLock) { - if (mResetter == this) { - setState(prevState -> State.EMPTY_STATE); + // interruptions are not necessarily visible to downstream clients + if (mResetter == null) { + mResetter = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mResetter == this) { + setState(prevState -> State.EMPTY_STATE); + } } } - } - }; - FgThread.getHandler().postDelayed(mResetter, RESET_DELAY_MS); + }; + FgThread.getHandler().postDelayed(mResetter, RESET_DELAY_MS); + } flushListeners = mFlushListeners.toArray(new Runnable[0]); mFlushListeners.clear(); @@ -192,7 +182,7 @@ public class ProxyLocationProvider extends AbstractLocationProvider { @Override protected void onFlush(Runnable callback) { - mServiceWatcher.runOnBinder(new ServiceWatcher.BinderRunner() { + mServiceWatcher.runOnBinder(new ServiceWatcher.BinderOperation() { @Override public void run(IBinder binder) throws RemoteException { ILocationProvider provider = ILocationProvider.Stub.asInterface(binder); @@ -232,20 +222,7 @@ public class ProxyLocationProvider extends AbstractLocationProvider { @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mServiceWatcher.dump(fd, pw, args); - } - - private static String guessPackageName(Context context, int uid, String packageName) { - String[] packageNames = context.getPackageManager().getPackagesForUid(uid); - if (packageNames == null || packageNames.length == 0) { - // illegal state exception will propagate back through binders - throw new IllegalStateException( - "location provider from uid " + uid + " has no package information"); - } else if (ArrayUtils.contains(packageNames, packageName)) { - return packageName; - } else { - return packageNames[0]; - } + mServiceWatcher.dump(pw); } private class Proxy extends ILocationProviderManager.Stub { @@ -255,27 +232,37 @@ public class ProxyLocationProvider extends AbstractLocationProvider { // executed on binder thread @Override public void onInitialize(boolean allowed, ProviderProperties properties, - @Nullable String packageName, @Nullable String attributionTag) { + @Nullable String attributionTag) { synchronized (mLock) { if (mProxy != this) { return; } - CallerIdentity identity; - if (packageName == null) { - packageName = guessPackageName(mContext, Binder.getCallingUid(), - Objects.requireNonNull(mService).getPackageName()); - // unsafe is ok since the package is coming direct from the package manager here - identity = CallerIdentity.fromBinderUnsafe(packageName, attributionTag); - } else { - identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag); + if (mResetter != null) { + FgThread.getHandler().removeCallbacks(mResetter); + mResetter = null; } + // set extra attribution tags from manifest if necessary + String[] attributionTags = new String[0]; + if (mBoundServiceInfo.getMetadata() != null) { + String tagsStr = mBoundServiceInfo.getMetadata().getString(EXTRA_LOCATION_TAGS); + if (!TextUtils.isEmpty(tagsStr)) { + attributionTags = tagsStr.split(LOCATION_TAGS_SEPARATOR); + } + } + ArraySet<String> extraAttributionTags = new ArraySet<>(attributionTags); + + // unsafe is ok since we trust the package name already + CallerIdentity identity = CallerIdentity.fromBinderUnsafe( + mBoundServiceInfo.getComponentName().getPackageName(), + attributionTag); + setState(prevState -> State.EMPTY_STATE - .withExtraAttributionTags(prevState.extraAttributionTags) .withAllowed(allowed) .withProperties(properties) - .withIdentity(identity)); + .withIdentity(identity) + .withExtraAttributionTags(extraAttributionTags)); } } diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java new file mode 100644 index 000000000000..3ca8a5a1f554 --- /dev/null +++ b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2021 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.server.servicewatcher; + +import static android.content.pm.PackageManager.GET_META_DATA; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; +import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.UserHandle.USER_SYSTEM; + +import android.annotation.BoolRes; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.app.ActivityManagerInternal; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.UserHandle; +import android.permission.PermissionManager; +import android.util.Log; + +import com.android.internal.util.Preconditions; +import com.android.server.FgThread; +import com.android.server.LocalServices; +import com.android.server.servicewatcher.ServiceWatcher.ServiceChangedListener; +import com.android.server.servicewatcher.ServiceWatcher.ServiceSupplier; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * Supplies services based on the current active user and version as defined in the service + * manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to + * ensure only system (ie, privileged) services are matched. It also handles services that are not + * direct boot aware, and will automatically pick the best service as the user's direct boot state + * changes. + * + * <p>Optionally, two permissions may be specified: (1) a caller permission - any service that does + * not require callers to hold this permission is rejected (2) a service permission - any service + * whose package does not hold this permission is rejected. + */ +public class CurrentUserServiceSupplier extends BroadcastReceiver implements + ServiceSupplier<CurrentUserServiceSupplier.BoundServiceInfo> { + + private static final String TAG = "CurrentUserServiceSupplier"; + + private static final String EXTRA_SERVICE_VERSION = "serviceVersion"; + private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser"; + + private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return -1; + } else if (o2 == null) { + return 1; + } + + // ServiceInfos with higher version numbers always win. if version numbers are equal + // then we prefer components that work for all users vs components that only work for a + // single user at a time. otherwise everything's equal. + int ret = Integer.compare(o1.getVersion(), o2.getVersion()); + if (ret == 0) { + if (o1.getUserId() != USER_SYSTEM && o2.getUserId() == USER_SYSTEM) { + ret = -1; + } else if (o1.getUserId() == USER_SYSTEM && o2.getUserId() != USER_SYSTEM) { + ret = 1; + } + } + return ret; + }; + + /** Bound service information with version information. */ + public static class BoundServiceInfo extends ServiceWatcher.BoundServiceInfo { + + private static int parseUid(ResolveInfo resolveInfo) { + int uid = resolveInfo.serviceInfo.applicationInfo.uid; + Bundle metadata = resolveInfo.serviceInfo.metaData; + if (metadata != null && metadata.getBoolean(EXTRA_SERVICE_IS_MULTIUSER, false)) { + // reconstruct a uid for the same app but with the system user - hope this exists + uid = UserHandle.getUid(USER_SYSTEM, UserHandle.getAppId(uid)); + } + return uid; + } + + private static int parseVersion(ResolveInfo resolveInfo) { + int version = Integer.MIN_VALUE; + if (resolveInfo.serviceInfo.metaData != null) { + version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version); + } + return version; + } + + private final int mVersion; + private final @Nullable Bundle mMetadata; + + protected BoundServiceInfo(String action, ResolveInfo resolveInfo) { + this(action, parseUid(resolveInfo), resolveInfo.serviceInfo.getComponentName(), + parseVersion(resolveInfo), resolveInfo.serviceInfo.metaData); + } + + protected BoundServiceInfo(String action, int uid, ComponentName componentName, int version, + @Nullable Bundle metadata) { + super(action, uid, componentName); + + mVersion = version; + mMetadata = metadata; + } + + public int getVersion() { + return mVersion; + } + + public @Nullable Bundle getMetadata() { + return mMetadata; + } + + @Override + public String toString() { + return super.toString() + "@" + mVersion; + } + } + + private static @Nullable String retrieveExplicitPackage(Context context, + @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) { + Resources resources = context.getResources(); + boolean enableOverlay = resources.getBoolean(enableOverlayResId); + if (!enableOverlay) { + return resources.getString(nonOverlayPackageResId); + } else { + return null; + } + } + + private final Context mContext; + private final ActivityManagerInternal mActivityManager; + private final Intent mIntent; + // a permission that the service forces callers (ie ServiceWatcher/system server) to hold + private final @Nullable String mCallerPermission; + // a permission that the service package should hold + private final @Nullable String mServicePermission; + + private volatile ServiceChangedListener mListener; + + public CurrentUserServiceSupplier(Context context, String action) { + this(context, action, null, null, null); + } + + public CurrentUserServiceSupplier(Context context, String action, + @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) { + this(context, action, + retrieveExplicitPackage(context, enableOverlayResId, nonOverlayPackageResId), null, + null); + } + + public CurrentUserServiceSupplier(Context context, String action, + @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId, + @Nullable String callerPermission, @Nullable String servicePermission) { + this(context, action, + retrieveExplicitPackage(context, enableOverlayResId, nonOverlayPackageResId), + callerPermission, servicePermission); + } + + public CurrentUserServiceSupplier(Context context, String action, + @Nullable String explicitPackage, @Nullable String callerPermission, + @Nullable String servicePermission) { + mContext = context; + mActivityManager = Objects.requireNonNull( + LocalServices.getService(ActivityManagerInternal.class)); + mIntent = new Intent(action); + + if (explicitPackage != null) { + mIntent.setPackage(explicitPackage); + } + + mCallerPermission = callerPermission; + mServicePermission = servicePermission; + } + + @Override + public boolean hasMatchingService() { + List<ResolveInfo> resolveInfos = mContext.getPackageManager() + .queryIntentServicesAsUser(mIntent, + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY, + UserHandle.USER_SYSTEM); + return !resolveInfos.isEmpty(); + } + + @Override + public void register(ServiceChangedListener listener) { + Preconditions.checkState(mListener == null); + + mListener = listener; + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_UNLOCKED); + mContext.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, + FgThread.getHandler()); + } + + @Override + public void unregister() { + Preconditions.checkArgument(mListener != null); + + mListener = null; + mContext.unregisterReceiver(this); + } + + @Override + public BoundServiceInfo getServiceInfo() { + BoundServiceInfo bestServiceInfo = null; + + // only allow privileged services in the correct direct boot state to match + int currentUserId = mActivityManager.getCurrentUserId(); + List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser( + mIntent, + GET_META_DATA | MATCH_DIRECT_BOOT_AUTO | MATCH_SYSTEM_ONLY, + currentUserId); + for (ResolveInfo resolveInfo : resolveInfos) { + ServiceInfo service = Objects.requireNonNull(resolveInfo.serviceInfo); + + if (mCallerPermission != null) { + if (!mCallerPermission.equals(service.permission)) { + Log.d(TAG, service.getComponentName().flattenToShortString() + + " disqualified due to not requiring " + mCallerPermission); + continue; + } + } + + BoundServiceInfo serviceInfo = new BoundServiceInfo(mIntent.getAction(), resolveInfo); + + if (mServicePermission != null) { + if (PermissionManager.checkPackageNamePermission(mServicePermission, + service.packageName, serviceInfo.getUserId()) != PERMISSION_GRANTED) { + Log.d(TAG, serviceInfo.getComponentName().flattenToShortString() + + " disqualified due to not holding " + mCallerPermission); + continue; + } + } + + if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) { + bestServiceInfo = serviceInfo; + } + } + + return bestServiceInfo; + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId == UserHandle.USER_NULL) { + return; + } + ServiceChangedListener listener = mListener; + if (listener == null) { + return; + } + + switch (action) { + case Intent.ACTION_USER_SWITCHED: + listener.onServiceChanged(); + break; + case Intent.ACTION_USER_UNLOCKED: + // user unlocked implies direct boot mode may have changed + if (userId == mActivityManager.getCurrentUserId()) { + listener.onServiceChanged(); + } + break; + default: + break; + } + } +} diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java b/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java index 5d49663209b7..23b5d98de59f 100644 --- a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java +++ b/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2021 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. @@ -16,516 +16,244 @@ package com.android.server.servicewatcher; -import static android.content.Context.BIND_AUTO_CREATE; -import static android.content.Context.BIND_NOT_FOREGROUND; -import static android.content.Context.BIND_NOT_VISIBLE; -import static android.content.pm.PackageManager.GET_META_DATA; -import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; -import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; -import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; -import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; - -import android.annotation.BoolRes; -import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.StringRes; import android.annotation.UserIdInt; -import android.app.ActivityManager; -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.ResolveInfo; -import android.content.res.Resources; -import android.os.Bundle; import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; -import android.util.ArrayMap; -import android.util.Log; -import com.android.internal.annotations.Immutable; -import com.android.internal.content.PackageMonitor; -import com.android.internal.util.Preconditions; -import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.FgThread; -import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.function.Predicate; /** - * Maintains a binding to the best service that matches the given intent information. Bind and - * unbind callbacks, as well as all binder operations, will all be run on a single thread. + * A ServiceWatcher is responsible for continuously maintaining an active binding to a service + * selected by it's {@link ServiceSupplier}. The {@link ServiceSupplier} may change the service it + * selects over time, and the currently bound service may crash, restart, have a user change, have + * changes made to its package, and so on and so forth. The ServiceWatcher is responsible for + * maintaining the binding across all these changes. + * + * <p>Clients may invoke {@link BinderOperation}s on the ServiceWatcher, and it will make a best + * effort to run these on the currently bound service, but individual operations may fail (if there + * is no service currently bound for instance). In order to help clients maintain the correct state, + * clients may supply a {@link ServiceListener}, which is informed when the ServiceWatcher connects + * and disconnects from a service. This allows clients to bring a bound service back into a known + * state on connection, and then run binder operations from there. In order to help clients + * accomplish this, ServiceWatcher guarantees that {@link BinderOperation}s and the + * {@link ServiceListener} will always be run on the same thread, so that strong ordering guarantees + * can be established between them. + * + * There is never any guarantee of whether a ServiceWatcher is currently connected to a service, and + * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely + * on this, and instead use {@link ServiceListener} notifications as necessary to recover from + * failures. */ -public class ServiceWatcher implements ServiceConnection { - - private static final String TAG = "ServiceWatcher"; - private static final boolean D = Log.isLoggable(TAG, Log.DEBUG); - - private static final String EXTRA_SERVICE_VERSION = "serviceVersion"; - private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser"; - - private static final long RETRY_DELAY_MS = 15 * 1000; +public interface ServiceWatcher { - private static final Predicate<ResolveInfo> DEFAULT_SERVICE_CHECK_PREDICATE = x -> true; - - /** Function to run on binder interface. */ - public interface BinderRunner { - /** Called to run client code with the binder. */ + /** + * Operation to run on a binder interface. All operations will be run on the thread used by the + * ServiceWatcher this is run with. + */ + interface BinderOperation { + /** Invoked to run the operation. Run on the ServiceWatcher thread. */ void run(IBinder binder) throws RemoteException; + /** - * Called if an error occurred and the function could not be run. This callback is only - * intended for resource deallocation and cleanup in response to a single binder operation, - * it should not be used to propagate errors further. + * Invoked if {@link #run(IBinder)} could not be invoked because there was no current + * binding, or if {@link #run(IBinder)} threw an exception ({@link RemoteException} or + * {@link RuntimeException}). This callback is only intended for resource deallocation and + * cleanup in response to a single binder operation, it should not be used to propagate + * errors further. Run on the ServiceWatcher thread. */ default void onError() {} } - /** Function to run on binder interface when first bound. */ - public interface OnBindRunner { - /** Called to run client code with the binder. */ - void run(IBinder binder, BoundService service) throws RemoteException; - } - /** - * Information on the service ServiceWatcher has selected as the best option for binding. + * Listener for bind and unbind events. All operations will be run on the thread used by the + * ServiceWatcher this is run with. + * + * @param <TBoundServiceInfo> type of bound service */ - @Immutable - public static final class BoundService implements Comparable<BoundService> { - - public static final BoundService NONE = new BoundService(Integer.MIN_VALUE, null, - false, null, -1); - - public final int version; - @Nullable - public final ComponentName component; - public final boolean serviceIsMultiuser; - public final int uid; - @Nullable - public final Bundle metadata; - - BoundService(ResolveInfo resolveInfo) { - Preconditions.checkArgument(resolveInfo.serviceInfo.getComponentName() != null); - - metadata = resolveInfo.serviceInfo.metaData; - if (metadata != null) { - version = metadata.getInt(EXTRA_SERVICE_VERSION, Integer.MIN_VALUE); - serviceIsMultiuser = metadata.getBoolean(EXTRA_SERVICE_IS_MULTIUSER, false); - } else { - version = Integer.MIN_VALUE; - serviceIsMultiuser = false; - } - - component = resolveInfo.serviceInfo.getComponentName(); - uid = resolveInfo.serviceInfo.applicationInfo.uid; - } - - private BoundService(int version, @Nullable ComponentName component, - boolean serviceIsMultiuser, @Nullable Bundle metadata, int uid) { - Preconditions.checkArgument(component != null || version == Integer.MIN_VALUE); - this.version = version; - this.component = component; - this.serviceIsMultiuser = serviceIsMultiuser; - this.metadata = metadata; - this.uid = uid; - } - - public @Nullable String getPackageName() { - return component != null ? component.getPackageName() : null; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BoundService)) { - return false; - } - BoundService that = (BoundService) o; - return version == that.version && uid == that.uid - && Objects.equals(component, that.component); - } + interface ServiceListener<TBoundServiceInfo extends BoundServiceInfo> { + /** Invoked when a service is bound. Run on the ServiceWatcher thread. */ + void onBind(IBinder binder, TBoundServiceInfo service) throws RemoteException; - @Override - public int hashCode() { - return Objects.hash(version, component, uid); - } - - @Override - public int compareTo(BoundService that) { - // ServiceInfos with higher version numbers always win (having a version number > - // MIN_VALUE implies having a non-null component). if version numbers are equal, a - // non-null component wins over a null component. if the version numbers are equal and - // both components exist then we prefer components that work for all users vs components - // that only work for a single user at a time. otherwise everything's equal. - int ret = Integer.compare(version, that.version); - if (ret == 0) { - if (component == null && that.component != null) { - ret = -1; - } else if (component != null && that.component == null) { - ret = 1; - } else { - if (UserHandle.getUserId(uid) != UserHandle.USER_SYSTEM - && UserHandle.getUserId(that.uid) == UserHandle.USER_SYSTEM) { - ret = -1; - } else if (UserHandle.getUserId(uid) == UserHandle.USER_SYSTEM - && UserHandle.getUserId(that.uid) != UserHandle.USER_SYSTEM) { - ret = 1; - } - } - } - return ret; - } - - @Override - public String toString() { - if (component == null) { - return "none"; - } else { - return component.toShortString() + "@" + version + "[u" - + UserHandle.getUserId(uid) + "]"; - } - } - } - - private final Context mContext; - private final Handler mHandler; - private final Intent mIntent; - private final Predicate<ResolveInfo> mServiceCheckPredicate; - - private final PackageMonitor mPackageMonitor = new PackageMonitor() { - @Override - public boolean onPackageChanged(String packageName, int uid, String[] components) { - return true; - } - - @Override - public void onSomePackagesChanged() { - onBestServiceChanged(false); - } - }; - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action == null) { - return; - } - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId == UserHandle.USER_NULL) { - return; - } - - switch (action) { - case Intent.ACTION_USER_SWITCHED: - onUserSwitched(userId); - break; - case Intent.ACTION_USER_UNLOCKED: - onUserUnlocked(userId); - break; - default: - break; - } - - } - }; - - // read/write from handler thread only - private final Map<ComponentName, BoundService> mPendingBinds = new ArrayMap<>(); - - @Nullable - private final OnBindRunner mOnBind; - - @Nullable - private final Runnable mOnUnbind; - - // read/write from handler thread only - private boolean mRegistered; - - // read/write from handler thread only - private int mCurrentUserId; - - // write from handler thread only, read anywhere - private volatile BoundService mTargetService; - private volatile IBinder mBinder; - - public ServiceWatcher(Context context, String action, - @Nullable OnBindRunner onBind, @Nullable Runnable onUnbind, - @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) { - this(context, FgThread.getHandler(), action, onBind, onUnbind, enableOverlayResId, - nonOverlayPackageResId, DEFAULT_SERVICE_CHECK_PREDICATE); - } - - public ServiceWatcher(Context context, Handler handler, String action, - @Nullable OnBindRunner onBind, @Nullable Runnable onUnbind, - @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) { - this(context, handler, action, onBind, onUnbind, enableOverlayResId, nonOverlayPackageResId, - DEFAULT_SERVICE_CHECK_PREDICATE); - } - - public ServiceWatcher(Context context, Handler handler, String action, - @Nullable OnBindRunner onBind, @Nullable Runnable onUnbind, - @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId, - @NonNull Predicate<ResolveInfo> serviceCheckPredicate) { - mContext = context; - mHandler = handler; - mIntent = new Intent(Objects.requireNonNull(action)); - mServiceCheckPredicate = Objects.requireNonNull(serviceCheckPredicate); - - Resources resources = context.getResources(); - boolean enableOverlay = resources.getBoolean(enableOverlayResId); - if (!enableOverlay) { - mIntent.setPackage(resources.getString(nonOverlayPackageResId)); - } - - mOnBind = onBind; - mOnUnbind = onUnbind; - - mCurrentUserId = UserHandle.USER_NULL; - - mTargetService = BoundService.NONE; - mBinder = null; + /** Invoked when a service is unbound. Run on the ServiceWatcher thread. */ + void onUnbind(); } /** - * Returns true if there is at least one component that could satisfy the ServiceWatcher's - * constraints. + * A listener for when a {@link ServiceSupplier} decides that the current service has changed. */ - public boolean checkServiceResolves() { - List<ResolveInfo> resolveInfos = mContext.getPackageManager() - .queryIntentServicesAsUser(mIntent, - MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY, - UserHandle.USER_SYSTEM); - for (ResolveInfo resolveInfo : resolveInfos) { - if (mServiceCheckPredicate.test(resolveInfo)) { - return true; - } - } - return false; + interface ServiceChangedListener { + /** + * Should be invoked when the current service may have changed. + */ + void onServiceChanged(); } /** - * Starts the process of determining the best matching service and maintaining a binding to it. + * This supplier encapsulates the logic of deciding what service a {@link ServiceWatcher} should + * be bound to at any given moment. + * + * @param <TBoundServiceInfo> type of bound service */ - public void register() { - mHandler.sendMessage(PooledLambda.obtainMessage(ServiceWatcher::registerInternal, - ServiceWatcher.this)); - } - - private void registerInternal() { - Preconditions.checkState(!mRegistered); - - mPackageMonitor.register(mContext, UserHandle.ALL, true, mHandler); - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_USER_SWITCHED); - intentFilter.addAction(Intent.ACTION_USER_UNLOCKED); - mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, intentFilter, null, - mHandler); + interface ServiceSupplier<TBoundServiceInfo extends BoundServiceInfo> { + /** + * Should return true if there exists at least one service capable of meeting the criteria + * of this supplier. This does not imply that {@link #getServiceInfo()} will always return a + * non-null result, as any service may be disqualified for various reasons at any point in + * time. May be invoked at any time from any thread and thus should generally not have any + * dependency on the other methods in this interface. + */ + boolean hasMatchingService(); - // TODO: This makes the behavior of the class unpredictable as the caller needs - // to know the internal impl detail that calling register would pick the current user. - mCurrentUserId = ActivityManager.getCurrentUser(); + /** + * Invoked when the supplier should start monitoring for any changes that could result in a + * different service selection, and should invoke + * {@link ServiceChangedListener#onServiceChanged()} in that case. {@link #getServiceInfo()} + * may be invoked after this method is called. + */ + void register(ServiceChangedListener listener); - mRegistered = true; + /** + * Invoked when the supplier should stop monitoring for any changes that could result in a + * different service selection, should no longer invoke + * {@link ServiceChangedListener#onServiceChanged()}. {@link #getServiceInfo()} will not be + * invoked after this method is called. + */ + void unregister(); - mHandler.post(() -> onBestServiceChanged(false)); + /** + * Must be implemented to return the current service selected by this supplier. May return + * null if no service currently meets the criteria. Only invoked while registered. + */ + @Nullable TBoundServiceInfo getServiceInfo(); } /** - * Stops the process of determining the best matching service and releases any binding. + * Information on the service selected as the best option for binding. */ - public void unregister() { - mHandler.sendMessage(PooledLambda.obtainMessage(ServiceWatcher::unregisterInternal, - ServiceWatcher.this)); - } - - private void unregisterInternal() { - Preconditions.checkState(mRegistered); + class BoundServiceInfo { - mRegistered = false; + protected final @Nullable String mAction; + protected final int mUid; + protected final ComponentName mComponentName; - mPackageMonitor.unregister(); - mContext.unregisterReceiver(mBroadcastReceiver); - - mHandler.post(() -> onBestServiceChanged(false)); - } - - private void onBestServiceChanged(boolean forceRebind) { - Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); - - BoundService bestServiceInfo = BoundService.NONE; - - if (mRegistered) { - List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser( - mIntent, - GET_META_DATA | MATCH_DIRECT_BOOT_AUTO | MATCH_SYSTEM_ONLY, - mCurrentUserId); - for (ResolveInfo resolveInfo : resolveInfos) { - if (!mServiceCheckPredicate.test(resolveInfo)) { - continue; - } - BoundService serviceInfo = new BoundService(resolveInfo); - if (serviceInfo.compareTo(bestServiceInfo) > 0) { - bestServiceInfo = serviceInfo; - } - } - } - - if (forceRebind || !bestServiceInfo.equals(mTargetService)) { - rebind(bestServiceInfo); - } - } - - private void rebind(BoundService newServiceInfo) { - Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); - - if (!mTargetService.equals(BoundService.NONE)) { - if (D) { - Log.d(TAG, "[" + mIntent.getAction() + "] unbinding from " + mTargetService); - } - - mContext.unbindService(this); - onServiceDisconnected(mTargetService.component); - mPendingBinds.remove(mTargetService.component); - mTargetService = BoundService.NONE; + protected BoundServiceInfo(String action, ResolveInfo resolveInfo) { + this(action, resolveInfo.serviceInfo.applicationInfo.uid, + resolveInfo.serviceInfo.getComponentName()); } - mTargetService = newServiceInfo; - if (mTargetService.equals(BoundService.NONE)) { - return; + protected BoundServiceInfo(String action, int uid, ComponentName componentName) { + mAction = action; + mUid = uid; + mComponentName = Objects.requireNonNull(componentName); } - Preconditions.checkState(mTargetService.component != null); - - Log.i(TAG, getLogPrefix() + " binding to " + mTargetService); - - Intent bindIntent = new Intent(mIntent).setComponent(mTargetService.component); - if (!mContext.bindServiceAsUser(bindIntent, this, - BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE, - mHandler, UserHandle.of(UserHandle.getUserId(mTargetService.uid)))) { - mTargetService = BoundService.NONE; - Log.e(TAG, getLogPrefix() + " unexpected bind failure - retrying later"); - mHandler.postDelayed(() -> onBestServiceChanged(false), RETRY_DELAY_MS); - } else { - mPendingBinds.put(mTargetService.component, mTargetService); + /** Returns the action associated with this bound service. */ + public @Nullable String getAction() { + return mAction; } - } - - @Override - public final void onServiceConnected(ComponentName component, IBinder binder) { - Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); - Preconditions.checkState(mBinder == null); - if (D) { - Log.d(TAG, getLogPrefix() + " connected to " + component.toShortString()); + /** Returns the component of this bound service. */ + public ComponentName getComponentName() { + return mComponentName; } - final BoundService boundService = mPendingBinds.remove(component); - if (boundService == null) { - return; + /** Returns the user id for this bound service. */ + public @UserIdInt int getUserId() { + return UserHandle.getUserId(mUid); } - mBinder = binder; - if (mOnBind != null) { - try { - mOnBind.run(binder, boundService); - } catch (RuntimeException | RemoteException e) { - // binders may propagate some specific non-RemoteExceptions from the other side - // through the binder as well - we cannot allow those to crash the system server - Log.e(TAG, getLogPrefix() + " exception running on " + component, e); + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; } - } - } - - @Override - public final void onServiceDisconnected(ComponentName component) { - Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); - if (mBinder == null) { - return; + BoundServiceInfo that = (BoundServiceInfo) o; + return mUid == that.mUid + && Objects.equals(mAction, that.mAction) + && mComponentName.equals(that.mComponentName); } - if (D) { - Log.d(TAG, getLogPrefix() + " disconnected from " + component.toShortString()); + @Override + public final int hashCode() { + return Objects.hash(mAction, mUid, mComponentName); } - mBinder = null; - if (mOnUnbind != null) { - mOnUnbind.run(); + @Override + public String toString() { + if (mComponentName == null) { + return "none"; + } else { + return mUid + "/" + mComponentName.flattenToShortString(); + } } } - @Override - public final void onBindingDied(ComponentName component) { - Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); - - Log.i(TAG, getLogPrefix() + " " + component.toShortString() + " died"); - - onBestServiceChanged(true); - } - - @Override - public final void onNullBinding(ComponentName component) { - Log.e(TAG, getLogPrefix() + " " + component.toShortString() + " has null binding"); - } - - void onUserSwitched(@UserIdInt int userId) { - mCurrentUserId = userId; - onBestServiceChanged(false); + /** + * Creates a new ServiceWatcher instance. + */ + static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create( + Context context, + String tag, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) { + return create(context, FgThread.getHandler(), tag, serviceSupplier, serviceListener); } - void onUserUnlocked(@UserIdInt int userId) { - if (userId == mCurrentUserId) { - onBestServiceChanged(false); - } + /** + * Creates a new ServiceWatcher instance that runs on the given handler. + */ + static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create( + Context context, + Handler handler, + String tag, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) { + return new ServiceWatcherImpl<>(context, handler, tag, serviceSupplier, serviceListener); } /** - * Runs the given function asynchronously if and only if currently connected. Suppresses any - * RemoteException thrown during execution. + * Returns true if there is at least one service that the ServiceWatcher could hypothetically + * bind to, as selected by the {@link ServiceSupplier}. */ - public final void runOnBinder(BinderRunner runner) { - mHandler.post(() -> { - if (mBinder == null) { - runner.onError(); - return; - } + boolean checkServiceResolves(); - try { - runner.run(mBinder); - } catch (RuntimeException | RemoteException e) { - // binders may propagate some specific non-RemoteExceptions from the other side - // through the binder as well - we cannot allow those to crash the system server - Log.e(TAG, getLogPrefix() + " exception running on " + mTargetService, e); - runner.onError(); - } - }); - } + /** + * Registers the ServiceWatcher, so that it will begin maintaining an active binding to the + * service selected by {@link ServiceSupplier}, until {@link #unregister()} is called. + */ + void register(); - private String getLogPrefix() { - return "[" + mIntent.getAction() + "]"; - } + /** + * Unregisters the ServiceWatcher, so that it will release any active bindings. If the + * ServiceWatcher is currently bound, this will result in one final + * {@link ServiceListener#onUnbind()} invocation, which may happen after this method completes + * (but which is guaranteed to occur before any further + * {@link ServiceListener#onBind(IBinder, BoundServiceInfo)} invocation in response to a later + * call to {@link #register()}). + */ + void unregister(); - @Override - public String toString() { - return mTargetService.toString(); - } + /** + * Runs the given binder operation on the currently bound service (if available). The operation + * will always fail if the ServiceWatcher is not currently registered. + */ + void runOnBinder(BinderOperation operation); /** - * Dump for debugging. + * Dumps ServiceWatcher information. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("target service=" + mTargetService); - pw.println("connected=" + (mBinder != null)); - } -} + void dump(PrintWriter pw); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java new file mode 100644 index 000000000000..7757a7a096db --- /dev/null +++ b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2021 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.server.servicewatcher; + +import static android.content.Context.BIND_AUTO_CREATE; +import static android.content.Context.BIND_NOT_FOREGROUND; +import static android.content.Context.BIND_NOT_VISIBLE; + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.content.PackageMonitor; +import com.android.internal.util.Preconditions; +import com.android.server.servicewatcher.ServiceWatcher.BoundServiceInfo; +import com.android.server.servicewatcher.ServiceWatcher.ServiceChangedListener; + +import java.io.PrintWriter; +import java.util.Objects; + +/** + * Implementation of ServiceWatcher. Keeping the implementation separate from the interface allows + * us to store the generic relationship between the service supplier and the service listener, while + * hiding the generics from clients, simplifying the API. + */ +class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceWatcher, + ServiceChangedListener { + + static final String TAG = "ServiceWatcher"; + static final boolean D = Log.isLoggable(TAG, Log.DEBUG); + + static final long RETRY_DELAY_MS = 15 * 1000; + + final Context mContext; + final Handler mHandler; + final String mTag; + final ServiceSupplier<TBoundServiceInfo> mServiceSupplier; + final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener; + + private final PackageMonitor mPackageMonitor = new PackageMonitor() { + @Override + public boolean onPackageChanged(String packageName, int uid, String[] components) { + return true; + } + + @Override + public void onSomePackagesChanged() { + onServiceChanged(false); + } + }; + + @GuardedBy("this") + private boolean mRegistered = false; + @GuardedBy("this") + private MyServiceConnection mServiceConnection = new MyServiceConnection(null); + + ServiceWatcherImpl(Context context, Handler handler, String tag, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + ServiceListener<? super TBoundServiceInfo> serviceListener) { + mContext = context; + mHandler = handler; + mTag = tag; + mServiceSupplier = serviceSupplier; + mServiceListener = serviceListener; + } + + @Override + public boolean checkServiceResolves() { + return mServiceSupplier.hasMatchingService(); + } + + @Override + public synchronized void register() { + Preconditions.checkState(!mRegistered); + + mRegistered = true; + mPackageMonitor.register(mContext, UserHandle.ALL, /*externalStorage=*/ true, mHandler); + mServiceSupplier.register(this); + + onServiceChanged(false); + } + + @Override + public synchronized void unregister() { + Preconditions.checkState(mRegistered); + + mServiceSupplier.unregister(); + mPackageMonitor.unregister(); + mRegistered = false; + + onServiceChanged(false); + } + + @Override + public synchronized void onServiceChanged() { + onServiceChanged(false); + } + + @Override + public synchronized void runOnBinder(BinderOperation operation) { + MyServiceConnection serviceConnection = mServiceConnection; + mHandler.post(() -> serviceConnection.runOnBinder(operation)); + } + + synchronized void onServiceChanged(boolean forceRebind) { + TBoundServiceInfo newBoundServiceInfo; + if (mRegistered) { + newBoundServiceInfo = mServiceSupplier.getServiceInfo(); + } else { + newBoundServiceInfo = null; + } + + if (forceRebind || !Objects.equals(mServiceConnection.getBoundServiceInfo(), + newBoundServiceInfo)) { + MyServiceConnection oldServiceConnection = mServiceConnection; + MyServiceConnection newServiceConnection = new MyServiceConnection(newBoundServiceInfo); + mServiceConnection = newServiceConnection; + mHandler.post(() -> { + oldServiceConnection.unbind(); + newServiceConnection.bind(); + }); + } + } + + @Override + public String toString() { + return mServiceConnection.getBoundServiceInfo().toString(); + } + + @Override + public void dump(PrintWriter pw) { + pw.println("target service=" + mServiceConnection.getBoundServiceInfo()); + pw.println("connected=" + mServiceConnection.isConnected()); + } + + // runs on the handler thread, and expects most of it's methods to be called from that thread + private class MyServiceConnection implements ServiceConnection { + + private final @Nullable TBoundServiceInfo mBoundServiceInfo; + + // volatile so that isConnected can be called from any thread easily + private volatile @Nullable IBinder mBinder; + private @Nullable Runnable mRebinder; + + MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) { + mBoundServiceInfo = boundServiceInfo; + } + + // may be called from any thread + @Nullable TBoundServiceInfo getBoundServiceInfo() { + return mBoundServiceInfo; + } + + // may be called from any thread + boolean isConnected() { + return mBinder != null; + } + + void bind() { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + + if (mBoundServiceInfo == null) { + return; + } + + Log.i(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo); + + Intent bindIntent = new Intent(mBoundServiceInfo.getAction()).setComponent( + mBoundServiceInfo.getComponentName()); + if (!mContext.bindServiceAsUser(bindIntent, this, + BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE, + mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) { + Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later"); + mRebinder = this::bind; + mHandler.postDelayed(mRebinder, RETRY_DELAY_MS); + } else { + mRebinder = null; + } + } + + void unbind() { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + + if (mBoundServiceInfo == null) { + return; + } + + if (D) { + Log.d(TAG, "[" + mTag + "] unbinding from " + mBoundServiceInfo); + } + + if (mRebinder != null) { + mHandler.removeCallbacks(mRebinder); + mRebinder = null; + } else { + mContext.unbindService(this); + } + + onServiceDisconnected(mBoundServiceInfo.getComponentName()); + } + + void runOnBinder(BinderOperation operation) { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + + if (mBinder == null) { + operation.onError(); + return; + } + + try { + operation.run(mBinder); + } catch (RuntimeException | RemoteException e) { + // binders may propagate some specific non-RemoteExceptions from the other side + // through the binder as well - we cannot allow those to crash the system server + Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e); + operation.onError(); + } + } + + @Override + public final void onServiceConnected(ComponentName component, IBinder binder) { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + Preconditions.checkState(mBinder == null); + + if (D) { + Log.d(TAG, "[" + mTag + "] connected to " + component.toShortString()); + } + + mBinder = binder; + + if (mServiceListener != null) { + try { + mServiceListener.onBind(binder, mBoundServiceInfo); + } catch (RuntimeException | RemoteException e) { + // binders may propagate some specific non-RemoteExceptions from the other side + // through the binder as well - we cannot allow those to crash the system server + Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e); + } + } + } + + @Override + public final void onServiceDisconnected(ComponentName component) { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + + if (mBinder == null) { + return; + } + + if (D) { + Log.d(TAG, "[" + mTag + "] disconnected from " + mBoundServiceInfo); + } + + mBinder = null; + if (mServiceListener != null) { + mServiceListener.onUnbind(); + } + } + + @Override + public final void onBindingDied(ComponentName component) { + Preconditions.checkState(Looper.myLooper() == mHandler.getLooper()); + + Log.i(TAG, "[" + mTag + "] " + mBoundServiceInfo + " died"); + + onServiceChanged(true); + } + + @Override + public final void onNullBinding(ComponentName component) { + Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " has null binding"); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java index 0b51488280e0..6c3f016588f8 100644 --- a/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java +++ b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java @@ -16,19 +16,14 @@ package com.android.server.timezonedetector.location; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.Manifest.permission.BIND_TIME_ZONE_PROVIDER_SERVICE; +import static android.Manifest.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE; import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; -import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.warnLog; - -import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -39,11 +34,12 @@ import android.service.timezone.TimeZoneProviderSuggestion; import android.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.CurrentUserServiceSupplier.BoundServiceInfo; import com.android.server.servicewatcher.ServiceWatcher; -import com.android.server.servicewatcher.ServiceWatcher.BoundService; +import com.android.server.servicewatcher.ServiceWatcher.ServiceListener; import java.util.Objects; -import java.util.function.Predicate; /** * System server-side proxy for ITimeZoneProvider implementations, i.e. this provides the @@ -52,7 +48,8 @@ import java.util.function.Predicate; * different process. As "remote" providers are bound / unbound this proxy will rebind to the "best" * available remote process. */ -class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { +class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy implements + ServiceListener<BoundServiceInfo> { @NonNull private final ServiceWatcher mServiceWatcher; @@ -69,38 +66,13 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { super(context, threadingDomain); mManagerProxy = null; mRequest = TimeZoneProviderRequest.createStopUpdatesRequest(); - - // A predicate that is used to confirm that an intent service can be used as a - // location-based TimeZoneProvider. The service must: - // 1) Declare android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE" - this - // ensures that the provider will only communicate with the system server. - // 2) Be in an application that has been granted the - // android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE permission. This - // ensures only trusted time zone providers will be discovered. - final String requiredClientPermission = Manifest.permission.BIND_TIME_ZONE_PROVIDER_SERVICE; - final String requiredPermission = - Manifest.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE; - Predicate<ResolveInfo> intentServiceCheckPredicate = resolveInfo -> { - ServiceInfo serviceInfo = resolveInfo.serviceInfo; - - boolean hasClientPermissionRequirement = - requiredClientPermission.equals(serviceInfo.permission); - - String packageName = serviceInfo.packageName; - PackageManager packageManager = context.getPackageManager(); - int checkResult = packageManager.checkPermission(requiredPermission, packageName); - boolean hasRequiredPermission = checkResult == PERMISSION_GRANTED; - - boolean result = hasClientPermissionRequirement && hasRequiredPermission; - if (!result) { - warnLog("resolveInfo=" + resolveInfo + " does not meet requirements:" - + " hasClientPermissionRequirement=" + hasClientPermissionRequirement - + ", hasRequiredPermission=" + hasRequiredPermission); - } - return result; - }; - mServiceWatcher = new ServiceWatcher(context, handler, action, this::onBind, this::onUnbind, - enableOverlayResId, nonOverlayPackageResId, intentServiceCheckPredicate); + mServiceWatcher = ServiceWatcher.create(context, + handler, + "RealLocationTimeZoneProviderProxy", + new CurrentUserServiceSupplier(context, action, enableOverlayResId, + nonOverlayPackageResId, BIND_TIME_ZONE_PROVIDER_SERVICE, + INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE), + this); } @Override @@ -123,7 +95,8 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { return resolves; } - private void onBind(IBinder binder, BoundService boundService) { + @Override + public void onBind(IBinder binder, BoundServiceInfo boundService) { mThreadingDomain.assertCurrentThread(); synchronized (mSharedLock) { @@ -138,7 +111,8 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { } } - private void onUnbind() { + @Override + public void onUnbind() { mThreadingDomain.assertCurrentThread(); synchronized (mSharedLock) { @@ -199,7 +173,7 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { synchronized (mSharedLock) { ipw.println("{RealLocationTimeZoneProviderProxy}"); ipw.println("mRequest=" + mRequest); - mServiceWatcher.dump(null, ipw, args); + mServiceWatcher.dump(ipw); } } |