Have the NetworkScoreService bind to the scorer.
If the current active scorer provides a service that can handle the
android.net.scoring.SCORE_NETWORKS action then the NetworkScoreService
will bind to that service to keep the scorer alive. If no service is
discovered then no attempt to bind will be made.
BUG: 27612145
Change-Id: I3f6ed0cbd83e658f1533c3e37b0cac2692c01761
diff --git a/core/java/android/net/NetworkScorerAppManager.java b/core/java/android/net/NetworkScorerAppManager.java
index e555fa4..29291ca 100644
--- a/core/java/android/net/NetworkScorerAppManager.java
+++ b/core/java/android/net/NetworkScorerAppManager.java
@@ -19,11 +19,9 @@
import android.Manifest;
import android.Manifest.permission;
import android.annotation.Nullable;
-import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
@@ -69,12 +67,32 @@
*/
public final String mConfigurationActivityClassName;
+ /**
+ * Optional class name of the scoring service we can bind to. Null if none is set.
+ */
+ public final String mScoringServiceClassName;
+
public NetworkScorerAppData(String packageName, int packageUid, CharSequence scorerName,
- @Nullable String configurationActivityClassName) {
+ @Nullable String configurationActivityClassName,
+ @Nullable String scoringServiceClassName) {
mScorerName = scorerName;
mPackageName = packageName;
mPackageUid = packageUid;
mConfigurationActivityClassName = configurationActivityClassName;
+ mScoringServiceClassName = scoringServiceClassName;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("NetworkScorerAppData{");
+ sb.append("mPackageName='").append(mPackageName).append('\'');
+ sb.append(", mPackageUid=").append(mPackageUid);
+ sb.append(", mScorerName=").append(mScorerName);
+ sb.append(", mConfigurationActivityClassName='").append(mConfigurationActivityClassName)
+ .append('\'');
+ sb.append(", mScoringServiceClassName='").append(mScoringServiceClassName).append('\'');
+ sb.append('}');
+ return sb.toString();
}
}
@@ -128,18 +146,27 @@
Intent intent = new Intent(NetworkScoreManager.ACTION_CUSTOM_ENABLE);
intent.setPackage(receiverInfo.packageName);
List<ResolveInfo> configActivities = pm.queryIntentActivities(intent, 0 /* flags */);
- if (!configActivities.isEmpty()) {
+ if (configActivities != null && !configActivities.isEmpty()) {
ActivityInfo activityInfo = configActivities.get(0).activityInfo;
if (activityInfo != null) {
configurationActivityClassName = activityInfo.name;
}
}
+ // Find the scoring service class we can bind to, if any.
+ String scoringServiceClassName = null;
+ Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_SCORE_NETWORKS);
+ serviceIntent.setPackage(receiverInfo.packageName);
+ ResolveInfo resolveServiceInfo = pm.resolveService(serviceIntent, 0 /* flags */);
+ if (resolveServiceInfo != null && resolveServiceInfo.serviceInfo != null) {
+ scoringServiceClassName = resolveServiceInfo.serviceInfo.name;
+ }
+
// NOTE: loadLabel will attempt to load the receiver's label and fall back to the
// app label if none is present.
scorers.add(new NetworkScorerAppData(receiverInfo.packageName,
receiverInfo.applicationInfo.uid, receiverInfo.loadLabel(pm),
- configurationActivityClassName));
+ configurationActivityClassName, scoringServiceClassName));
}
return scorers;
diff --git a/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java b/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
index 9ab62cc..e7aca78 100644
--- a/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
+++ b/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
@@ -23,10 +23,10 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.net.NetworkScorerAppManager.NetworkScorerAppData;
import android.os.UserHandle;
import android.test.InstrumentationTestCase;
-import android.util.Pair;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
@@ -58,25 +58,26 @@
public void testGetAllValidScorers() throws Exception {
// Package 1 - Valid scorer.
- Pair<ResolveInfo, ResolveInfo> package1 = buildResolveInfo("package1", 1, true, true,
- false);
+ ResolveInfoHolder package1 = buildResolveInfo("package1", 1, true, true, false, false);
// Package 2 - Receiver does not have BROADCAST_NETWORK_PRIVILEGED permission.
- Pair<ResolveInfo, ResolveInfo> package2 = buildResolveInfo("package2", 2, false, true,
- false);
+ ResolveInfoHolder package2 = buildResolveInfo("package2", 2, false, true, false, false);
// Package 3 - App does not have SCORE_NETWORKS permission.
- Pair<ResolveInfo, ResolveInfo> package3 = buildResolveInfo("package3", 3, true, false,
- false);
+ ResolveInfoHolder package3 = buildResolveInfo("package3", 3, true, false, false, false);
// Package 4 - Valid scorer w/ optional config activity.
- Pair<ResolveInfo, ResolveInfo> package4 = buildResolveInfo("package4", 4, true, true, true);
+ ResolveInfoHolder package4 = buildResolveInfo("package4", 4, true, true, true, false);
- List<Pair<ResolveInfo, ResolveInfo>> scorers = new ArrayList<>();
+ // Package 5 - Valid scorer w/ optional service to bind to.
+ ResolveInfoHolder package5 = buildResolveInfo("package5", 5, true, true, false, true);
+
+ List<ResolveInfoHolder> scorers = new ArrayList<>();
scorers.add(package1);
scorers.add(package2);
scorers.add(package3);
scorers.add(package4);
+ scorers.add(package5);
setScorers(scorers);
Iterator<NetworkScorerAppData> result =
@@ -94,14 +95,20 @@
assertEquals(4, next.mPackageUid);
assertEquals(".ConfigActivity", next.mConfigurationActivityClassName);
+ assertTrue(result.hasNext());
+ next = result.next();
+ assertEquals("package5", next.mPackageName);
+ assertEquals(5, next.mPackageUid);
+ assertEquals(".ScoringService", next.mScoringServiceClassName);
+
assertFalse(result.hasNext());
}
- private void setScorers(List<Pair<ResolveInfo, ResolveInfo>> scorers) {
+ private void setScorers(List<ResolveInfoHolder> scorers) {
List<ResolveInfo> receivers = new ArrayList<>();
- for (final Pair<ResolveInfo, ResolveInfo> scorer : scorers) {
- receivers.add(scorer.first);
- if (scorer.second != null) {
+ for (final ResolveInfoHolder scorer : scorers) {
+ receivers.add(scorer.scorerResolveInfo);
+ if (scorer.configActivityResolveInfo != null) {
// This scorer has a config activity.
Mockito.when(mMockPm.queryIntentActivities(
Mockito.argThat(new ArgumentMatcher<Intent>() {
@@ -110,10 +117,26 @@
Intent intent = (Intent) object;
return NetworkScoreManager.ACTION_CUSTOM_ENABLE.equals(
intent.getAction())
- && scorer.first.activityInfo.packageName.equals(
+ && scorer.scorerResolveInfo.activityInfo.packageName.equals(
intent.getPackage());
}
- }), Mockito.eq(0))).thenReturn(Collections.singletonList(scorer.second));
+ }), Mockito.eq(0))).thenReturn(
+ Collections.singletonList(scorer.configActivityResolveInfo));
+ }
+
+ if (scorer.serviceResolveInfo != null) {
+ // This scorer has a service to bind to
+ Mockito.when(mMockPm.resolveService(
+ Mockito.argThat(new ArgumentMatcher<Intent>() {
+ @Override
+ public boolean matches(Object object) {
+ Intent intent = (Intent) object;
+ return NetworkScoreManager.ACTION_SCORE_NETWORKS.equals(
+ intent.getAction())
+ && scorer.scorerResolveInfo.activityInfo.packageName.equals(
+ intent.getPackage());
+ }
+ }), Mockito.eq(0))).thenReturn(scorer.serviceResolveInfo);
}
}
@@ -128,9 +151,9 @@
.thenReturn(receivers);
}
- private Pair<ResolveInfo, ResolveInfo> buildResolveInfo(String packageName, int packageUid,
- boolean hasReceiverPermission, boolean hasScorePermission, boolean hasConfigActivity)
- throws Exception {
+ private ResolveInfoHolder buildResolveInfo(String packageName, int packageUid,
+ boolean hasReceiverPermission, boolean hasScorePermission, boolean hasConfigActivity,
+ boolean hasServiceInfo) throws Exception {
Mockito.when(mMockPm.checkPermission(permission.SCORE_NETWORKS, packageName))
.thenReturn(hasScorePermission ?
PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
@@ -150,6 +173,27 @@
configActivityInfo.activityInfo = new ActivityInfo();
configActivityInfo.activityInfo.name = ".ConfigActivity";
}
- return Pair.create(resolveInfo, configActivityInfo);
+
+ ResolveInfo serviceInfo = null;
+ if (hasServiceInfo) {
+ serviceInfo = new ResolveInfo();
+ serviceInfo.serviceInfo = new ServiceInfo();
+ serviceInfo.serviceInfo.name = ".ScoringService";
+ }
+
+ return new ResolveInfoHolder(resolveInfo, configActivityInfo, serviceInfo);
+ }
+
+ private static class ResolveInfoHolder {
+ final ResolveInfo scorerResolveInfo;
+ final ResolveInfo configActivityResolveInfo;
+ final ResolveInfo serviceResolveInfo;
+
+ public ResolveInfoHolder(ResolveInfo scorerResolveInfo,
+ ResolveInfo configActivityResolveInfo, ResolveInfo serviceResolveInfo) {
+ this.scorerResolveInfo = scorerResolveInfo;
+ this.configActivityResolveInfo = configActivityResolveInfo;
+ this.serviceResolveInfo = serviceResolveInfo;
+ }
}
}
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index 879bb6f..2a78f90 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -18,10 +18,12 @@
import android.Manifest.permission;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.INetworkScoreCache;
import android.net.INetworkScoreService;
@@ -30,6 +32,7 @@
import android.net.NetworkScorerAppManager.NetworkScorerAppData;
import android.net.ScoredNetwork;
import android.os.Binder;
+import android.os.IBinder;
import android.os.PatternMatcher;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -55,17 +58,17 @@
*/
public class NetworkScoreService extends INetworkScoreService.Stub {
private static final String TAG = "NetworkScoreService";
+ private static final boolean DBG = false;
private final Context mContext;
-
private final Map<Integer, INetworkScoreCache> mScoreCaches;
-
/** Lock used to update mReceiver when scorer package changes occur. */
- private Object mReceiverLock = new Object[0];
+ private final Object mReceiverLock = new Object[0];
/** Clears scores when the active scorer package is no longer valid. */
@GuardedBy("mReceiverLock")
private ScorerChangedReceiver mReceiver;
+ private ScoringServiceConnection mServiceConnection;
private class ScorerChangedReceiver extends BroadcastReceiver {
final String mRegisteredPackage;
@@ -77,14 +80,23 @@
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if ((Intent.ACTION_PACKAGE_CHANGED.equals(action) ||
- Intent.ACTION_PACKAGE_REPLACED.equals(action) ||
- Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) &&
- NetworkScorerAppManager.getActiveScorer(mContext) == null) {
- // Package change has invalidated a scorer.
- Log.i(TAG, "Package " + mRegisteredPackage +
- " is no longer valid, disabling scoring");
- setScorerInternal(null);
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+ || Intent.ACTION_PACKAGE_REPLACED.equals(action)
+ || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
+ NetworkScorerAppData activeScorer =
+ NetworkScorerAppManager.getActiveScorer(mContext);
+ if (activeScorer == null) {
+ // Package change has invalidated a scorer.
+ Log.i(TAG, "Package " + mRegisteredPackage +
+ " is no longer valid, disabling scoring.");
+ setScorerInternal(null);
+ } else if (activeScorer.mScoringServiceClassName == null) {
+ // The scoring service is not available, make sure it's unbound.
+ unbindFromScoringServiceIfNeeded();
+ } else {
+ // The scoring service may have changed or been added.
+ bindToScoringServiceIfNeeded(activeScorer);
+ }
}
}
}
@@ -96,6 +108,7 @@
/** Called when the system is ready to run third-party code but before it actually does so. */
void systemReady() {
+ if (DBG) Log.d(TAG, "systemReady");
ContentResolver cr = mContext.getContentResolver();
if (Settings.Global.getInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 0) == 0) {
// On first run, we try to initialize the scorer to the one configured at build time.
@@ -111,7 +124,14 @@
registerPackageReceiverIfNeeded();
}
+ /** Called when the system is ready for us to start third-party code. */
+ void systemRunning() {
+ if (DBG) Log.d(TAG, "systemRunning");
+ bindToScoringServiceIfNeeded();
+ }
+
private void registerPackageReceiverIfNeeded() {
+ if (DBG) Log.d(TAG, "registerPackageReceiverIfNeeded");
NetworkScorerAppData scorer = NetworkScorerAppManager.getActiveScorer(mContext);
synchronized (mReceiverLock) {
// Unregister the receiver if the current scorer has changed since last registration.
@@ -142,6 +162,41 @@
}
}
+ private void bindToScoringServiceIfNeeded() {
+ if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
+ NetworkScorerAppData scorerData = NetworkScorerAppManager.getActiveScorer(mContext);
+ bindToScoringServiceIfNeeded(scorerData);
+ }
+
+ private void bindToScoringServiceIfNeeded(NetworkScorerAppData scorerData) {
+ if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + scorerData + ")");
+ if (scorerData != null && scorerData.mScoringServiceClassName != null) {
+ ComponentName componentName =
+ new ComponentName(scorerData.mPackageName, scorerData.mScoringServiceClassName);
+ // If we're connected to a different component then drop it.
+ if (mServiceConnection != null
+ && !mServiceConnection.mComponentName.equals(componentName)) {
+ unbindFromScoringServiceIfNeeded();
+ }
+
+ // If we're not connected at all then create a new connection.
+ if (mServiceConnection == null) {
+ mServiceConnection = new ScoringServiceConnection(componentName);
+ }
+
+ // Make sure the connection is connected (idempotent)
+ mServiceConnection.connect(mContext);
+ }
+ }
+
+ private void unbindFromScoringServiceIfNeeded() {
+ if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
+ if (mServiceConnection != null) {
+ mServiceConnection.disconnect(mContext);
+ }
+ mServiceConnection = null;
+ }
+
@Override
public boolean updateScores(ScoredNetwork[] networks) {
if (!NetworkScorerAppManager.isCallerActiveScorer(mContext, getCallingUid())) {
@@ -228,8 +283,10 @@
/** Set the active scorer. Callers are responsible for checking permissions as appropriate. */
private boolean setScorerInternal(String packageName) {
+ if (DBG) Log.d(TAG, "setScorerInternal(" + packageName + ")");
long token = Binder.clearCallingIdentity();
try {
+ unbindFromScoringServiceIfNeeded();
// Preemptively clear scores even though the set operation could fail. We do this for
// safety as scores should never be compared across apps; in practice, Settings should
// only be allowing valid apps to be set as scorers, so failure here should be rare.
@@ -237,8 +294,13 @@
// Get the scorer that is about to be replaced, if any, so we can notify it directly.
NetworkScorerAppData prevScorer = NetworkScorerAppManager.getActiveScorer(mContext);
boolean result = NetworkScorerAppManager.setActiveScorer(mContext, packageName);
+ // Unconditionally attempt to bind to the current scorer. If setActiveScorer() failed
+ // then we'll attempt to restore the previous binding (if any), otherwise an attempt
+ // will be made to bind to the new scorer.
+ bindToScoringServiceIfNeeded();
if (result) { // new scorer successfully set
registerPackageReceiverIfNeeded();
+
Intent intent = new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED);
if (prevScorer != null) { // Directly notify the old scorer.
intent.setPackage(prevScorer.mPackageName);
@@ -295,7 +357,6 @@
return;
}
writer.println("Current scorer: " + currentScorer.mPackageName);
- writer.flush();
for (INetworkScoreCache scoreCache : getScoreCaches()) {
try {
@@ -307,6 +368,12 @@
}
}
}
+ if (mServiceConnection != null) {
+ mServiceConnection.dump(fd, writer, args);
+ } else {
+ writer.println("ScoringServiceConnection: null");
+ }
+ writer.flush();
}
/**
@@ -320,4 +387,50 @@
return new HashSet<>(mScoreCaches.values());
}
}
+
+ private static class ScoringServiceConnection implements ServiceConnection {
+ private final ComponentName mComponentName;
+ private boolean mBound = false;
+
+ ScoringServiceConnection(ComponentName componentName) {
+ mComponentName = componentName;
+ }
+
+ void connect(Context context) {
+ disconnect(context);
+ Intent service = new Intent();
+ service.setComponent(mComponentName);
+ mBound = context.bindServiceAsUser(service, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ UserHandle.SYSTEM);
+ if (!mBound) {
+ Log.w(TAG, "Bind call failed for " + service);
+ }
+ }
+
+ void disconnect(Context context) {
+ try {
+ if (mBound) {
+ mBound = false;
+ context.unbindService(this);
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Unbind failed.", e);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DBG) Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("ScoringServiceConnection: " + mComponentName + ", bound: " + mBound);
+ }
+ }
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 659450e..154de0c4 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1423,6 +1423,12 @@
} catch (Throwable e) {
reportWtf("Notifying MmsService running", e);
}
+
+ try {
+ if (networkScoreF != null) networkScoreF.systemRunning();
+ } catch (Throwable e) {
+ reportWtf("Notifying NetworkScoreService running", e);
+ }
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
});