From a7d6fc579f016e0bcd67597ce4870100b0f62ba2 Mon Sep 17 00:00:00 2001 From: Adnan Begovic Date: Thu, 30 Apr 2015 11:10:56 -0700 Subject: CMSDK: Refactor compile, prepare for integration tests. Also don't build CMPlatformTests unless explicitely called. Change-Id: I3fd8f884d8815eab9987077766c0ff2fe3f98b4d --- .../platform/internal/ManagedServices.java | 634 +++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 cm/lib/main/java/org/cyanogenmod/platform/internal/ManagedServices.java (limited to 'cm/lib/main/java/org/cyanogenmod/platform/internal/ManagedServices.java') diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/ManagedServices.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/ManagedServices.java new file mode 100644 index 0000000..598d90c --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/ManagedServices.java @@ -0,0 +1,634 @@ +/** + * 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 org.cyanogenmod.platform.internal; + +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.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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import com.android.internal.R; + +/** + * 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 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 (PackageManager.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 { + public String caption; + public String serviceInterface; + public String secureSettingName; + public String bindPermission; + public String settingsAction; + public int clientLabel; + } +} \ No newline at end of file -- cgit v1.1