diff options
author | Adnan Begovic <adnan@cyngn.com> | 2015-04-23 23:16:27 -0700 |
---|---|---|
committer | Adnan Begovic <adnan@cyngn.com> | 2015-04-26 16:55:08 -0700 |
commit | aa8614e39b90c0d9cc2d86777d28c691773d9dae (patch) | |
tree | a6b998d9637dfc722c1dc16dfe66560673d675fb /cm/lib | |
parent | 42e54529ed9eec71d40c1bc6b8c2409766a95f64 (diff) | |
download | vendor_cmsdk-aa8614e39b90c0d9cc2d86777d28c691773d9dae.zip vendor_cmsdk-aa8614e39b90c0d9cc2d86777d28c691773d9dae.tar.gz vendor_cmsdk-aa8614e39b90c0d9cc2d86777d28c691773d9dae.tar.bz2 |
CMSDK: Create Quick Settings Tile API.
Create a simple CustomTile object with builder which lets a 3rd party
application publish a quick settings tile to the status bar panel.
An example CustomTile build:
CustomTile customTile = new CustomTile.Builder(mContext)
.setLabel("custom label")
.setContentDescription("custom description")
.setOnClickIntent(pendingIntent)
.setOnClickUri(Uri.parse("custom uri"))
.setIcon(R.drawable.ic_launcher)
.build();
Which can be published to the status bar panel via CMStatusBarManager#publishTile.
The CustomTile contains a click intent and click uri which can be
sent or broadcasted when the CustomQSTile's handleClick is fired.
This implementation closely mirrors that of NotificationManager#notify for
notifications. In that each CMStatusBarManager#publishTile can have an appended
id which can be kept by the 3rd party application to either update the tile with,
or to remove the tile via CMStatusBarManager#removeTile.
Change-Id: I4b8a50e4e53ef2ececc9c7fc9c8d0ec6acfd0c0e
Diffstat (limited to 'cm/lib')
-rw-r--r-- | cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java | 477 | ||||
-rw-r--r-- | cm/lib/java/org/cyanogenmod/platform/internal/ManagedServices.java | 634 |
2 files changed, 1111 insertions, 0 deletions
diff --git a/cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java b/cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java new file mode 100644 index 0000000..88472a1 --- /dev/null +++ b/cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java @@ -0,0 +1,477 @@ +/** + * Copyright (c) 2015, The CyanogenMod 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.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IInterface; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; + +import com.android.server.SystemService; +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.CustomTile; +import cyanogenmod.app.CustomTileListenerService; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.app.ICustomTileListener; +import cyanogenmod.app.ICMStatusBarManager; + +import org.cyanogenmod.internal.statusbar.ExternalQuickSettingsRecord; +import org.cyanogenmod.internal.statusbar.IStatusBarCustomTileHolder; + +import java.util.ArrayList; + +import com.android.internal.R; + +/** + * Internal service which manages interactions with system ui elements + * @hide + */ +public class CMStatusBarManagerService extends SystemService { + private static final String TAG = "CMStatusBarManagerService"; + + private Handler mHandler = new Handler(); + private CustomTileListeners mCustomTileListeners; + + static final int MAX_PACKAGE_TILES = 4; + + private final ManagedServices.UserProfiles mUserProfiles = new ManagedServices.UserProfiles(); + + final ArrayList<ExternalQuickSettingsRecord> mQSTileList = + new ArrayList<ExternalQuickSettingsRecord>(); + final ArrayMap<String, ExternalQuickSettingsRecord> mCustomTileByKey = + new ArrayMap<String, ExternalQuickSettingsRecord>(); + + public CMStatusBarManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + Log.d(TAG, "registerCMStatusBar cmstatusbar: " + this); + mCustomTileListeners = new CustomTileListeners(); + publishBinderService(CMContextConstants.CM_STATUS_BAR_SERVICE, mService); + } + + private final IBinder mService = new ICMStatusBarManager.Stub() { + /** + * @hide + */ + @Override + public void createCustomTileWithTag(String pkg, String opPkg, String tag, int id, + CustomTile customTile, int[] idOut, int userId) throws RemoteException { + enforceCustomTilePublish(); + createCustomTileWithTagInternal(pkg, opPkg, Binder.getCallingUid(), + Binder.getCallingPid(), tag, id, customTile, idOut, userId); + } + + /** + * @hide + */ + @Override + public void removeCustomTileWithTag(String pkg, String tag, int id, int userId) { + checkCallerIsSystemOrSameApp(pkg); + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), userId, true, false, "cancelCustomTileWithTag", pkg); + removeCustomTileWithTagInternal(Binder.getCallingUid(), + Binder.getCallingPid(), pkg, tag, id, userId); + } + + /** + * Register a listener binder directly with the status bar manager. + * + * Only works with system callers. Apps should extend + * {@link cyanogenmod.app.CustomTileListenerService}. + * @hide + */ + @Override + public void registerListener(final ICustomTileListener listener, + final ComponentName component, final int userid) { + enforceBindCustomTileListener(); + mCustomTileListeners.registerService(listener, component, userid); + } + + /** + * Remove a listener binder directly + * @hide + */ + @Override + public void unregisterListener(ICustomTileListener listener, int userid) { + enforceBindCustomTileListener(); + mCustomTileListeners.unregisterService(listener, userid); + } + }; + + void createCustomTileWithTagInternal(final String pkg, final String opPkg, final int callingUid, + final int callingPid, final String tag, final int id, final CustomTile customTile, + final int[] idOut, final int incomingUserId) { + + if (pkg == null || customTile == null) { + throw new IllegalArgumentException("null not allowed: pkg=" + pkg + + " id=" + id + " customTile=" + customTile); + } + + final int userId = ActivityManager.handleIncomingUser(callingPid, + callingUid, incomingUserId, true, false, "createCustomTileWithTag", pkg); + final UserHandle user = new UserHandle(userId); + + // remove custom tile call ends up in not removing the custom tile. + mHandler.post(new Runnable() { + @Override + public void run() { + final StatusBarPanelCustomTile sbc = new StatusBarPanelCustomTile( + pkg, opPkg, id, tag, callingUid, callingPid, customTile, + user); + ExternalQuickSettingsRecord r = new ExternalQuickSettingsRecord(sbc); + ExternalQuickSettingsRecord old = mCustomTileByKey.get(sbc.getKey()); + + int index = indexOfQsTileLocked(sbc.getKey()); + if (index < 0) { + // If this tile unknown to us, check DOS protection + if (checkDosProtection(pkg, callingUid, userId)) return; + mQSTileList.add(r); + } else { + old = mQSTileList.get(index); + mQSTileList.set(index, r); + r.isUpdate = true; + } + + mCustomTileByKey.put(sbc.getKey(), r); + + if (customTile.icon != 0) { + StatusBarPanelCustomTile oldSbn = (old != null) ? old.sbTile : null; + mCustomTileListeners.notifyPostedLocked(sbc, oldSbn); + } else { + Slog.e(TAG, "Not posting custom tile with icon==0: " + customTile); + if (old != null && !old.isCanceled) { + mCustomTileListeners.notifyRemovedLocked(sbc); + } + } + } + }); + idOut[0] = id; + } + + private boolean checkDosProtection(String pkg, int callingUid, int userId) { + final boolean isSystemTile = isUidSystem(callingUid) || ("android".equals(pkg)); + // Limit the number of Custom tiles that any given package except the android + // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. + if (!isSystemTile) { + synchronized (mQSTileList) { + int count = 0; + final int N = mQSTileList.size(); + + for (int i = 0; i < N; i++) { + final ExternalQuickSettingsRecord r = mQSTileList.get(i); + if (r.sbTile.getPackage().equals(pkg) && r.sbTile.getUserId() == userId) { + count++; + if (count >= MAX_PACKAGE_TILES) { + Slog.e(TAG, "Package has already posted " + count + + " custom tiles. Not showing more. package=" + pkg); + return true; + } + } + } + } + } + return false; + } + + // lock on mQSTileList + int indexOfQsTileLocked(String key) { + final int N = mQSTileList.size(); + for (int i = 0; i < N; i++) { + if (key.equals(mQSTileList.get(i).getKey())) { + return i; + } + } + return -1; + } + + // lock on mQSTileList + int indexOfQsTileLocked(String pkg, String tag, int id, int userId) { + ArrayList<ExternalQuickSettingsRecord> list = mQSTileList; + final int len = list.size(); + for (int i = 0; i < len; i++) { + ExternalQuickSettingsRecord r = list.get(i); + if (!customTileMatchesUserId(r, userId) || r.sbTile.getId() != id) { + continue; + } + if (tag == null) { + if (r.sbTile.getTag() != null) { + continue; + } + } else { + if (!tag.equals(r.sbTile.getTag())) { + continue; + } + } + if (r.sbTile.getPackage().equals(pkg)) { + return i; + } + } + return -1; + } + + private static void checkCallerIsSystemOrSameApp(String pkg) { + if (isCallerSystem()) { + return; + } + final int uid = Binder.getCallingUid(); + try { + ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo( + pkg, 0, UserHandle.getCallingUserId()); + if (ai == null) { + throw new SecurityException("Unknown package " + pkg); + } + if (!UserHandle.isSameApp(ai.uid, uid)) { + throw new SecurityException("Calling uid " + uid + " gave package" + + pkg + " which is owned by uid " + ai.uid); + } + } catch (RemoteException re) { + throw new SecurityException("Unknown package " + pkg + "\n" + re); + } + } + + private static boolean isUidSystem(int uid) { + final int appid = UserHandle.getAppId(uid); + return (appid == android.os.Process.SYSTEM_UID + || appid == android.os.Process.PHONE_UID || uid == 0); + } + + private static boolean isCallerSystem() { + return isUidSystem(Binder.getCallingUid()); + } + + /** + * Determine whether the userId applies to the custom tile in question, either because + * they match exactly, or one of them is USER_ALL (which is treated as a wildcard). + */ + private boolean customTileMatchesUserId(ExternalQuickSettingsRecord r, int userId) { + return + // looking for USER_ALL custom tile? match everything + userId == UserHandle.USER_ALL + // a custom tile sent to USER_ALL matches any query + || r.getUserId() == UserHandle.USER_ALL + // an exact user match + || r.getUserId() == userId; + } + + void removeCustomTileWithTagInternal(final int callingUid, final int callingPid, + final String pkg, final String tag, final int id, final int userId) { + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mQSTileList) { + int index = indexOfQsTileLocked(pkg, tag, id, userId); + if (index >= 0) { + ExternalQuickSettingsRecord r = mQSTileList.get(index); + mQSTileList.remove(index); + // status bar + r.isCanceled = true; + mCustomTileListeners.notifyRemovedLocked(r.sbTile); + mCustomTileByKey.remove(r.sbTile.getKey()); + } + } + } + }); + } + + private void enforceSystemOrSystemUI(String message) { + if (isCallerSystem()) return; + mContext.enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE, + message); + } + + private void enforceCustomTilePublish() { + //mContext.enforceCallingOrSelfPermission( + // android.Manifest.permission.PUBLISH_QUICK_SETTINGS_TILE, + // "StatusBarManagerService"); + } + + private void enforceBindCustomTileListener() { + //mContext.enforceCallingOrSelfPermission( + // android.Manifest.permission.BIND_CUSTOM_TILE_LISTENER_SERVICE, + // "StatusBarManagerService"); + } + + private boolean isVisibleToListener(StatusBarPanelCustomTile sbc, + ManagedServices.ManagedServiceInfo listener) { + return listener.enabledAndUserMatches(sbc.getUserId()); + } + + public class CustomTileListeners extends ManagedServices { + + private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); + + public CustomTileListeners() { + super(CMStatusBarManagerService.this.mContext, mHandler, mQSTileList, mUserProfiles); + } + + @Override + protected Config getConfig() { + Config c = new Config(); + c.caption = "custom tile listener"; + c.serviceInterface = CustomTileListenerService.SERVICE_INTERFACE; + //TODO: Implement this in the future + //c.secureSettingName = Settings.Secure.ENABLED_CUSTOM_TILE_LISTENERS; + //c.bindPermission = android.Manifest.permission.BIND_CUSTOM_TILE_LISTENER_SERVICE; + //TODO: Implement this in the future + //c.settingsAction = Settings.ACTION_CUSTOM_TILE_LISTENER_SETTINGS; + //c.clientLabel = R.string.custom_tile_listener_binding_label; + return c; + } + + @Override + protected IInterface asInterface(IBinder binder) { + return ICustomTileListener.Stub.asInterface(binder); + } + + @Override + public void onServiceAdded(ManagedServiceInfo info) { + final ICustomTileListener listener = (ICustomTileListener) info.service; + try { + listener.onListenerConnected(); + } catch (RemoteException e) { + // we tried + } + } + + @Override + protected void onServiceRemovedLocked(ManagedServiceInfo removed) { + mLightTrimListeners.remove(removed); + } + + + /** + * asynchronously notify all listeners about a new custom tile + * + * <p> + * Also takes care of removing a custom tile that has been visible to a listener before, + * but isn't anymore. + */ + public void notifyPostedLocked(StatusBarPanelCustomTile sbc, + StatusBarPanelCustomTile oldSbc) { + // Lazily initialized snapshots of the custom tile. + StatusBarPanelCustomTile sbcClone = null; + + for (final ManagedServiceInfo info : mServices) { + boolean sbnVisible = isVisibleToListener(sbc, info); + boolean oldSbnVisible = oldSbc != null ? isVisibleToListener(oldSbc, info) : false; + // This custom tile hasn't been and still isn't visible -> ignore. + if (!oldSbnVisible && !sbnVisible) { + continue; + } + + // This custom tile became invisible -> remove the old one. + if (oldSbnVisible && !sbnVisible) { + final StatusBarPanelCustomTile oldSbcClone = oldSbc.clone(); + mHandler.post(new Runnable() { + @Override + public void run() { + notifyRemoved(info, oldSbcClone); + } + }); + continue; + } + sbcClone = sbc.clone(); + + final StatusBarPanelCustomTile sbcToPost = sbcClone; + mHandler.post(new Runnable() { + @Override + public void run() { + notifyPosted(info, sbcToPost); + } + }); + } + } + + /** + * asynchronously notify all listeners about a removed custom tile + */ + public void notifyRemovedLocked(StatusBarPanelCustomTile sbc) { + // make a copy in case changes are made to the underlying CustomTile object + final StatusBarPanelCustomTile sbcClone = sbc.clone(); + for (final ManagedServiceInfo info : mServices) { + if (!isVisibleToListener(sbcClone, info)) { + continue; + } + mHandler.post(new Runnable() { + @Override + public void run() { + notifyRemoved(info, sbcClone); + } + }); + } + } + + private void notifyPosted(final ManagedServiceInfo info, + final StatusBarPanelCustomTile sbc) { + final ICustomTileListener listener = (ICustomTileListener)info.service; + StatusBarCustomTileHolder sbcHolder = new StatusBarCustomTileHolder(sbc); + try { + listener.onCustomTilePosted(sbcHolder); + } catch (RemoteException ex) { + Log.e(TAG, "unable to notify listener (posted): " + listener, ex); + } + } + + private void notifyRemoved(ManagedServiceInfo info, StatusBarPanelCustomTile sbc) { + if (!info.enabledAndUserMatches(sbc.getUserId())) { + return; + } + final ICustomTileListener listener = (ICustomTileListener) info.service; + StatusBarCustomTileHolder sbcHolder = new StatusBarCustomTileHolder(sbc); + try { + listener.onCustomTileRemoved(sbcHolder); + } catch (RemoteException ex) { + Log.e(TAG, "unable to notify listener (removed): " + listener, ex); + } + } + } + + /** + * Wrapper for a StatusBarPanelCustomTile object that allows transfer across a oneway + * binder without sending large amounts of data over a oneway transaction. + */ + private static final class StatusBarCustomTileHolder + extends IStatusBarCustomTileHolder.Stub { + private StatusBarPanelCustomTile mValue; + + public StatusBarCustomTileHolder(StatusBarPanelCustomTile value) { + mValue = value; + } + + /** Get the held value and clear it. This function should only be called once per holder */ + @Override + public StatusBarPanelCustomTile get() { + StatusBarPanelCustomTile value = mValue; + mValue = null; + return value; + } + } +} diff --git a/cm/lib/java/org/cyanogenmod/platform/internal/ManagedServices.java b/cm/lib/java/org/cyanogenmod/platform/internal/ManagedServices.java new file mode 100644 index 0000000..598d90c --- /dev/null +++ b/cm/lib/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<ManagedServiceInfo> mServices = new ArrayList<ManagedServiceInfo>(); + // things that will be put into mServices as soon as they're ready + private final ArrayList<String> mServicesBinding = new ArrayList<String>(); + // lists the component names of all enabled (and therefore connected) + // app services for current profiles. + private ArraySet<ComponentName> mEnabledServicesForCurrentProfiles + = new ArraySet<ComponentName>(); + // Just the packages from mEnabledServicesForCurrentProfiles + private ArraySet<String> mEnabledServicesPackageNames = new ArraySet<String>(); + + // 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<N; i++) { + final ManagedServiceInfo info = mServices.get(i); + if (info.service.asBinder() == token) return info; + } + throw new SecurityException("Disallowed call from unknown " + getCaption() + ": " + + service); + } + + 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); + } + } + + /** + * Remove access for any services that no longer exist. + */ + private void disableNonexistentServices() { + int[] userIds = mUserProfiles.getCurrentProfileIds(); + final int N = userIds.length; + for (int i = 0 ; i < N; ++i) { + disableNonexistentServices(userIds[i]); + } + } + + private void disableNonexistentServices(int userId) { + String flatIn = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + mConfig.secureSettingName, + userId); + if (!TextUtils.isEmpty(flatIn)) { + if (DEBUG) Slog.v(TAG, "flat before: " + flatIn); + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> 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<ComponentName> installed = new ArraySet<ComponentName>(); + 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<String> remaining = new ArrayList<String>(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<String> flat = new SparseArray<String>(); + + for (int i = 0; i < nUserIds; ++i) { + flat.put(userIds[i], Settings.Secure.getStringForUser( + mContext.getContentResolver(), + mConfig.secureSettingName, + userIds[i])); + } + + ArrayList<ManagedServiceInfo> toRemove = new ArrayList<ManagedServiceInfo>(); + final SparseArray<ArrayList<ComponentName>> toAdd + = new SparseArray<ArrayList<ComponentName>>(); + + synchronized (mMutex) { + // Unbind automatically bound services, retain system services. + for (ManagedServiceInfo service : mServices) { + if (!service.isSystem) { + toRemove.add(service); + } + } + + final ArraySet<ComponentName> newEnabled = new ArraySet<ComponentName>(); + final ArraySet<String> newPackages = new ArraySet<String>(); + + for (int i = 0; i < nUserIds; ++i) { + final ArrayList<ComponentName> add = new ArrayList<ComponentName>(); + 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<ComponentName> 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 : "<connection>") + .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<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>(); + + public void updateCache(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); + } + } + } + } + + 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 |