[go: nahoru, domu]

Make KeyedAppStatesReporter abstract.

Move existing functionality into SingletonKeyedAppStatesReporter.
KeyedAppStatesReporter can now have additional subclasses for
testing.

Test: ./gradlew :enterprise-feedback:test
Bug: 129753359
Change-Id: Ie905db387848c8be263d15acba97e41b81cac75f
diff --git a/enterprise/feedback/api/1.0.0-alpha02.txt b/enterprise/feedback/api/1.0.0-alpha02.txt
index e85daff..7aa61dc 100644
--- a/enterprise/feedback/api/1.0.0-alpha02.txt
+++ b/enterprise/feedback/api/1.0.0-alpha02.txt
@@ -22,12 +22,9 @@
     method public abstract androidx.enterprise.feedback.KeyedAppState.KeyedAppStateBuilder setSeverity(int);
   }
 
-  public class KeyedAppStatesReporter {
-    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
-    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
-    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    field public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+  public abstract class KeyedAppStatesReporter {
+    method public abstract void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public abstract void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
   }
 
   public abstract class KeyedAppStatesService extends android.app.Service {
@@ -56,5 +53,12 @@
     method public abstract androidx.enterprise.feedback.ReceivedKeyedAppState.ReceivedKeyedAppStateBuilder setTimestamp(long);
   }
 
+  public class SingletonKeyedAppStatesReporter extends androidx.enterprise.feedback.KeyedAppStatesReporter {
+    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
+    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
+    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+  }
+
 }
 
diff --git a/enterprise/feedback/api/current.txt b/enterprise/feedback/api/current.txt
index e85daff..7aa61dc 100644
--- a/enterprise/feedback/api/current.txt
+++ b/enterprise/feedback/api/current.txt
@@ -22,12 +22,9 @@
     method public abstract androidx.enterprise.feedback.KeyedAppState.KeyedAppStateBuilder setSeverity(int);
   }
 
-  public class KeyedAppStatesReporter {
-    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
-    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
-    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    field public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+  public abstract class KeyedAppStatesReporter {
+    method public abstract void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public abstract void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
   }
 
   public abstract class KeyedAppStatesService extends android.app.Service {
@@ -56,5 +53,12 @@
     method public abstract androidx.enterprise.feedback.ReceivedKeyedAppState.ReceivedKeyedAppStateBuilder setTimestamp(long);
   }
 
+  public class SingletonKeyedAppStatesReporter extends androidx.enterprise.feedback.KeyedAppStatesReporter {
+    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
+    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
+    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+  }
+
 }
 
diff --git a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
index 0fea1b5..3dc36fc 100644
--- a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
+++ b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
@@ -16,48 +16,27 @@
 
 package androidx.enterprise.feedback;
 
-import android.annotation.SuppressLint;
 import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.Build;
-import android.os.Bundle;
 import android.os.Message;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 /**
  * A reporter of keyed app states to enable communication between an app and an EMM (enterprise
  * mobility management).
+ *
+ * For production use {@link SingletonKeyedAppStatesReporter}.
  */
-public class KeyedAppStatesReporter {
+public abstract class KeyedAppStatesReporter {
 
-    private static final String LOG_TAG = "KeyedAppStatesReporter";
+    // Package-private constructor to restrict subclasses to the same package
+    KeyedAppStatesReporter() {}
 
     static final String PHONESKY_PACKAGE_NAME = "com.android.vending";
 
-    @SuppressLint("StaticFieldLeak") // Application Context only.
-    private static volatile KeyedAppStatesReporter sSingleton;
-
     /** The value of {@link Message#what} to indicate a state update. */
     static final int WHAT_STATE = 1;
 
@@ -99,85 +78,15 @@
     static final String APP_STATE_DATA = "androidx.enterprise.feedback.APP_STATE_DATA";
 
     /** The intent action for reporting app states. */
-    public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+    static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
 
-    private final Context mContext;
+    static boolean canPackageReceiveAppStates(Context context, String packageName) {
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
 
-    private final Map<String, BufferedServiceConnection> mServiceConnections = new HashMap<>();
-
-    private static final int EXECUTOR_IDLE_ALIVE_TIME_SECS = 20;
-    private final Executor mExecutor;
-
-    /**
-     * Creates an {@link ExecutorService} which has no persistent background thread, and ensures
-     * tasks will run in submit order.
-     */
-    private static ExecutorService createExecutorService() {
-        return new ThreadPoolExecutor(
-                /* corePoolSize= */ 0,
-                /* maximumPoolSize= */ 1,
-                EXECUTOR_IDLE_ALIVE_TIME_SECS,
-                TimeUnit.SECONDS,
-                new LinkedBlockingQueue<Runnable>() /* Not used */);
-    }
-
-    /**
-     * Sets executor used to construct the singleton.
-     *
-     * <p>If required, this method must be called before calling {@link #getInstance(Context)}.
-     *
-     * <p>If this method is not called, the reporter will run on a newly-created thread.
-     * This newly-created thread will be cleaned up and recreated as necessary when idle.
-     */
-    public static void initialize(@NonNull Context context, @NonNull Executor executor) {
-        if (context == null || executor == null) {
-            throw new NullPointerException();
-        }
-        synchronized (KeyedAppStatesReporter.class) {
-            if (sSingleton != null) {
-                throw new IllegalStateException(
-                        "initialize can only be called once and must be called before "
-                            + "calling getInstance.");
-            }
-            initializeSingleton(context, executor);
-        }
-    }
-
-    /**
-     * Returns an instance of the reporter.
-     *
-     * <p>Creates and initializes an instance if one doesn't already exist.
-     */
-    @NonNull
-    public static KeyedAppStatesReporter getInstance(@NonNull Context context) {
-        if (context == null || context.getApplicationContext() == null) {
-            throw new NullPointerException();
-        }
-        if (sSingleton == null) {
-            synchronized (KeyedAppStatesReporter.class) {
-                if (sSingleton == null) {
-                    initializeSingleton(context, createExecutorService());
-                }
-            }
-        }
-        return sSingleton;
-    }
-
-    private static void initializeSingleton(@NonNull Context context, @NonNull Executor executor) {
-        sSingleton = new KeyedAppStatesReporter(context, executor);
-        sSingleton.bind();
-    }
-
-    @VisibleForTesting
-    static void resetSingleton() {
-        synchronized (KeyedAppStatesReporter.class) {
-            sSingleton = null;
-        }
-    }
-
-    private KeyedAppStatesReporter(Context context, Executor executor) {
-        this.mContext = context.getApplicationContext();
-        this.mExecutor = executor;
+        return packageName.equals(PHONESKY_PACKAGE_NAME)
+            || devicePolicyManager.isDeviceOwnerApp(packageName)
+            || devicePolicyManager.isProfileOwnerApp(packageName);
     }
 
     /**
@@ -201,25 +110,7 @@
      *
      * @see #setStatesImmediate(Collection)
      */
-    public void setStates(@NonNull Collection<KeyedAppState> states) {
-        setStates(states, false);
-    }
-
-    private void setStates(final Collection<KeyedAppState> states, final boolean immediate) {
-        mExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                if (states.isEmpty()) {
-                    return;
-                }
-
-                unbindOldBindings();
-                bind();
-
-                send(buildStatesBundle(states), immediate);
-            }
-        });
-    }
+    public abstract void setStates(@NonNull Collection<KeyedAppState> states);
 
     /**
      * Performs the same function as {@link #setStates(Collection)}, except it
@@ -229,150 +120,5 @@
      * <p>The receiver is not obligated to meet this immediate upload request.
      * For example, Play and Android Management APIs have daily quotas.
      */
-    public void setStatesImmediate(@NonNull Collection<KeyedAppState> states) {
-        setStates(states, true);
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void bind() {
-        Collection<String> acceptablePackageNames = getDeviceOwnerAndProfileOwnerPackageNames();
-        acceptablePackageNames.add(PHONESKY_PACKAGE_NAME);
-        bind(acceptablePackageNames);
-    }
-
-    private void bind(Collection<String> acceptablePackageNames) {
-        // Remove already-bound packages
-        Collection<String> filteredPackageNames = new HashSet<>();
-        for (String packageName : acceptablePackageNames) {
-            if (!mServiceConnections.containsKey(packageName)) {
-                filteredPackageNames.add(packageName);
-            }
-        }
-
-        if (filteredPackageNames.isEmpty()) {
-            return;
-        }
-
-        Collection<ServiceInfo> serviceInfos =
-                getServiceInfoInPackages(new Intent(ACTION_APP_STATES), filteredPackageNames);
-
-        for (ServiceInfo serviceInfo : serviceInfos) {
-            Intent bindIntent = new Intent();
-            bindIntent.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
-
-            BufferedServiceConnection bufferedServiceConnection =
-                    new BufferedServiceConnection(
-                        mExecutor, mContext, bindIntent, Context.BIND_AUTO_CREATE);
-            bufferedServiceConnection.bindService();
-
-            mServiceConnections.put(serviceInfo.packageName, bufferedServiceConnection);
-        }
-    }
-
-    private Collection<String> getDeviceOwnerAndProfileOwnerPackageNames() {
-        DevicePolicyManager devicePolicyManager =
-                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
-        Collection<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
-
-        if (activeAdmins == null) {
-            return new ArrayList<>();
-        }
-
-        Collection<String> deviceOwnerProfileOwnerPackageNames = new ArrayList<>();
-
-        for (ComponentName componentName : activeAdmins) {
-            if (devicePolicyManager.isDeviceOwnerApp(componentName.getPackageName())
-                    || devicePolicyManager.isProfileOwnerApp(componentName.getPackageName())) {
-                deviceOwnerProfileOwnerPackageNames.add(componentName.getPackageName());
-            }
-        }
-
-        return deviceOwnerProfileOwnerPackageNames;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void unbindOldBindings() {
-        Iterator<Entry<String, BufferedServiceConnection>> iterator =
-                mServiceConnections.entrySet().iterator();
-
-        while (iterator.hasNext()) {
-            Entry<String, BufferedServiceConnection> entry = iterator.next();
-            if (packageNameShouldBeUnbound(entry.getKey())) {
-                entry.getValue().unbind();
-                iterator.remove();
-            }
-        }
-    }
-
-    /** Assumes the given package name is a stored service connection. */
-    private boolean packageNameShouldBeUnbound(String packageName) {
-        if (Build.VERSION.SDK_INT < 26
-                && mServiceConnections.get(packageName).hasBeenDisconnected()) {
-            return true;
-        }
-
-        if (mServiceConnections.get(packageName).isDead()) {
-            return true;
-        }
-
-        if (!canPackageReceiveAppStates(mContext, packageName)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    static boolean canPackageReceiveAppStates(Context context, String packageName) {
-        DevicePolicyManager devicePolicyManager =
-                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
-
-        return packageName.equals(PHONESKY_PACKAGE_NAME)
-                || devicePolicyManager.isDeviceOwnerApp(packageName)
-                || devicePolicyManager.isProfileOwnerApp(packageName);
-    }
-
-    private Collection<ServiceInfo> getServiceInfoInPackages(
-            Intent intent, Collection<String> acceptablePackageNames) {
-        PackageManager packageManager = mContext.getPackageManager();
-        List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent, /* flags = */0);
-
-        Collection<ServiceInfo> validServiceInfo = new ArrayList<>();
-        for (ResolveInfo i : resolveInfos) {
-            if (acceptablePackageNames.contains(i.serviceInfo.packageName)) {
-                validServiceInfo.add(i.serviceInfo);
-            }
-        }
-        return validServiceInfo;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Bundle buildStatesBundle(Collection<KeyedAppState> keyedAppStates) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelableArrayList(APP_STATES, buildStateBundles(keyedAppStates));
-        return bundle;
-    }
-
-    // Returns an ArrayList as required to be used with Bundle#putParcelableArrayList.
-    private static ArrayList<Bundle> buildStateBundles(Collection<KeyedAppState> keyedAppStates) {
-        ArrayList<Bundle> bundles = new ArrayList<>();
-        for (KeyedAppState keyedAppState : keyedAppStates) {
-            bundles.add(keyedAppState.toStateBundle());
-        }
-        return bundles;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void send(Bundle appStatesBundle, boolean immediate) {
-        for (BufferedServiceConnection serviceConnection : mServiceConnections.values()) {
-            // Messages cannot be reused so we create a copy for each service connection.
-            serviceConnection.send(createStateMessage(appStatesBundle, immediate));
-        }
-    }
-
-    private static Message createStateMessage(Bundle appStatesBundle, boolean immediate) {
-        Message message = Message.obtain();
-        message.what = immediate ? WHAT_IMMEDIATE_STATE : WHAT_STATE;
-        message.obj = appStatesBundle;
-        return message;
-    }
+    public abstract void setStatesImmediate(@NonNull Collection<KeyedAppState> states);
 }
diff --git a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java
new file mode 100644
index 0000000..fba3232
--- /dev/null
+++ b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2019 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 androidx.enterprise.feedback;
+
+import android.annotation.SuppressLint;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link KeyedAppStatesReporter} that only allows a single instance to exist at one time,
+ * avoiding repeated instantiations.
+ */
+public class SingletonKeyedAppStatesReporter extends KeyedAppStatesReporter {
+
+    private static final String LOG_TAG = "KeyedAppStatesReporter";
+
+    @SuppressLint("StaticFieldLeak") // Application Context only.
+    private static volatile SingletonKeyedAppStatesReporter sSingleton;
+
+    private final Context mContext;
+
+    private final Map<String, BufferedServiceConnection> mServiceConnections = new HashMap<>();
+
+    private static final int EXECUTOR_IDLE_ALIVE_TIME_SECS = 20;
+    private final Executor mExecutor;
+
+    /**
+     * Creates an {@link ExecutorService} which has no persistent background thread, and ensures
+     * tasks will run in submit order.
+     */
+    private static ExecutorService createExecutorService() {
+        return new ThreadPoolExecutor(
+                /* corePoolSize= */ 0,
+                /* maximumPoolSize= */ 1,
+                EXECUTOR_IDLE_ALIVE_TIME_SECS,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>() /* Not used */);
+    }
+
+    /**
+     * Sets executor used to construct the singleton.
+     *
+     * <p>If required, this method must be called before calling {@link #getInstance(Context)}.
+     *
+     * <p>If this method is not called, the reporter will run on a newly-created thread.
+     * This newly-created thread will be cleaned up and recreated as necessary when idle.
+     */
+    public static void initialize(@NonNull Context context, @NonNull Executor executor) {
+        if (context == null || executor == null) {
+            throw new NullPointerException();
+        }
+        synchronized (KeyedAppStatesReporter.class) {
+            if (sSingleton != null) {
+                throw new IllegalStateException(
+                        "initialize can only be called once and must be called before "
+                            + "calling getInstance.");
+            }
+            initializeSingleton(context, executor);
+        }
+    }
+
+    /**
+     * Returns an instance of the reporter.
+     *
+     * <p>Creates and initializes an instance if one doesn't already exist.
+     */
+    @NonNull
+    public static KeyedAppStatesReporter getInstance(@NonNull Context context) {
+        if (context == null || context.getApplicationContext() == null) {
+            throw new NullPointerException();
+        }
+        if (sSingleton == null) {
+            synchronized (KeyedAppStatesReporter.class) {
+                if (sSingleton == null) {
+                    initializeSingleton(context, createExecutorService());
+                }
+            }
+        }
+        return sSingleton;
+    }
+
+    private static void initializeSingleton(@NonNull Context context, @NonNull Executor executor) {
+        sSingleton = new SingletonKeyedAppStatesReporter(context, executor);
+        sSingleton.bind();
+    }
+
+    @VisibleForTesting
+    static void resetSingleton() {
+        synchronized (KeyedAppStatesReporter.class) {
+            sSingleton = null;
+        }
+    }
+
+    private SingletonKeyedAppStatesReporter(Context context, Executor executor) {
+        this.mContext = context.getApplicationContext();
+        this.mExecutor = executor;
+    }
+
+    @Override
+    public void setStates(@NonNull Collection<KeyedAppState> states) {
+        setStates(states, false);
+    }
+
+    private void setStates(final Collection<KeyedAppState> states, final boolean immediate) {
+        mExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (states.isEmpty()) {
+                    return;
+                }
+
+                unbindOldBindings();
+                bind();
+
+                send(buildStatesBundle(states), immediate);
+            }
+        });
+    }
+
+    @Override
+    public void setStatesImmediate(@NonNull Collection<KeyedAppState> states) {
+        setStates(states, true);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void bind() {
+        Collection<String> acceptablePackageNames = getDeviceOwnerAndProfileOwnerPackageNames();
+        acceptablePackageNames.add(PHONESKY_PACKAGE_NAME);
+        bind(acceptablePackageNames);
+    }
+
+    private void bind(Collection<String> acceptablePackageNames) {
+        // Remove already-bound packages
+        Collection<String> filteredPackageNames = new HashSet<>();
+        for (String packageName : acceptablePackageNames) {
+            if (!mServiceConnections.containsKey(packageName)) {
+                filteredPackageNames.add(packageName);
+            }
+        }
+
+        if (filteredPackageNames.isEmpty()) {
+            return;
+        }
+
+        Collection<ServiceInfo> serviceInfos =
+                getServiceInfoInPackages(new Intent(ACTION_APP_STATES), filteredPackageNames);
+
+        for (ServiceInfo serviceInfo : serviceInfos) {
+            Intent bindIntent = new Intent();
+            bindIntent.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
+
+            BufferedServiceConnection bufferedServiceConnection =
+                    new BufferedServiceConnection(
+                        mExecutor, mContext, bindIntent, Context.BIND_AUTO_CREATE);
+            bufferedServiceConnection.bindService();
+
+            mServiceConnections.put(serviceInfo.packageName, bufferedServiceConnection);
+        }
+    }
+
+    private Collection<String> getDeviceOwnerAndProfileOwnerPackageNames() {
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        Collection<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
+
+        if (activeAdmins == null) {
+            return new ArrayList<>();
+        }
+
+        Collection<String> deviceOwnerProfileOwnerPackageNames = new ArrayList<>();
+
+        for (ComponentName componentName : activeAdmins) {
+            if (devicePolicyManager.isDeviceOwnerApp(componentName.getPackageName())
+                    || devicePolicyManager.isProfileOwnerApp(componentName.getPackageName())) {
+                deviceOwnerProfileOwnerPackageNames.add(componentName.getPackageName());
+            }
+        }
+
+        return deviceOwnerProfileOwnerPackageNames;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void unbindOldBindings() {
+        Iterator<Entry<String, BufferedServiceConnection>> iterator =
+                mServiceConnections.entrySet().iterator();
+
+        while (iterator.hasNext()) {
+            Entry<String, BufferedServiceConnection> entry = iterator.next();
+            if (packageNameShouldBeUnbound(entry.getKey())) {
+                entry.getValue().unbind();
+                iterator.remove();
+            }
+        }
+    }
+
+    /** Assumes the given package name is a stored service connection. */
+    private boolean packageNameShouldBeUnbound(String packageName) {
+        if (Build.VERSION.SDK_INT < 26
+                && mServiceConnections.get(packageName).hasBeenDisconnected()) {
+            return true;
+        }
+
+        if (mServiceConnections.get(packageName).isDead()) {
+            return true;
+        }
+
+        if (!canPackageReceiveAppStates(mContext, packageName)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private Collection<ServiceInfo> getServiceInfoInPackages(
+            Intent intent, Collection<String> acceptablePackageNames) {
+        PackageManager packageManager = mContext.getPackageManager();
+        List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent, /* flags = */0);
+
+        Collection<ServiceInfo> validServiceInfo = new ArrayList<>();
+        for (ResolveInfo i : resolveInfos) {
+            if (acceptablePackageNames.contains(i.serviceInfo.packageName)) {
+                validServiceInfo.add(i.serviceInfo);
+            }
+        }
+        return validServiceInfo;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static Bundle buildStatesBundle(Collection<KeyedAppState> keyedAppStates) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(APP_STATES, buildStateBundles(keyedAppStates));
+        return bundle;
+    }
+
+    // Returns an ArrayList as required to be used with Bundle#putParcelableArrayList.
+    private static ArrayList<Bundle> buildStateBundles(Collection<KeyedAppState> keyedAppStates) {
+        ArrayList<Bundle> bundles = new ArrayList<>();
+        for (KeyedAppState keyedAppState : keyedAppStates) {
+            bundles.add(keyedAppState.toStateBundle());
+        }
+        return bundles;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void send(Bundle appStatesBundle, boolean immediate) {
+        for (BufferedServiceConnection serviceConnection : mServiceConnections.values()) {
+            // Messages cannot be reused so we create a copy for each service connection.
+            serviceConnection.send(createStateMessage(appStatesBundle, immediate));
+        }
+    }
+
+    private static Message createStateMessage(Bundle appStatesBundle, boolean immediate) {
+        Message message = Message.obtain();
+        message.what = immediate ? WHAT_IMMEDIATE_STATE : WHAT_STATE;
+        message.obj = appStatesBundle;
+        return message;
+    }
+}
diff --git a/enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java b/enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
similarity index 94%
rename from enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java
rename to enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
index ccce0e4..1b88d1e 100644
--- a/enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java
+++ b/enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
@@ -65,11 +65,11 @@
 import java.util.Collections;
 import java.util.concurrent.Executor;
 
-/** Tests {@link KeyedAppStatesReporter}. */
+/** Tests {@link SingletonKeyedAppStatesReporter}. */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = 21)
-public class KeyedAppStatesReporterTest {
+public class SingletonKeyedAppStatesReporterTest {
 
     private final ComponentName mTestComponentName = new ComponentName("test_package", "");
 
@@ -89,15 +89,15 @@
     @Before
     public void setUp() {
         // Reset the singleton so tests are independent
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
     }
 
     @Test
     @SmallTest
     public void getInstance_nullContext_throwsNullPointerException() {
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
         try {
-            KeyedAppStatesReporter.getInstance(null);
+            SingletonKeyedAppStatesReporter.getInstance(null);
             fail();
         } catch (NullPointerException expected) {
         }
@@ -106,11 +106,11 @@
     @Test
     @SmallTest
     public void initialize_usesExecutor() {
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
         TestExecutor testExecutor = new TestExecutor();
-        KeyedAppStatesReporter.initialize(mContext, testExecutor);
+        SingletonKeyedAppStatesReporter.initialize(mContext, testExecutor);
 
-        KeyedAppStatesReporter.getInstance(mContext).setStates(singleton(mState));
+        SingletonKeyedAppStatesReporter.getInstance(mContext).setStates(singleton(mState));
 
         assertThat(testExecutor.lastExecuted()).isNotNull();
     }
@@ -118,11 +118,11 @@
     @Test
     @SmallTest
     public void initialize_calledMultipleTimes_throwsIllegalStateException() {
-        KeyedAppStatesReporter.resetSingleton();
-        KeyedAppStatesReporter.initialize(mContext, mExecutor);
+        SingletonKeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
 
         try {
-            KeyedAppStatesReporter.initialize(mContext, mExecutor);
+            SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
         } catch (IllegalStateException expected) {
         }
     }
@@ -130,11 +130,11 @@
     @Test
     @SmallTest
     public void initialize_calledAfterGetInstance_throwsIllegalStateException() {
-        KeyedAppStatesReporter.resetSingleton();
-        KeyedAppStatesReporter.getInstance(mContext);
+        SingletonKeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.getInstance(mContext);
 
         try {
-            KeyedAppStatesReporter.initialize(mContext, mExecutor);
+            SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
         } catch (IllegalStateException expected) {
         }
     }
@@ -500,7 +500,7 @@
     }
 
     private KeyedAppStatesReporter getReporter(Context context) {
-        KeyedAppStatesReporter.initialize(context, mExecutor);
-        return KeyedAppStatesReporter.getInstance(context);
+        SingletonKeyedAppStatesReporter.initialize(context, mExecutor);
+        return SingletonKeyedAppStatesReporter.getInstance(context);
     }
 }