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);
             }
         });