/** * 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 android.app.ActivityManager; import android.app.PendingIntent; 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.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import com.android.server.notification.NotificationManagerService.DumpFilter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; 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 String ENABLED_SERVICES_SEPARATOR = ":"; protected final Context mContext; protected final Object mMutex; private final UserProfiles mUserProfiles; private final SettingsObserver mSettingsObserver; private final Config mConfig; // contains connections to all connected services, including app services // and system services protected final ArrayList mServices = new ArrayList(); // things that will be put into mServices as soon as they're ready private final ArrayList mServicesBinding = new ArrayList(); // lists the component names of all enabled (and therefore connected) // app services for current profiles. private ArraySet mEnabledServicesForCurrentProfiles = new ArraySet(); // Just the packages from mEnabledServicesForCurrentProfiles private ArraySet mEnabledServicesPackageNames = new ArraySet(); // Kept to de-dupe user change events (experienced after boot, when we receive a settings and a // user change). private int[] mLastSeenProfileIds; public ManagedServices(Context context, Handler handler, Object mutex, UserProfiles userProfiles) { mContext = context; mMutex = mutex; mUserProfiles = userProfiles; mConfig = getConfig(); mSettingsObserver = new SettingsObserver(handler); } abstract protected Config getConfig(); private String getCaption() { return mConfig.caption; } abstract protected IInterface asInterface(IBinder binder); abstract protected void onServiceAdded(ManagedServiceInfo info); 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() { mSettingsObserver.observe(); } public void dump(PrintWriter pw, DumpFilter filter) { 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() + "):"); for (ManagedServiceInfo info : mServices) { if (filter != null && !filter.matches(info.component)) continue; pw.println(" " + info.component + " (user " + info.userid + "): " + info.service + (info.isSystem?" SYSTEM":"")); } } public void onPackagesChanged(boolean queryReplace, String[] pkgList) { if (DEBUG) Slog.d(TAG, "onPackagesChanged queryReplace=" + queryReplace + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList)) + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames); boolean anyServicesInvolved = false; if (pkgList != null && (pkgList.length > 0)) { for (String pkgName : pkgList) { if (mEnabledServicesPackageNames.contains(pkgName)) { anyServicesInvolved = true; } } } if (anyServicesInvolved) { // if we're not replacing a package, clean up orphaned bits if (!queryReplace) { disableNonexistentServices(); } // make sure we're still bound to any of our services who may have just upgraded rebindServices(); } } public void onUserSwitched() { if (DEBUG) Slog.d(TAG, "onUserSwitched"); if (Arrays.equals(mLastSeenProfileIds, mUserProfiles.getCurrentProfileIds())) { if (DEBUG) Slog.d(TAG, "Current profile IDs didn't change, skipping rebindServices()."); return; } rebindServices(); } public ManagedServiceInfo checkServiceTokenLocked(IInterface service) { checkNotNull(service); final IBinder token = service.asBinder(); final int N = mServices.size(); for (int i=0; i installedServices = pm.queryIntentServicesAsUser( new Intent(mConfig.serviceInterface), PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId); if (DEBUG) Slog.v(TAG, mConfig.serviceInterface + " services: " + installedServices); Set installed = new ArraySet(); for (int i = 0, count = installedServices.size(); i < count; i++) { ResolveInfo resolveInfo = installedServices.get(i); ServiceInfo info = resolveInfo.serviceInfo; 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(new ComponentName(info.packageName, info.name)); } String flatOut = ""; if (!installed.isEmpty()) { String[] enabled = flatIn.split(ENABLED_SERVICES_SEPARATOR); ArrayList remaining = new ArrayList(enabled.length); for (int i = 0; i < enabled.length; i++) { ComponentName enabledComponent = ComponentName.unflattenFromString(enabled[i]); if (installed.contains(enabledComponent)) { remaining.add(enabled[i]); } } flatOut = TextUtils.join(ENABLED_SERVICES_SEPARATOR, remaining); } if (DEBUG) Slog.v(TAG, "flat after: " + flatOut); if (!flatIn.equals(flatOut)) { Settings.Secure.putStringForUser(mContext.getContentResolver(), mConfig.secureSettingName, flatOut, userId); } } } /** * Called whenever packages change, the user switches, or the secure setting * is altered. (For example in response to USER_SWITCHED in our broadcast receiver) */ private void rebindServices() { if (DEBUG) Slog.d(TAG, "rebindServices"); final int[] userIds = mUserProfiles.getCurrentProfileIds(); final int nUserIds = userIds.length; final SparseArray flat = new SparseArray(); for (int i = 0; i < nUserIds; ++i) { flat.put(userIds[i], Settings.Secure.getStringForUser( mContext.getContentResolver(), mConfig.secureSettingName, userIds[i])); } ArrayList toRemove = new ArrayList(); final SparseArray> toAdd = new SparseArray>(); synchronized (mMutex) { // Unbind automatically bound services, retain system services. for (ManagedServiceInfo service : mServices) { if (!service.isSystem) { toRemove.add(service); } } final ArraySet newEnabled = new ArraySet(); final ArraySet newPackages = new ArraySet(); for (int i = 0; i < nUserIds; ++i) { final ArrayList add = new ArrayList(); toAdd.put(userIds[i], add); // decode the list of components String toDecode = flat.get(userIds[i]); if (toDecode != null) { String[] components = toDecode.split(ENABLED_SERVICES_SEPARATOR); for (int j = 0; j < components.length; j++) { final ComponentName component = ComponentName.unflattenFromString(components[j]); if (component != null) { newEnabled.add(component); add.add(component); newPackages.add(component.getPackageName()); } } } } mEnabledServicesForCurrentProfiles = newEnabled; mEnabledServicesPackageNames = newPackages; } for (ManagedServiceInfo info : toRemove) { final ComponentName component = info.component; final int oldUser = info.userid; Slog.v(TAG, "disabling " + getCaption() + " for user " + oldUser + ": " + component); unregisterService(component, info.userid); } for (int i = 0; i < nUserIds; ++i) { final ArrayList add = toAdd.get(userIds[i]); final int N = add.size(); for (int j = 0; j < N; j++) { final ComponentName component = add.get(j); Slog.v(TAG, "enabling " + getCaption() + " for user " + userIds[i] + ": " + component); registerService(component, userIds[i]); } } mLastSeenProfileIds = mUserProfiles.getCurrentProfileIds(); } /** * Version of registerService that takes the name of a service component to bind to. */ private void registerService(final ComponentName name, final int userid) { if (DEBUG) Slog.v(TAG, "registerService: " + name + " u=" + userid); synchronized (mMutex) { final String servicesBindingTag = name.toString() + "/" + userid; if (mServicesBinding.contains(servicesBindingTag)) { // stop registering this thing already! we're working on it return; } mServicesBinding.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 if (DEBUG) Slog.v(TAG, " disconnecting old " + getCaption() + ": " + info.service); removeServiceLocked(i); if (info.connection != null) { mContext.unbindService(info.connection); } } } 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 { if (DEBUG) Slog.v(TAG, "binding: " + intent); if (!mContext.bindServiceAsUser(intent, new ServiceConnection() { IInterface mService; @Override public void onServiceConnected(ComponentName name, IBinder binder) { boolean added = false; ManagedServiceInfo info = null; synchronized (mMutex) { mServicesBinding.remove(servicesBindingTag); try { mService = asInterface(binder); info = newServiceInfo(mService, name, userid, false /*isSystem*/, this, targetSdkVersion); binder.linkToDeath(info, 0); added = mServices.add(info); } catch (RemoteException e) { // already dead } } if (added) { onServiceAdded(info); } } @Override public void onServiceDisconnected(ComponentName name) { Slog.v(TAG, getCaption() + " connection lost: " + name); } }, Context.BIND_AUTO_CREATE, new UserHandle(userid))) { mServicesBinding.remove(servicesBindingTag); Slog.w(TAG, "Unable to bind " + getCaption() + " service: " + intent); return; } } catch (SecurityException ex) { Slog.e(TAG, "Unable to bind " + getCaption() + " service: " + intent, ex); return; } } } /** * Remove a service for the given user by ComponentName */ private void unregisterService(ComponentName name, int userid) { synchronized (mMutex) { 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) { try { mContext.unbindService(info.connection); } catch (IllegalArgumentException ex) { // something happened to the service: we think we have a connection // but it's bogus. Slog.e(TAG, getCaption() + " " + name + " could not be unbound: " + ex); } } } } } } /** * 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) { if (DEBUG) 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) { synchronized (mMutex) { try { ManagedServiceInfo info = newServiceInfo(service, component, userid, true /*isSystem*/, null, Build.VERSION_CODES.LOLLIPOP); 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) { mContext.unbindService(info.connection); } } private class SettingsObserver extends ContentObserver { private final Uri mSecureSettingsUri = Settings.Secure.getUriFor(mConfig.secureSettingName); private SettingsObserver(Handler handler) { super(handler); } private void observe() { ContentResolver resolver = mContext.getContentResolver(); resolver.registerContentObserver(mSecureSettingsUri, false, this, UserHandle.USER_ALL); update(null); } @Override public void onChange(boolean selfChange, Uri uri) { update(uri); } private void update(Uri uri) { if (uri == null || mSecureSettingsUri.equals(uri)) { if (DEBUG) Slog.d(TAG, "Setting changed: mSecureSettingsUri=" + mSecureSettingsUri + " / uri=" + uri); rebindServices(); } } } 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; } @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 : "") .append(",service=").append(service) .append(']').toString(); } public boolean enabledAndUserMatches(int nid) { if (!isEnabledForCurrentProfiles()) { return false; } if (this.userid == UserHandle.USER_ALL) return true; if (nid == UserHandle.USER_ALL || nid == this.userid) return true; return supportsProfiles() && mUserProfiles.isCurrentProfile(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 readd 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); } } public static class UserProfiles { // Profiles of the current user. private final SparseArray mCurrentProfiles = new SparseArray(); public void updateCache(Context context) { UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); if (userManager != null) { int currentUserId = ActivityManager.getCurrentUser(); List profiles = userManager.getProfiles(currentUserId); synchronized (mCurrentProfiles) { mCurrentProfiles.clear(); for (UserInfo user : profiles) { mCurrentProfiles.put(user.id, user); } } } } public int[] getCurrentProfileIds() { synchronized (mCurrentProfiles) { int[] users = new int[mCurrentProfiles.size()]; final int N = mCurrentProfiles.size(); for (int i = 0; i < N; ++i) { users[i] = mCurrentProfiles.keyAt(i); } return users; } } public boolean isCurrentProfile(int userId) { synchronized (mCurrentProfiles) { return mCurrentProfiles.get(userId) != null; } } } protected static class Config { String caption; String serviceInterface; String secureSettingName; String bindPermission; String settingsAction; int clientLabel; } }