content: /**
   * Copyright (c) 2014, 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.notification;
  
  import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
  import static android.content.Context.BIND_AUTO_CREATE;
  import static android.content.Context.BIND_FOREGROUND_SERVICE;
  import static android.content.Context.DEVICE_POLICY_SERVICE;
  import static android.os.UserHandle.USER_ALL;
  
  import android.annotation.NonNull;
  import android.app.ActivityManager;
  import android.app.PendingIntent;
  import android.app.admin.DevicePolicyManager;
  import android.content.ComponentName;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.IPackageManager;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.ResolveInfo;
  import android.content.pm.ServiceInfo;
  import android.content.pm.UserInfo;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.IInterface;
  import android.os.Looper;
  import android.os.RemoteException;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.Settings;
  import android.service.notification.ManagedServiceInfoProto;
  import android.service.notification.ManagedServicesProto;
  import android.service.notification.ManagedServicesProto.ServiceProto;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.IntArray;
  import android.util.Log;
  import android.util.Pair;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.proto.ProtoOutputStream;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.XmlUtils;
  import com.android.internal.util.function.TriPredicate;
  import com.android.server.notification.NotificationManagerService.DumpFilter;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Objects;
  import java.util.Set;
  
  /**
   * Manages the lifecycle of application-provided services bound by system server.
   *
   * Services managed by this helper must have:
   *  - An associated system settings value with a list of enabled component names.
   *  - A well-known action for services to use in their intent-filter.
   *  - A system permission for services to require in order to ensure system has exclusive binding.
   *  - A settings page for user configuration of enabled services, and associated intent action.
   *  - A remote interface definition (aidl) provided by the service used for communication.
   */
  abstract public class ManagedServices {
      protected final String TAG = getClass().getSimpleName();
      protected final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
  
      private static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000;
      protected static final String ENABLED_SERVICES_SEPARATOR = ":";
  
      /**
       * List of components and apps that can have running {@link ManagedServices}.
       */
      static final String TAG_MANAGED_SERVICES = "service_listing";
      static final String ATT_APPROVED_LIST = "approved";
      static final String ATT_USER_ID = "user";
      static final String ATT_IS_PRIMARY = "primary";
      static final String ATT_VERSION = "version";
  
      static final int DB_VERSION = 1;
  
      static final int APPROVAL_BY_PACKAGE = 0;
      static final int APPROVAL_BY_COMPONENT = 1;
  
      protected final Context mContext;
      protected final Object mMutex;
      private final UserProfiles mUserProfiles;
      private final IPackageManager mPm;
      protected final UserManager mUm;
      private final Config mConfig;
      private final Handler mHandler = new Handler(Looper.getMainLooper());
  
      // contains connections to all connected services, including app services
      // and system services
      private final ArrayList<ManagedServiceInfo> mServices = new ArrayList<>();
      /**
       * The services that have been bound by us. If the service is also connected, it will also
       * be in {@link #mServices}.
       */
      private final ArrayList<Pair<ComponentName, Integer>> mServicesBound = new ArrayList<>();
      private final ArraySet<Pair<ComponentName, Integer>> mServicesRebinding = new ArraySet<>();
  
      // lists the component names of all enabled (and therefore potentially connected)
      // app services for current profiles.
      private ArraySet<ComponentName> mEnabledServicesForCurrentProfiles
              = new ArraySet<>();
      // Just the packages from mEnabledServicesForCurrentProfiles
      private ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
      // List of enabled packages that have nevertheless asked not to be run
      private ArraySet<ComponentName> mSnoozingForCurrentProfiles = new ArraySet<>();
  
      // List of approved packages or components (by user, then by primary/secondary) that are
      // allowed to be bound as managed services. A package or component appearing in this list does
      // not mean that we are currently bound to said package/component.
      private ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved = new ArrayMap<>();
  
      // True if approved services are stored in xml, not settings.
      private boolean mUseXml;
  
      // Whether managed services are approved individually or package wide
      protected int mApprovalLevel;
  
      public ManagedServices(Context context, Object mutex, UserProfiles userProfiles,
              IPackageManager pm) {
          mContext = context;
          mMutex = mutex;
          mUserProfiles = userProfiles;
          mPm = pm;
          mConfig = getConfig();
          mApprovalLevel = APPROVAL_BY_COMPONENT;
          mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
      }
  
      abstract protected Config getConfig();
  
      private String getCaption() {
          return mConfig.caption;
      }
  
      abstract protected IInterface asInterface(IBinder binder);
  
      abstract protected boolean checkType(IInterface service);
  
      abstract protected void onServiceAdded(ManagedServiceInfo info);
  
      protected List<ManagedServiceInfo> getServices() {
          synchronized (mMutex) {
              List<ManagedServiceInfo> services = new ArrayList<>(mServices);
              return services;
          }
      }
  
      protected int getBindFlags() {
          return BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE | BIND_ALLOW_WHITELIST_MANAGEMENT;
      }
  
      protected void onServiceRemovedLocked(ManagedServiceInfo removed) { }
  
      private ManagedServiceInfo newServiceInfo(IInterface service,
              ComponentName component, int userId, boolean isSystem, ServiceConnection connection,
              int targetSdkVersion) {
          return new ManagedServiceInfo(service, component, userId, isSystem, connection,
                  targetSdkVersion);
      }
  
      public void onBootPhaseAppsCanStart() {}
  
      public void dump(PrintWriter pw, DumpFilter filter) {
          pw.println("    Allowed " + getCaption() + "s:");
          synchronized (mApproved) {
              final int N = mApproved.size();
              for (int i = 0; i < N; i++) {
                  final int userId = mApproved.keyAt(i);
                  final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.valueAt(i);
                  if (approvedByType != null) {
                      final int M = approvedByType.size();
                      for (int j = 0; j < M; j++) {
                          final boolean isPrimary = approvedByType.keyAt(j);
                          final ArraySet<String> approved = approvedByType.valueAt(j);
                          if (approvedByType != null && approvedByType.size() > 0) {
                              pw.println("      " + String.join(ENABLED_SERVICES_SEPARATOR, approved)
                                      + " (user: " + userId + " isPrimary: " + isPrimary + ")");
                          }
                      }
                  }
              }
          }
  
          pw.println("    All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size()
                  + ") enabled for current profiles:");
          for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
              if (filter != null && !filter.matches(cmpt)) continue;
              pw.println("      " + cmpt);
          }
  
          pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
          synchronized (mMutex) {
              for (ManagedServiceInfo info : mServices) {
                  if (filter != null && !filter.matches(info.component)) continue;
                  pw.println("      " + info.component
                          + " (user " + info.userid + "): " + info.service
                          + (info.isSystem ? " SYSTEM" : "")
                          + (info.isGuest(this) ? " GUEST" : ""));
              }
          }
  
          pw.println("    Snoozed " + getCaption() + "s (" +
                  mSnoozingForCurrentProfiles.size() + "):");
          for (ComponentName name : mSnoozingForCurrentProfiles) {
              pw.println("      " + name.flattenToShortString());
          }
      }
  
      public void dump(ProtoOutputStream proto, DumpFilter filter) {
          proto.write(ManagedServicesProto.CAPTION, getCaption());
          synchronized (mApproved) {
              final int N = mApproved.size();
              for (int i = 0; i < N; i++) {
                  final int userId = mApproved.keyAt(i);
                  final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.valueAt(i);
                  if (approvedByType != null) {
                      final int M = approvedByType.size();
                      for (int j = 0; j < M; j++) {
                          final boolean isPrimary = approvedByType.keyAt(j);
                          final ArraySet<String> approved = approvedByType.valueAt(j);
                          if (approvedByType != null && approvedByType.size() > 0) {
                              final long sToken = proto.start(ManagedServicesProto.APPROVED);
                              for (String s : approved) {
                                  proto.write(ServiceProto.NAME, s);
                              }
                              proto.write(ServiceProto.USER_ID, userId);
                              proto.write(ServiceProto.IS_PRIMARY, isPrimary);
                              proto.end(sToken);
                          }
                      }
                  }
              }
          }
  
          for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
              if (filter != null && !filter.matches(cmpt)) continue;
              cmpt.writeToProto(proto, ManagedServicesProto.ENABLED);
          }
  
          synchronized (mMutex) {
              for (ManagedServiceInfo info : mServices) {
                  if (filter != null && !filter.matches(info.component)) continue;
                  info.writeToProto(proto, ManagedServicesProto.LIVE_SERVICES, this);
              }
          }
  
          for (ComponentName name : mSnoozingForCurrentProfiles) {
              name.writeToProto(proto, ManagedServicesProto.SNOOZED);
          }
      }
  
      protected void onSettingRestored(String element, String value, int backupSdkInt, int userId) {
          if (!mUseXml) {
              Slog.d(TAG, "Restored managed service setting: " + element);
              if (mConfig.secureSettingName.equals(element) ||
                      (mConfig.secondarySettingName != null
                              && mConfig.secondarySettingName.equals(element))) {
                  if (backupSdkInt < Build.VERSION_CODES.O) {
                      // automatic system grants were added in O, so append the approved apps
                      // rather than wiping out the setting
                      String currentSetting =
                              getApproved(userId, mConfig.secureSettingName.equals(element));
                      if (!TextUtils.isEmpty(currentSetting)) {
                          if (!TextUtils.isEmpty(value)) {
                              value = value + ENABLED_SERVICES_SEPARATOR + currentSetting;
                          } else {
                              value = currentSetting;
                          }
                      }
                  }
                  Settings.Secure.putStringForUser(
                          mContext.getContentResolver(), element, value, userId);
                  loadAllowedComponentsFromSettings();
                  rebindServices(false, userId);
              }
          }
      }
  
      public void writeXml(XmlSerializer out, boolean forBackup, int userId) throws IOException {
          out.startTag(null, getConfig().xmlTag);
  
          out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
  
          if (forBackup) {
              trimApprovedListsAccordingToInstalledServices(userId);
          }
  
          synchronized (mApproved) {
              final int N = mApproved.size();
              for (int i = 0; i < N; i++) {
                  final int approvedUserId = mApproved.keyAt(i);
                  if (forBackup && approvedUserId != userId) {
                      continue;
                  }
                  final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.valueAt(i);
                  if (approvedByType != null) {
                      final int M = approvedByType.size();
                      for (int j = 0; j < M; j++) {
                          final boolean isPrimary = approvedByType.keyAt(j);
                          final Set<String> approved = approvedByType.valueAt(j);
                          if (approved != null) {
                              String allowedItems = String.join(ENABLED_SERVICES_SEPARATOR, approved);
                              out.startTag(null, TAG_MANAGED_SERVICES);
                              out.attribute(null, ATT_APPROVED_LIST, allowedItems);
                              out.attribute(null, ATT_USER_ID, Integer.toString(approvedUserId));
                              out.attribute(null, ATT_IS_PRIMARY, Boolean.toString(isPrimary));
                              writeExtraAttributes(out, approvedUserId);
                              out.endTag(null, TAG_MANAGED_SERVICES);
  
                              if (!forBackup && isPrimary) {
                                  // Also write values to settings, for observers who haven't migrated yet
                                  Settings.Secure.putStringForUser(mContext.getContentResolver(),
                                          getConfig().secureSettingName, allowedItems,
                                          approvedUserId);
                              }
  
                          }
                      }
                  }
              }
          }
  
          writeExtraXmlTags(out);
  
          out.endTag(null, getConfig().xmlTag);
      }
  
      /**
       * Writes extra xml attributes to {@link #TAG_MANAGED_SERVICES} tag.
       */
      protected void writeExtraAttributes(XmlSerializer out, int userId) throws IOException {}
  
      /**
       * Writes extra xml tags within the parent tag specified in {@link Config#xmlTag}.
       */
      protected void writeExtraXmlTags(XmlSerializer out) throws IOException {}
  
      /**
       * This is called to process tags other than {@link #TAG_MANAGED_SERVICES}.
       */
      protected void readExtraTag(String tag, XmlPullParser parser) throws IOException {}
  
      protected void migrateToXml() {
          loadAllowedComponentsFromSettings();
      }
  
      public void readXml(
              XmlPullParser parser,
              TriPredicate<String, Integer, String> allowedManagedServicePackages,
              boolean forRestore,
              int userId)
              throws XmlPullParserException, IOException {
          // read grants
          int type;
          while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
              String tag = parser.getName();
              if (type == XmlPullParser.END_TAG
                      && getConfig().xmlTag.equals(tag)) {
                  break;
              }
              if (type == XmlPullParser.START_TAG) {
                  if (TAG_MANAGED_SERVICES.equals(tag)) {
                      Slog.i(TAG, "Read " + mConfig.caption + " permissions from xml");
  
                      final String approved = XmlUtils.readStringAttribute(parser, ATT_APPROVED_LIST);
                      // Ignore parser's user id for restore.
                      final int resolvedUserId = forRestore
                              ? userId : XmlUtils.readIntAttribute(parser, ATT_USER_ID, 0);
                      final boolean isPrimary =
                              XmlUtils.readBooleanAttribute(parser, ATT_IS_PRIMARY, true);
                      readExtraAttributes(tag, parser, resolvedUserId);
                      if (allowedManagedServicePackages == null || allowedManagedServicePackages.test(
                              getPackageName(approved), resolvedUserId, getRequiredPermission())) {
                          if (mUm.getUserInfo(resolvedUserId) != null) {
                              addApprovedList(approved, resolvedUserId, isPrimary);
                          }
                          mUseXml = true;
                      }
                  } else {
                      readExtraTag(tag, parser);
                  }
              }
          }
          rebindServices(false, USER_ALL);
      }
  
      /**
       * Read extra attributes in the {@link #TAG_MANAGED_SERVICES} tag.
       */
      protected void readExtraAttributes(String tag, XmlPullParser parser, int userId)
              throws IOException {}
  
      protected abstract String getRequiredPermission();
  
      private void loadAllowedComponentsFromSettings() {
          for (UserInfo user : mUm.getUsers()) {
              final ContentResolver cr = mContext.getContentResolver();
              addApprovedList(Settings.Secure.getStringForUser(
                      cr,
                      getConfig().secureSettingName,
                      user.id), user.id, true);
              if (!TextUtils.isEmpty(getConfig().secondarySettingName)) {
                  addApprovedList(Settings.Secure.getStringForUser(
                          cr,
                          getConfig().secondarySettingName,
                          user.id), user.id, false);
              }
          }
          Slog.d(TAG, "Done loading approved values from settings");
      }
  
      protected void addApprovedList(String approved, int userId, boolean isPrimary) {
          if (TextUtils.isEmpty(approved)) {
              approved = "";
          }
          synchronized (mApproved) {
              ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId);
              if (approvedByType == null) {
                  approvedByType = new ArrayMap<>();
                  mApproved.put(userId, approvedByType);
              }
  
              ArraySet<String> approvedList = approvedByType.get(isPrimary);
              if (approvedList == null) {
                  approvedList = new ArraySet<>();
                  approvedByType.put(isPrimary, approvedList);
              }
  
              String[] approvedArray = approved.split(ENABLED_SERVICES_SEPARATOR);
              for (String pkgOrComponent : approvedArray) {
                  String approvedItem = getApprovedValue(pkgOrComponent);
                  if (approvedItem != null) {
                      approvedList.add(approvedItem);
                  }
              }
          }
      }
  
      protected boolean isComponentEnabledForPackage(String pkg) {
          return mEnabledServicesPackageNames.contains(pkg);
      }
  
      protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
              boolean isPrimary, boolean enabled) {
          Slog.i(TAG,
                  (enabled ? " Allowing " : "Disallowing ") + mConfig.caption + " " + pkgOrComponent);
          synchronized (mApproved) {
              ArrayMap<Boolean, ArraySet<String>> allowedByType = mApproved.get(userId);
              if (allowedByType == null) {
                  allowedByType = new ArrayMap<>();
                  mApproved.put(userId, allowedByType);
              }
              ArraySet<String> approved = allowedByType.get(isPrimary);
              if (approved == null) {
                  approved = new ArraySet<>();
                  allowedByType.put(isPrimary, approved);
              }
              String approvedItem = getApprovedValue(pkgOrComponent);
  
              if (approvedItem != null) {
                  if (enabled) {
                      approved.add(approvedItem);
                  } else {
                      approved.remove(approvedItem);
                  }
              }
          }
  
          rebindServices(false, userId);
      }
  
      private String getApprovedValue(String pkgOrComponent) {
          if (mApprovalLevel == APPROVAL_BY_COMPONENT) {
              if(ComponentName.unflattenFromString(pkgOrComponent) != null) {
                  return pkgOrComponent;
              }
              return null;
          } else {
              return getPackageName(pkgOrComponent);
          }
      }
  
      protected String getApproved(int userId, boolean primary) {
          synchronized (mApproved) {
              final ArrayMap<Boolean, ArraySet<String>> allowedByType =
                      mApproved.getOrDefault(userId, new ArrayMap<>());
              ArraySet<String> approved = allowedByType.getOrDefault(primary, new ArraySet<>());
              return String.join(ENABLED_SERVICES_SEPARATOR, approved);
          }
      }
  
      protected List<ComponentName> getAllowedComponents(int userId) {
          final List<ComponentName> allowedComponents = new ArrayList<>();
          synchronized (mApproved) {
              final ArrayMap<Boolean, ArraySet<String>> allowedByType =
                      mApproved.getOrDefault(userId, new ArrayMap<>());
              for (int i = 0; i < allowedByType.size(); i++) {
                  final ArraySet<String> allowed = allowedByType.valueAt(i);
                  for (int j = 0; j < allowed.size(); j++) {
                      ComponentName cn = ComponentName.unflattenFromString(allowed.valueAt(j));
                      if (cn != null) {
                          allowedComponents.add(cn);
                      }
                  }
              }
          }
          return allowedComponents;
      }
  
      protected List<String> getAllowedPackages(int userId) {
          final List<String> allowedPackages = new ArrayList<>();
          synchronized (mApproved) {
              final ArrayMap<Boolean, ArraySet<String>> allowedByType =
                      mApproved.getOrDefault(userId, new ArrayMap<>());
              for (int i = 0; i < allowedByType.size(); i++) {
                  final ArraySet<String> allowed = allowedByType.valueAt(i);
                  for (int j = 0; j < allowed.size(); j++) {
                      String pkgName = getPackageName(allowed.valueAt(j));
                      if (!TextUtils.isEmpty(pkgName)) {
                          allowedPackages.add(pkgName);
                      }
                  }
              }
          }
          return allowedPackages;
      }
  
      protected boolean isPackageOrComponentAllowed(String pkgOrComponent, int userId) {
          synchronized (mApproved) {
              ArrayMap<Boolean, ArraySet<String>> allowedByType =
                      mApproved.getOrDefault(userId, new ArrayMap<>());
              for (int i = 0; i < allowedByType.size(); i++) {
                  ArraySet<String> allowed = allowedByType.valueAt(i);
                  if (allowed.contains(pkgOrComponent)) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      protected boolean isPackageAllowed(String pkg, int userId) {
          if (pkg == null) {
              return false;
          }
          synchronized (mApproved) {
              ArrayMap<Boolean, ArraySet<String>> allowedByType =
                      mApproved.getOrDefault(userId, new ArrayMap<>());
              for (int i = 0; i < allowedByType.size(); i++) {
                  ArraySet<String> allowed = allowedByType.valueAt(i);
                  for (String allowedEntry : allowed) {
                      ComponentName component = ComponentName.unflattenFromString(allowedEntry);
                      if (component != null) {
                          if (pkg.equals(component.getPackageName())) {
                              return true;
                          }
                      } else {
                          if (pkg.equals(allowedEntry)) {
                              return true;
                          }
                      }
                  }
              }
          }
          return false;
      }
  
      public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) {
          if (DEBUG) Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage
                  + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
                  + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames);
  
          if (pkgList != null && (pkgList.length > 0)) {
              boolean anyServicesInvolved = false;
              // Remove notification settings for uninstalled package
              if (removingPackage && uidList != null) {
                  int size = Math.min(pkgList.length, uidList.length);
                  for (int i = 0; i < size; i++) {
                      final String pkg = pkgList[i];
                      final int userId = UserHandle.getUserId(uidList[i]);
                      anyServicesInvolved = removeUninstalledItemsFromApprovedLists(userId, pkg);
                  }
              }
              for (String pkgName : pkgList) {
                  if (mEnabledServicesPackageNames.contains(pkgName)) {
                      anyServicesInvolved = true;
                  }
                  if (uidList != null && uidList.length > 0) {
                      for (int uid : uidList) {
                          if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) {
                              anyServicesInvolved = true;
                          }
                      }
                  }
              }
  
              if (anyServicesInvolved) {
                  // make sure we're still bound to any of our services who may have just upgraded
                  rebindServices(false, USER_ALL);
              }
          }
      }
  
      public void onUserRemoved(int user) {
          Slog.i(TAG, "Removing approved services for removed user " + user);
          synchronized (mApproved) {
              mApproved.remove(user);
          }
          rebindServices(true, user);
      }
  
      public void onUserSwitched(int user) {
          if (DEBUG) Slog.d(TAG, "onUserSwitched u=" + user);
          rebindServices(true, user);
      }
  
      public void onUserUnlocked(int user) {
          if (DEBUG) Slog.d(TAG, "onUserUnlocked u=" + user);
          rebindServices(false, user);
      }
  
      private ManagedServiceInfo getServiceFromTokenLocked(IInterface service) {
          if (service == null) {
              return null;
          }
          final IBinder token = service.asBinder();
          final int N = mServices.size();
          for (int i = 0; i < N; i++) {
              final ManagedServiceInfo info = mServices.get(i);
              if (info.service.asBinder() == token) return info;
          }
          return null;
      }
  
      protected boolean isServiceTokenValidLocked(IInterface service) {
          if (service == null) {
              return false;
          }
          ManagedServiceInfo info = getServiceFromTokenLocked(service);
          if (info != null) {
              return true;
          }
          return false;
      }
  
      protected ManagedServiceInfo checkServiceTokenLocked(IInterface service) {
          checkNotNull(service);
          ManagedServiceInfo info = getServiceFromTokenLocked(service);
          if (info != null) {
              return info;
          }
          throw new SecurityException("Disallowed call from unknown " + getCaption() + ": "
                  + service + " " + service.getClass());
      }
  
      public boolean isSameUser(IInterface service, int userId) {
          checkNotNull(service);
          synchronized (mMutex) {
              ManagedServiceInfo info = getServiceFromTokenLocked(service);
              if (info != null) {
                  return info.isSameUser(userId);
              }
              return false;
          }
      }
  
      public void unregisterService(IInterface service, int userid) {
          checkNotNull(service);
          // no need to check permissions; if your service binder is in the list,
          // that's proof that you had permission to add it in the first place
          unregisterServiceImpl(service, userid);
      }
  
      public void registerService(IInterface service, ComponentName component, int userid) {
          checkNotNull(service);
          ManagedServiceInfo info = registerServiceImpl(service, component, userid);
          if (info != null) {
              onServiceAdded(info);
          }
      }
  
      /**
       * Add a service to our callbacks. The lifecycle of this service is managed externally,
       * but unlike a system service, it should not be considered privileged.
       * */
      protected void registerGuestService(ManagedServiceInfo guest) {
          checkNotNull(guest.service);
          if (!checkType(guest.service)) {
              throw new IllegalArgumentException();
          }
          if (registerServiceImpl(guest) != null) {
              onServiceAdded(guest);
          }
      }
  
      protected void setComponentState(ComponentName component, boolean enabled) {
          boolean previous = !mSnoozingForCurrentProfiles.contains(component);
          if (previous == enabled) {
              return;
          }
  
          if (enabled) {
              mSnoozingForCurrentProfiles.remove(component);
          } else {
              mSnoozingForCurrentProfiles.add(component);
          }
  
          // State changed
          Slog.d(TAG, ((enabled) ? "Enabling " : "Disabling ") + "component " +
                  component.flattenToShortString());
  
          synchronized (mMutex) {
              final IntArray userIds = mUserProfiles.getCurrentProfileIds();
  
              for (int i = 0; i < userIds.size(); i++) {
                  final int userId = userIds.get(i);
                  if (enabled) {
                      if (isPackageOrComponentAllowed(component.flattenToString(), userId)
                              || isPackageOrComponentAllowed(component.getPackageName(), userId)) {
                          registerServiceLocked(component, userId);
                      } else {
                          Slog.d(TAG, component + " no longer has permission to be bound");
                      }
                  } else {
                      unregisterServiceLocked(component, userId);
                  }
              }
          }
      }
  
      private @NonNull ArraySet<ComponentName> loadComponentNamesFromValues(
              ArraySet<String> approved, int userId) {
          if (approved == null || approved.size() == 0)
              return new ArraySet<>();
          ArraySet<ComponentName> result = new ArraySet<>(approved.size());
          for (int i = 0; i < approved.size(); i++) {
              final String packageOrComponent = approved.valueAt(i);
              if (!TextUtils.isEmpty(packageOrComponent)) {
                  ComponentName component = ComponentName.unflattenFromString(packageOrComponent);
                  if (component != null) {
                      result.add(component);
                  } else {
                      result.addAll(queryPackageForServices(packageOrComponent, userId));
                  }
              }
          }
          return result;
      }
  
      protected Set<ComponentName> queryPackageForServices(String packageName, int userId) {
          return queryPackageForServices(packageName, 0, userId);
      }
  
      protected Set<ComponentName> queryPackageForServices(String packageName, int extraFlags,
              int userId) {
          Set<ComponentName> installed = new ArraySet<>();
          final PackageManager pm = mContext.getPackageManager();
          Intent queryIntent = new Intent(mConfig.serviceInterface);
          if (!TextUtils.isEmpty(packageName)) {
              queryIntent.setPackage(packageName);
          }
          List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
                  queryIntent,
                  PackageManager.GET_SERVICES | PackageManager.GET_META_DATA | extraFlags,
                  userId);
          if (DEBUG)
              Slog.v(TAG, mConfig.serviceInterface + " services: " + installedServices);
          if (installedServices != null) {
              for (int i = 0, count = installedServices.size(); i < count; i++) {
                  ResolveInfo resolveInfo = installedServices.get(i);
                  ServiceInfo info = resolveInfo.serviceInfo;
  
                  ComponentName component = new ComponentName(info.packageName, info.name);
                  if (!mConfig.bindPermission.equals(info.permission)) {
                      Slog.w(TAG, "Skipping " + getCaption() + " service "
                          + info.packageName + "/" + info.name
                          + ": it does not require the permission "
                          + mConfig.bindPermission);
                      continue;
                  }
                  installed.add(component);
              }
          }
          return installed;
      }
  
      protected Set<String> getAllowedPackages() {
          final Set<String> allowedPackages = new ArraySet<>();
          synchronized (mApproved) {
              for (int k = 0; k < mApproved.size(); k++) {
                  ArrayMap<Boolean, ArraySet<String>> allowedByType = mApproved.valueAt(k);
                  for (int i = 0; i < allowedByType.size(); i++) {
                      final ArraySet<String> allowed = allowedByType.valueAt(i);
                      for (int j = 0; j < allowed.size(); j++) {
                          String pkgName = getPackageName(allowed.valueAt(j));
                          if (!TextUtils.isEmpty(pkgName)) {
                              allowedPackages.add(pkgName);
                          }
                      }
                  }
              }
          }
          return allowedPackages;
      }
  
      private void trimApprovedListsAccordingToInstalledServices(int userId) {
          synchronized (mApproved) {
              final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId);
              if (approvedByType == null) {
                  return;
              }
              for (int i = 0; i < approvedByType.size(); i++) {
                  final ArraySet<String> approved = approvedByType.valueAt(i);
                  for (int j = approved.size() - 1; j >= 0; j--) {
                      final String approvedPackageOrComponent = approved.valueAt(j);
                      if (!isValidEntry(approvedPackageOrComponent, userId)) {
                          approved.removeAt(j);
                          Slog.v(TAG, "Removing " + approvedPackageOrComponent
                                  + " from approved list; no matching services found");
                      } else {
                          if (DEBUG) {
                              Slog.v(TAG, "Keeping " + approvedPackageOrComponent
                                      + " on approved list; matching services found");
                          }
                      }
                  }
              }
          }
      }
  
      private boolean removeUninstalledItemsFromApprovedLists(int uninstalledUserId, String pkg) {
          boolean removed = false;
          synchronized (mApproved) {
              final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(
                      uninstalledUserId);
              if (approvedByType != null) {
                  int M = approvedByType.size();
                  for (int j = 0; j < M; j++) {
                      final ArraySet<String> approved = approvedByType.valueAt(j);
                      int O = approved.size();
                      for (int k = O - 1; k >= 0; k--) {
                          final String packageOrComponent = approved.valueAt(k);
                          final String packageName = getPackageName(packageOrComponent);
                          if (TextUtils.equals(pkg, packageName)) {
                              approved.removeAt(k);
                              if (DEBUG) {
                                  Slog.v(TAG, "Removing " + packageOrComponent
                                          + " from approved list; uninstalled");
                              }
                          }
                      }
                  }
              }
          }
          return removed;
      }
  
      protected String getPackageName(String packageOrComponent) {
          final ComponentName component = ComponentName.unflattenFromString(packageOrComponent);
          if (component != null) {
              return component.getPackageName();
          } else {
              return packageOrComponent;
          }
      }
  
      protected boolean isValidEntry(String packageOrComponent, int userId) {
          return hasMatchingServices(packageOrComponent, userId);
      }
  
      private boolean hasMatchingServices(String packageOrComponent, int userId) {
          if (!TextUtils.isEmpty(packageOrComponent)) {
              final String packageName = getPackageName(packageOrComponent);
              return queryPackageForServices(packageName, userId).size() > 0;
          }
          return false;
      }
  
      @VisibleForTesting
      protected SparseArray<ArraySet<ComponentName>> getAllowedComponents(IntArray userIds) {
          final int nUserIds = userIds.size();
          final SparseArray<ArraySet<ComponentName>> componentsByUser = new SparseArray<>();
  
          for (int i = 0; i < nUserIds; ++i) {
              final int userId = userIds.get(i);
              synchronized (mApproved) {
                  final ArrayMap<Boolean, ArraySet<String>> approvedLists = mApproved.get(userId);
                  if (approvedLists != null) {
                      final int N = approvedLists.size();
                      for (int j = 0; j < N; j++) {
                          ArraySet<ComponentName> approvedByUser = componentsByUser.get(userId);
                          if (approvedByUser == null) {
                              approvedByUser = new ArraySet<>();
                              componentsByUser.put(userId, approvedByUser);
                          }
                          approvedByUser.addAll(
                                  loadComponentNamesFromValues(approvedLists.valueAt(j), userId));
                      }
                  }
              }
          }
          return componentsByUser;
      }
  
      @GuardedBy("mMutex")
      protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind,
              final IntArray activeUsers,
              SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) {
          mEnabledServicesForCurrentProfiles.clear();
          mEnabledServicesPackageNames.clear();
          final int nUserIds = activeUsers.size();
  
          for (int i = 0; i < nUserIds; ++i) {
              // decode the list of components
              final int userId = activeUsers.get(i);
              final ArraySet<ComponentName> userComponents = approvedComponentsByUser.get(userId);
              if (null == userComponents) {
                  componentsToBind.put(userId, new ArraySet<>());
                  continue;
              }
  
              final Set<ComponentName> add = new HashSet<>(userComponents);
              add.removeAll(mSnoozingForCurrentProfiles);
  
              componentsToBind.put(userId, add);
  
              mEnabledServicesForCurrentProfiles.addAll(userComponents);
  
              for (int j = 0; j < userComponents.size(); j++) {
                  final ComponentName component = userComponents.valueAt(j);
                  mEnabledServicesPackageNames.add(component.getPackageName());
              }
          }
      }
  
      @GuardedBy("mMutex")
      protected Set<ManagedServiceInfo> getRemovableConnectedServices() {
          final Set<ManagedServiceInfo> removableBoundServices = new ArraySet<>();
          for (ManagedServiceInfo service : mServices) {
              if (!service.isSystem && !service.isGuest(this)) {
                  removableBoundServices.add(service);
              }
          }
          return removableBoundServices;
      }
  
      protected void populateComponentsToUnbind(
              boolean forceRebind,
              Set<ManagedServiceInfo> removableBoundServices,
              SparseArray<Set<ComponentName>> allowedComponentsToBind,
              SparseArray<Set<ComponentName>> componentsToUnbind) {
          for (ManagedServiceInfo info : removableBoundServices) {
              final Set<ComponentName> allowedComponents = allowedComponentsToBind.get(info.userid);
              if (allowedComponents != null) {
                  if (forceRebind || !allowedComponents.contains(info.component)) {
                      Set<ComponentName> toUnbind =
                              componentsToUnbind.get(info.userid, new ArraySet<>());
                      toUnbind.add(info.component);
                      componentsToUnbind.put(info.userid, toUnbind);
                  }
              }
          }
      }
  
      /**
       * Called whenever packages change, the user switches, or the secure setting
       * is altered. (For example in response to USER_SWITCHED in our broadcast receiver)
       */
      protected void rebindServices(boolean forceRebind, int userToRebind) {
          if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind);
          IntArray userIds = mUserProfiles.getCurrentProfileIds();
          if (userToRebind != USER_ALL) {
              userIds = new IntArray(1);
              userIds.add(userToRebind);
          }
  
          final SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
          final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
  
          synchronized (mMutex) {
              final SparseArray<ArraySet<ComponentName>> approvedComponentsByUser =
                      getAllowedComponents(userIds);
              final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices();
  
              // Filter approvedComponentsByUser to collect all of the components that are allowed
              // for the currently active user(s).
              populateComponentsToBind(componentsToBind, userIds, approvedComponentsByUser);
  
              // For every current non-system connection, disconnect services that are no longer
              // approved, or ALL services if we are force rebinding
              populateComponentsToUnbind(
                      forceRebind, removableBoundServices, componentsToBind, componentsToUnbind);
          }
  
          unbindFromServices(componentsToUnbind);
          bindToServices(componentsToBind);
      }
  
      protected void unbindFromServices(SparseArray<Set<ComponentName>> componentsToUnbind) {
          for (int i = 0; i < componentsToUnbind.size(); i++) {
              final int userId = componentsToUnbind.keyAt(i);
              final Set<ComponentName> removableComponents = componentsToUnbind.get(userId);
              for (ComponentName cn : removableComponents) {
                  // No longer allowed to be bound, or must rebind.
                  Slog.v(TAG, "disabling " + getCaption() + " for user " + userId + ": " + cn);
                  unregisterService(cn, userId);
              }
          }
      }
  
      // Attempt to bind to services, skipping those that cannot be found or lack the permission.
      private void bindToServices(SparseArray<Set<ComponentName>> componentsToBind) {
          for (int i = 0; i < componentsToBind.size(); i++) {
              final int userId = componentsToBind.keyAt(i);
              final Set<ComponentName> add = componentsToBind.get(userId);
              for (ComponentName component : add) {
                  try {
                      ServiceInfo info = mPm.getServiceInfo(component,
                              PackageManager.MATCH_DIRECT_BOOT_AWARE
                                      | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
                      if (info == null) {
                          Slog.w(TAG, "Not binding " + getCaption() + " service " + component
                                  + ": service not found");
                          continue;
                      }
                      if (!mConfig.bindPermission.equals(info.permission)) {
                          Slog.w(TAG, "Not binding " + getCaption() + " service " + component
                                  + ": it does not require the permission " + mConfig.bindPermission);
                          continue;
                      }
                      Slog.v(TAG,
                              "enabling " + getCaption() + " for " + userId + ": " + component);
                      registerService(component, userId);
                  } catch (RemoteException e) {
                      e.rethrowFromSystemServer();
                  }
              }
          }
      }
  
      /**
       * Version of registerService that takes the name of a service component to bind to.
       */
      private void registerService(final ComponentName name, final int userid) {
          synchronized (mMutex) {
              registerServiceLocked(name, userid);
          }
      }
  
      /**
       * Inject a system service into the management list.
       */
      public void registerSystemService(final ComponentName name, final int userid) {
          synchronized (mMutex) {
              registerServiceLocked(name, userid, true /* isSystem */);
          }
      }
  
      private void registerServiceLocked(final ComponentName name, final int userid) {
          registerServiceLocked(name, userid, false /* isSystem */);
      }
  
      private void registerServiceLocked(final ComponentName name, final int userid,
              final boolean isSystem) {
          if (DEBUG) Slog.v(TAG, "registerService: " + name + " u=" + userid);
  
          final Pair<ComponentName, Integer> servicesBindingTag = Pair.create(name, userid);
          if (mServicesBound.contains(servicesBindingTag)) {
              Slog.v(TAG, "Not registering " + name + " is already bound");
              // stop registering this thing already! we're working on it
              return;
          }
          mServicesBound.add(servicesBindingTag);
  
          final int N = mServices.size();
          for (int i = N - 1; i >= 0; i--) {
              final ManagedServiceInfo info = mServices.get(i);
              if (name.equals(info.component)
                  && info.userid == userid) {
                  // cut old connections
                  Slog.v(TAG, "    disconnecting old " + getCaption() + ": " + info.service);
                  removeServiceLocked(i);
                  if (info.connection != null) {
                      unbindService(info.connection, info.component, info.userid);
                  }
              }
          }
  
          Intent intent = new Intent(mConfig.serviceInterface);
          intent.setComponent(name);
  
          intent.putExtra(Intent.EXTRA_CLIENT_LABEL, mConfig.clientLabel);
  
          final PendingIntent pendingIntent = PendingIntent.getActivity(
              mContext, 0, new Intent(mConfig.settingsAction), 0);
          intent.putExtra(Intent.EXTRA_CLIENT_INTENT, pendingIntent);
  
          ApplicationInfo appInfo = null;
          try {
              appInfo = mContext.getPackageManager().getApplicationInfo(
                  name.getPackageName(), 0);
          } catch (NameNotFoundException e) {
              // Ignore if the package doesn't exist we won't be able to bind to the service.
          }
          final int targetSdkVersion =
              appInfo != null ? appInfo.targetSdkVersion : Build.VERSION_CODES.BASE;
  
          try {
              Slog.v(TAG, "binding: " + intent);
              ServiceConnection serviceConnection = new ServiceConnection() {
                  IInterface mService;
  
                  @Override
                  public void onServiceConnected(ComponentName name, IBinder binder) {
                      Slog.v(TAG,  userid + " " + getCaption() + " service connected: " + name);
                      boolean added = false;
                      ManagedServiceInfo info = null;
                      synchronized (mMutex) {
                          mServicesRebinding.remove(servicesBindingTag);
                          try {
                              mService = asInterface(binder);
                              info = newServiceInfo(mService, name,
                                  userid, isSystem, this, targetSdkVersion);
                              binder.linkToDeath(info, 0);
                              added = mServices.add(info);
                          } catch (RemoteException e) {
                              Slog.e(TAG, "Failed to linkToDeath, already dead", e);
                          }
                      }
                      if (added) {
                          onServiceAdded(info);
                      }
                  }
  
                  @Override
                  public void onServiceDisconnected(ComponentName name) {
                      Slog.v(TAG, userid + " " + getCaption() + " connection lost: " + name);
                  }
  
                  @Override
                  public void onBindingDied(ComponentName name) {
                      Slog.w(TAG,  userid + " " + getCaption() + " binding died: " + name);
                      synchronized (mMutex) {
                          unbindService(this, name, userid);
                          if (!mServicesRebinding.contains(servicesBindingTag)) {
                              mServicesRebinding.add(servicesBindingTag);
                              mHandler.postDelayed(new Runnable() {
                                      @Override
                                      public void run() {
                                          registerService(name, userid);
                                      }
                                 }, ON_BINDING_DIED_REBIND_DELAY_MS);
                          } else {
                              Slog.v(TAG, getCaption() + " not rebinding in user " + userid
                                      + " as a previous rebind attempt was made: " + name);
                          }
                      }
                  }
  
                  @Override
                  public void onNullBinding(ComponentName name) {
                      Slog.v(TAG, "onNullBinding() called with: name = [" + name + "]");
                      mServicesBound.remove(servicesBindingTag);
                  }
              };
              if (!mContext.bindServiceAsUser(intent,
                      serviceConnection,
                      getBindFlags(),
                      new UserHandle(userid))) {
                  mServicesBound.remove(servicesBindingTag);
                  Slog.w(TAG, "Unable to bind " + getCaption() + " service: " + intent
                          + " in user " + userid);
                  return;
              }
          } catch (SecurityException ex) {
              mServicesBound.remove(servicesBindingTag);
              Slog.e(TAG, "Unable to bind " + getCaption() + " service: " + intent, ex);
          }
      }
  
      boolean isBound(ComponentName cn, int userId) {
          final Pair<ComponentName, Integer> servicesBindingTag = Pair.create(cn, userId);
          return mServicesBound.contains(servicesBindingTag);
      }
  
      /**
       * Remove a service for the given user by ComponentName
       */
      private void unregisterService(ComponentName name, int userid) {
          synchronized (mMutex) {
              unregisterServiceLocked(name, userid);
          }
      }
  
      private void unregisterServiceLocked(ComponentName name, int userid) {
          final int N = mServices.size();
          for (int i = N - 1; i >= 0; i--) {
              final ManagedServiceInfo info = mServices.get(i);
              if (name.equals(info.component) && info.userid == userid) {
                  removeServiceLocked(i);
                  if (info.connection != null) {
                      unbindService(info.connection, info.component, info.userid);
                  }
              }
          }
      }
  
      /**
       * Removes a service from the list but does not unbind
       *
       * @return the removed service.
       */
      private ManagedServiceInfo removeServiceImpl(IInterface service, final int userid) {
          if (DEBUG) Slog.d(TAG, "removeServiceImpl service=" + service + " u=" + userid);
          ManagedServiceInfo serviceInfo = null;
          synchronized (mMutex) {
              final int N = mServices.size();
              for (int i = N - 1; i >= 0; i--) {
                  final ManagedServiceInfo info = mServices.get(i);
                  if (info.service.asBinder() == service.asBinder() && info.userid == userid) {
                      Slog.d(TAG, "Removing active service " + info.component);
                      serviceInfo = removeServiceLocked(i);
                  }
              }
          }
          return serviceInfo;
      }
  
      private ManagedServiceInfo removeServiceLocked(int i) {
          final ManagedServiceInfo info = mServices.remove(i);
          onServiceRemovedLocked(info);
          return info;
      }
  
      private void checkNotNull(IInterface service) {
          if (service == null) {
              throw new IllegalArgumentException(getCaption() + " must not be null");
          }
      }
  
      private ManagedServiceInfo registerServiceImpl(final IInterface service,
              final ComponentName component, final int userid) {
          ManagedServiceInfo info = newServiceInfo(service, component, userid,
                  true /*isSystem*/, null /*connection*/, Build.VERSION_CODES.LOLLIPOP);
          return registerServiceImpl(info);
      }
  
      private ManagedServiceInfo registerServiceImpl(ManagedServiceInfo info) {
          synchronized (mMutex) {
              try {
                  info.service.asBinder().linkToDeath(info, 0);
                  mServices.add(info);
                  return info;
              } catch (RemoteException e) {
                  // already dead
              }
          }
          return null;
      }
  
      /**
       * Removes a service from the list and unbinds.
       */
      private void unregisterServiceImpl(IInterface service, int userid) {
          ManagedServiceInfo info = removeServiceImpl(service, userid);
          if (info != null && info.connection != null && !info.isGuest(this)) {
              unbindService(info.connection, info.component, info.userid);
          }
      }
  
      private void unbindService(ServiceConnection connection, ComponentName component, int userId) {
          try {
              mContext.unbindService(connection);
          } catch (IllegalArgumentException e) {
              Slog.e(TAG, getCaption() + " " + component + " could not be unbound", e);
          }
          synchronized (mMutex) {
              mServicesBound.remove(Pair.create(component, userId));
          }
      }
  
      public class ManagedServiceInfo implements IBinder.DeathRecipient {
          public IInterface service;
          public ComponentName component;
          public int userid;
          public boolean isSystem;
          public ServiceConnection connection;
          public int targetSdkVersion;
  
          public ManagedServiceInfo(IInterface service, ComponentName component,
                  int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion) {
              this.service = service;
              this.component = component;
              this.userid = userid;
              this.isSystem = isSystem;
              this.connection = connection;
              this.targetSdkVersion = targetSdkVersion;
          }
  
          public boolean isGuest(ManagedServices host) {
              return ManagedServices.this != host;
          }
  
          public ManagedServices getOwner() {
              return ManagedServices.this;
          }
  
          @Override
          public String toString() {
              return new StringBuilder("ManagedServiceInfo[")
                      .append("component=").append(component)
                      .append(",userid=").append(userid)
                      .append(",isSystem=").append(isSystem)
                      .append(",targetSdkVersion=").append(targetSdkVersion)
                      .append(",connection=").append(connection == null ? null : "<connection>")
                      .append(",service=").append(service)
                      .append(']').toString();
          }
  
          public void writeToProto(ProtoOutputStream proto, long fieldId, ManagedServices host) {
              final long token = proto.start(fieldId);
              component.writeToProto(proto, ManagedServiceInfoProto.COMPONENT);
              proto.write(ManagedServiceInfoProto.USER_ID, userid);
              proto.write(ManagedServiceInfoProto.SERVICE, service.getClass().getName());
              proto.write(ManagedServiceInfoProto.IS_SYSTEM, isSystem);
              proto.write(ManagedServiceInfoProto.IS_GUEST, isGuest(host));
              proto.end(token);
          }
  
          public boolean isSameUser(int userId) {
              if (!isEnabledForCurrentProfiles()) {
                  return false;
              }
              return this.userid == userId;
          }
  
          public boolean enabledAndUserMatches(int nid) {
              if (!isEnabledForCurrentProfiles()) {
                  return false;
              }
              if (this.userid == USER_ALL) return true;
              if (this.isSystem) return true;
              if (nid == USER_ALL || nid == this.userid) return true;
              return supportsProfiles()
                      && mUserProfiles.isCurrentProfile(nid)
                      && isPermittedForProfile(nid);
          }
  
          public boolean supportsProfiles() {
              return targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
          }
  
          @Override
          public void binderDied() {
              if (DEBUG) Slog.d(TAG, "binderDied");
              // Remove the service, but don't unbind from the service. The system will bring the
              // service back up, and the onServiceConnected handler will read the service with the
              // new binding. If this isn't a bound service, and is just a registered
              // service, just removing it from the list is all we need to do anyway.
              removeServiceImpl(this.service, this.userid);
          }
  
          /** convenience method for looking in mEnabledServicesForCurrentProfiles */
          public boolean isEnabledForCurrentProfiles() {
              if (this.isSystem) return true;
              if (this.connection == null) return false;
              return mEnabledServicesForCurrentProfiles.contains(this.component);
          }
  
          /**
           * Returns true if this service is allowed to receive events for the given userId. A
           * managed profile owner can disallow non-system services running outside of the profile
           * from receiving events from the profile.
           */
          public boolean isPermittedForProfile(int userId) {
              if (!mUserProfiles.isManagedProfile(userId)) {
                  return true;
              }
              DevicePolicyManager dpm =
                      (DevicePolicyManager) mContext.getSystemService(DEVICE_POLICY_SERVICE);
              final long identity = Binder.clearCallingIdentity();
              try {
                  return dpm.isNotificationListenerServicePermitted(
                          component.getPackageName(), userId);
              } finally {
                  Binder.restoreCallingIdentity(identity);
              }
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              ManagedServiceInfo that = (ManagedServiceInfo) o;
              return userid == that.userid
                      && isSystem == that.isSystem
                      && targetSdkVersion == that.targetSdkVersion
                      && Objects.equals(service, that.service)
                      && Objects.equals(component, that.component)
                      && Objects.equals(connection, that.connection);
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(service, component, userid, isSystem, connection, targetSdkVersion);
          }
      }
  
      /** convenience method for looking in mEnabledServicesForCurrentProfiles */
      public boolean isComponentEnabledForCurrentProfiles(ComponentName component) {
          return mEnabledServicesForCurrentProfiles.contains(component);
      }
  
      public static class UserProfiles {
          // Profiles of the current user.
          private final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
  
          public void updateCache(@NonNull Context context) {
              UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
              if (userManager != null) {
                  int currentUserId = ActivityManager.getCurrentUser();
                  List<UserInfo> profiles = userManager.getProfiles(currentUserId);
                  synchronized (mCurrentProfiles) {
                      mCurrentProfiles.clear();
                      for (UserInfo user : profiles) {
                          mCurrentProfiles.put(user.id, user);
                      }
                  }
              }
          }
  
          /**
           * Returns the currently active users (generally one user and its work profile).
           */
          public IntArray getCurrentProfileIds() {
              synchronized (mCurrentProfiles) {
                  IntArray users = new IntArray(mCurrentProfiles.size());
                  final int N = mCurrentProfiles.size();
                  for (int i = 0; i < N; ++i) {
                      users.add(mCurrentProfiles.keyAt(i));
                  }
                  return users;
              }
          }
  
          public boolean isCurrentProfile(int userId) {
              synchronized (mCurrentProfiles) {
                  return mCurrentProfiles.get(userId) != null;
              }
          }
  
          public boolean isManagedProfile(int userId) {
              synchronized (mCurrentProfiles) {
                  UserInfo user = mCurrentProfiles.get(userId);
                  return user != null && user.isManagedProfile();
              }
          }
      }
  
      public static class Config {
          public String caption;
          public String serviceInterface;
          public String secureSettingName;
          public String secondarySettingName;
          public String xmlTag;
          public String bindPermission;
          public String settingsAction;
          public int clientLabel;
      }
  }
  