diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..fd47e87 --- /dev/null +++ b/Android.mk @@ -0,0 +1,131 @@ +# 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. + +LOCAL_PATH := $(call my-dir) + +# The CyanogenMod Platform Framework Library +# ============================================================ +include $(CLEAR_VARS) + +cyanogenmod_app_src := src/java/ +library_src := cm/lib/java/org/cyanogenmod/platform/internal + +LOCAL_MODULE := org.cyanogenmod.platform +LOCAL_MODULE_TAGS := optional +LOCAL_JAVA_LIBRARIES := services +LOCAL_REQUIRED_MODULES := services + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, $(cyanogenmod_app_src)) \ + $(call all-java-files-under, $(library_src)) + +## READ ME: ######################################################## +## +## When updating this list of aidl files, consider if that aidl is +## part of the SDK API. If it is, also add it to the list below that +## is preprocessed and distributed with the SDK. This list should +## not contain any aidl files for parcelables, but the one below should +## if you intend for 3rd parties to be able to send those objects +## across process boundaries. +## +## READ ME: ######################################################## +LOCAL_SRC_FILES += \ + $(call all-Iaidl-files-under, $(cyanogemod_app_src)) + +# Include aidl files from cyanogenmod.app namespace as well as internal src aidl files +LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/src/java + +include $(BUILD_JAVA_LIBRARY) +framework_module := $(LOCAL_INSTALLED_MODULE) + +cm_framework_built := $(call java-lib-deps, org.cyanogenmod.platform) + +# ==== org.cyanogenmod.platform.xml lib def ======================== +include $(CLEAR_VARS) + +LOCAL_MODULE := org.cyanogenmod.platform.xml +LOCAL_MODULE_TAGS := optional + +LOCAL_MODULE_CLASS := ETC + +# This will install the file in /system/etc/permissions +LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions + +LOCAL_SRC_FILES := $(LOCAL_MODULE) + +include $(BUILD_PREBUILT) + +# the sdk +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_MODULE:= org.cyanogenmod.platform.sdk +LOCAL_MODULE_TAGS := optional +LOCAL_REQUIRED_MODULES := services + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, $(cyanogenmod_app_src)) \ + $(call all-Iaidl-files-under, $(cyanogenmod_app_src)) + +# Included aidl files from cyanogenmod.app namespace +LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/src/java + +$(full_target): $(cm_framework_built) $(gen) +include $(BUILD_STATIC_JAVA_LIBRARY) + +# =========================================================== +# Common Droiddoc vars +cmplat.docs.src_files := \ + $(call all-java-files-under, $(cyanogenmod_app_src)) \ + $(call all-html-files-under, $(cyanogenmod_app_src)) +cmplat.docs.java_libraries := \ + org.cyanogenmod.platform.sdk + +# Documentation +# =========================================================== +include $(CLEAR_VARS) + +LOCAL_MODULE := org.cyanogenmod.platform.sdk +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_MODULE_TAGS := optional + +intermediates.COMMON := $(call intermediates-dir-for,$(LOCAL_MODULE_CLASS), org.cyanogenmod.platform.sdk,,COMMON) + +LOCAL_SRC_FILES := $(cmplat.docs.src_files) +LOCAL_ADDITONAL_JAVA_DIR := $(intermediates.COMMON)/src + +LOCAL_SDK_VERSION := 21 +LOCAL_IS_HOST_MODULE := false +LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR := build/tools/droiddoc/templates-sdk +LOCAL_ADDITIONAL_DEPENDENCIES := \ + services + +LOCAL_JAVA_LIBRARIES := $(cmplat.docs.java_libraries) + +LOCAL_DROIDDOC_OPTIONS := \ + -offlinemode \ + -hdf android.whichdoc offline \ + -federate Android http://developer.android.com \ + -federationapi Android prebuilts/sdk/api/21.txt + +$(full_target): $(cm_framework_built) $(gen) +include $(BUILD_DROIDDOC) + +# Cleanup temp vars +# =========================================================== +cmplat.docs.src_files := +cmplat.docs.java_libraries := +intermediates.COMMON := + +include $(call all-makefiles-under, $(LOCAL_PATH)) 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 mQSTileList = + new ArrayList(); + final ArrayMap mCustomTileByKey = + new ArrayMap(); + + 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 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 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 + * + *

+ * 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 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 diff --git a/org.cyanogenmod.platform.xml b/org.cyanogenmod.platform.xml new file mode 100644 index 0000000..b85d06a --- /dev/null +++ b/org.cyanogenmod.platform.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/java/cyanogenmod/app/CMContextConstants.java b/src/java/cyanogenmod/app/CMContextConstants.java new file mode 100644 index 0000000..78c3545 --- /dev/null +++ b/src/java/cyanogenmod/app/CMContextConstants.java @@ -0,0 +1,42 @@ +/** + * 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 cyanogenmod.app; + +/** + * Constants to be used with {@link android.content.Context#getSystemService} + * to retrieve published system services + */ +public class CMContextConstants { + + /** + * @hide + */ + private CMContextConstants() { + // Empty constructor + } + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.app.CMStatusBarManager} for informing the user of + * background events. + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.app.CMStatusBarManager + */ + public static final String CM_STATUS_BAR_SERVICE = "cmstatusbar"; + +} diff --git a/src/java/cyanogenmod/app/CMStatusBarManager.java b/src/java/cyanogenmod/app/CMStatusBarManager.java new file mode 100644 index 0000000..1fe6006 --- /dev/null +++ b/src/java/cyanogenmod/app/CMStatusBarManager.java @@ -0,0 +1,215 @@ +/** + * 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 cyanogenmod.app; + +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; + +import cyanogenmod.app.ICMStatusBarManager; + +/** + * The CMStatusBarManager allows you to publish and remove CustomTiles within the + * Quick Settings Panel. + * + *

+ * Each of the publish methods takes an int id parameter and optionally a + * {@link String} tag parameter, which may be {@code null}. These parameters + * are used to form a pair (tag, id), or ({@code null}, id) if tag is + * unspecified. This pair identifies this custom tile from your app to the + * system, so that pair should be unique within your app. If you call one + * of the publish methods with a (tag, id) pair that is currently active and + * a new set of custom tile parameters, it will be updated. For example, + * if you pass a new custom tile icon, the old icon in the panel will + * be replaced with the new one. This is also the same tag and id you pass + * to the {@link #removeTile(int)} or {@link #removeTile(String, int)} method to clear + * this custom tile. + * + *

+ * To get the instance of this class, utilize CMStatusBarManager#getInstance(Context context) + * + * @see cyanogenmod.app.CustomTile + */ +public class CMStatusBarManager { + private static final String TAG = "CMStatusBarManager"; + private static boolean localLOGV = false; + + private Context mContext; + + private static ICMStatusBarManager sService; + + private static CMStatusBarManager sCMStatusBarManagerInstance; + private CMStatusBarManager(Context context) { + Context appContext = context.getApplicationContext(); + if (appContext != null) { + mContext = appContext; + } else { + mContext = context; + } + sService = getService(); + } + + /** + * Get or create an instance of the {@link cyanogenmod.app.CMStatusBarManager} + * @param context + * @return {@link cyanogenmod.app.CMStatusBarManager} + */ + public static CMStatusBarManager getInstance(Context context) { + if (sCMStatusBarManagerInstance == null) { + sCMStatusBarManagerInstance = new CMStatusBarManager(context); + } + return sCMStatusBarManagerInstance; + } + + /** + * Post a custom tile to be shown in the status bar panel. If a custom tile with + * the same id has already been posted by your application and has not yet been removed, it + * will be replaced by the updated information. + * + * @param id An identifier for this customTile unique within your + * application. + * @param customTile A {@link CustomTile} object describing what to show the user. + * Must not be null. + */ + public void publishTile(int id, CustomTile customTile) { + publishTile(null, id, customTile); + } + + /** + * Post a custom tile to be shown in the status bar panel. If a custom tile with + * the same tag and id has already been posted by your application and has not yet been + * removed, it will be replaced by the updated information. + * + * @param tag A string identifier for this custom tile. May be {@code null}. + * @param id An identifier for this custom tile. The pair (tag, id) must be unique + * within your application. + * @param customTile A {@link cyanogenmod.app.CustomTile} object describing what to + * show the user. Must not be null. + */ + public void publishTile(String tag, int id, CustomTile customTile) { + if (sService == null) { + Log.w(TAG, "not connected to CMStatusBarManagerService"); + return; + } + + int[] idOut = new int[1]; + String pkg = mContext.getPackageName(); + if (localLOGV) Log.v(TAG, pkg + ": create(" + id + ", " + customTile + ")"); + try { + sService.createCustomTileWithTag(pkg, mContext.getOpPackageName(), tag, id, + customTile, idOut, UserHandle.myUserId()); + if (id != idOut[0]) { + Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); + } + } catch (RemoteException e) { + Slog.w("CMStatusBarManager", "warning: no cm status bar service"); + } + } + + /** + * Similar to {@link cyanogenmod.app.CMStatusBarManager#publishTile(int id, cyanogenmod.app.CustomTile)}, + * however lets you specify a {@link android.os.UserHandle} + * @param tag A string identifier for this custom tile. May be {@code null}. + * @param id An identifier for this custom tile. The pair (tag, id) must be unique + * within your application. + * @param customTile A {@link cyanogenmod.app.CustomTile} object describing what to + * show the user. Must not be null. + * @param user A user handle to publish the tile as. + */ + public void publishTileAsUser(String tag, int id, CustomTile customTile, UserHandle user) { + if (sService == null) { + Log.w(TAG, "not connected to CMStatusBarManagerService"); + return; + } + + int[] idOut = new int[1]; + String pkg = mContext.getPackageName(); + if (localLOGV) Log.v(TAG, pkg + ": create(" + id + ", " + customTile + ")"); + try { + sService.createCustomTileWithTag(pkg, mContext.getOpPackageName(), tag, id, + customTile, idOut, user.getIdentifier()); + if (id != idOut[0]) { + Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); + } + } catch (RemoteException e) { + Slog.w("CMStatusBarManager", "warning: no cm status bar service"); + } + } + + /** + * Remove a custom tile that's currently published to the StatusBarPanel. + * @param id The identifier for the custom tile to be removed. + */ + public void removeTile(int id) { + removeTile(null, id); + } + + /** + * Remove a custom tile that's currently published to the StatusBarPanel. + * @param tag The string identifier for the custom tile to be removed. + * @param id The identifier for the custom tile to be removed. + */ + public void removeTile(String tag, int id) { + if (sService == null) { + Log.w(TAG, "not connected to CMStatusBarManagerService"); + return; + } + + String pkg = mContext.getPackageName(); + if (localLOGV) Log.v(TAG, pkg + ": remove(" + id + ")"); + try { + sService.removeCustomTileWithTag(pkg, tag, id, UserHandle.myUserId()); + } catch (RemoteException e) { + Slog.w("CMStatusBarManager", "warning: no cm status bar service"); + } + } + + /** + * Similar to {@link cyanogenmod.app.CMStatusBarManager#removeTile(String tag, int id)} however lets you + * specific a {@link android.os.UserHandle} + * @param tag The string identifier for the custom tile to be removed. + * @param id The identifier for the custom tile to be removed. + * @param user The user handle to remove the tile from. + */ + public void removeTileAsUser(String tag, int id, UserHandle user) { + if (sService == null) { + Log.w(TAG, "not connected to CMStatusBarManagerService"); + return; + } + + String pkg = mContext.getPackageName(); + if (localLOGV) Log.v(TAG, pkg + ": remove(" + id + ")"); + try { + sService.removeCustomTileWithTag(pkg, tag, id, user.getIdentifier()); + } catch (RemoteException e) { + } + } + + /** @hide */ + public ICMStatusBarManager getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(CMContextConstants.CM_STATUS_BAR_SERVICE); + sService = ICMStatusBarManager.Stub.asInterface(b); + return sService; + } +} diff --git a/src/java/cyanogenmod/app/CustomTile.aidl b/src/java/cyanogenmod/app/CustomTile.aidl new file mode 100644 index 0000000..1a35ad0 --- /dev/null +++ b/src/java/cyanogenmod/app/CustomTile.aidl @@ -0,0 +1,19 @@ +/** + * 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 cyanogenmod.app; + +parcelable CustomTile; diff --git a/src/java/cyanogenmod/app/CustomTile.java b/src/java/cyanogenmod/app/CustomTile.java new file mode 100644 index 0000000..4048c9e --- /dev/null +++ b/src/java/cyanogenmod/app/CustomTile.java @@ -0,0 +1,296 @@ +/** + * 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 cyanogenmod.app; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * A class that represents a quick settings tile + * + *

The {@link cyanogenmod.app.CustomTile.Builder} has been added to make it + * easier to construct CustomTiles.

+ */ +public class CustomTile implements Parcelable { + + /** + * An optional intent to execute when the custom tile entry is clicked. If + * this is an activity, it must include the + * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires + * that you take care of task management + **/ + public PendingIntent onClick; + + /** + * An optional Uri to be parsed and broadcast on tile click + **/ + public Uri onClickUri; + + /** + * A label specific to the quick settings tile to be created + */ + public String label; + + /** + * A content description for the custom tile state + */ + public String contentDescription; + + /** + * An icon to represent the custom tile + */ + public int icon; + + /** + * Unflatten the CustomTile from a parcel. + */ + public CustomTile(Parcel parcel) + { + if (parcel.readInt() != 0) { + this.onClick = PendingIntent.CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + this.onClickUri = Uri.CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + this.label = parcel.readString(); + } + + if (parcel.readInt() != 0) { + this.contentDescription = parcel.readString(); + } + this.icon = parcel.readInt(); + } + + /** + * Constructs a CustomTile object with default values. + * You might want to consider using {@link cyanogenmod.app.CustomTile.Builder} instead. + */ + public CustomTile() + { + // Empty constructor + } + + @Override + public CustomTile clone() { + CustomTile that = new CustomTile(); + cloneInto(that); + return that; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + String NEW_LINE = System.getProperty("line.separator"); + if (onClickUri != null) { + b.append("onClickUri=" + onClickUri.toString() + NEW_LINE); + } + if (onClick != null) { + b.append("onClick=" + onClick.toString() + NEW_LINE); + } + if (!TextUtils.isEmpty(label)) { + b.append("label=" + label + NEW_LINE); + } + if (!TextUtils.isEmpty(contentDescription)) { + b.append("contentDescription=" + contentDescription + NEW_LINE); + } + b.append("icon=" + icon + NEW_LINE); + return b.toString(); + } + + /** + * Copy all of this into that + * @hide + */ + public void cloneInto(CustomTile that) { + that.onClick = this.onClick; + that.onClickUri = this.onClickUri; + that.label = this.label; + that.contentDescription = this.contentDescription; + that.icon = this.icon; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + if (onClick != null) { + out.writeInt(1); + onClick.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + if (onClickUri != null) { + out.writeInt(1); + onClickUri.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + if (label != null) { + out.writeInt(1); + out.writeString(label); + } else { + out.writeInt(0); + } + if (contentDescription != null) { + out.writeInt(1); + out.writeString(contentDescription); + } else { + out.writeInt(0); + } + out.writeInt(icon); + } + + /** + * Parcelable.Creator that instantiates CustomTile objects + */ + public static final Creator CREATOR = + new Creator() { + public CustomTile createFromParcel(Parcel in) { + return new CustomTile(in); + } + + @Override + public CustomTile[] newArray(int size) { + return new CustomTile[size]; + } + }; + + /** + * Builder class for {@link cyanogenmod.app.CustomTile} objects. + * + * Provides a convenient way to set the various fields of a {@link cyanogenmod.app.CustomTile} + * + *

Example: + * + *

+     * 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();
+     * 
+ */ + public static class Builder { + private PendingIntent mOnClick; + private Uri mOnClickUri; + private String mLabel; + private String mContentDescription; + private int mIcon; + private Context mContext; + + /** + * Constructs a new Builder with the defaults: + */ + public Builder(Context context) { + mContext = context; + } + + /** + * Set the label for the custom tile + * @param label a string to be used for the custom tile label + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setLabel(String label) { + mLabel = label; + return this; + } + + /** + * Set the label for the custom tile + * @param id a string resource id to be used for the custom tile label + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setLabel(int id) { + mLabel = mContext.getString(id); + return this; + } + + /** + * Set the content description for the custom tile + * @param contentDescription a string to explain content + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setContentDescription(String contentDescription) { + mContentDescription = contentDescription; + return this; + } + + /** + * Set the content description for the custom tile + * @param id a string resource id to explain content + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setContentDescription(int id) { + mContentDescription = mContext.getString(id); + return this; + } + + /** + * Set a {@link android.app.PendingIntent} to be fired on custom tile click + * @param intent + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setOnClickIntent(PendingIntent intent) { + mOnClick = intent; + return this; + } + + /** + * Set a {@link android.net.Uri} to be broadcasted in an intent on custom tile click + * @param uri + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setOnClickUri(Uri uri) { + mOnClickUri = uri; + return this; + } + + /** + * Set an icon for the custom tile to be presented to the user + * @param drawableId + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setIcon(int drawableId) { + mIcon = drawableId; + return this; + } + + /** + * Create a {@link cyanogenmod.app.CustomTile} object + * @return {@link cyanogenmod.app.CustomTile} + */ + public CustomTile build() { + CustomTile tile = new CustomTile(); + tile.onClick = mOnClick; + tile.onClickUri = mOnClickUri; + tile.label = mLabel; + tile.contentDescription = mContentDescription; + tile.icon = mIcon; + return tile; + } + } +} diff --git a/src/java/cyanogenmod/app/CustomTileListenerService.java b/src/java/cyanogenmod/app/CustomTileListenerService.java new file mode 100644 index 0000000..6c7dce0 --- /dev/null +++ b/src/java/cyanogenmod/app/CustomTileListenerService.java @@ -0,0 +1,194 @@ +/** + * 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 cyanogenmod.app; + +import android.annotation.SdkConstant; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import cyanogenmod.app.ICustomTileListener; +import cyanogenmod.app.ICMStatusBarManager; + +import org.cyanogenmod.internal.statusbar.IStatusBarCustomTileHolder; + +/** + * A service that receives calls from the system when new custom tiles are + * posted or removed. + *

To extend this class, you must declare the service in your manifest file with + * the TODO: add permission + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:

+ *
+ * <service android:name=".CustomTileListener"
+ *          android:label="@string/service_name"
+ *          android:permission="TODO: Add me">
+ *     <intent-filter>
+ *         <action android:name="cyanogenmod.app.CustomTileListenerService" />
+ *     </intent-filter>
+ * </service>
+ */ +public class CustomTileListenerService extends Service { + private final String TAG = CustomTileListenerService.class.getSimpleName() + + "[" + getClass().getSimpleName() + "]"; + /** + * The {@link android.content.Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE + = "cyanogenmod.app.CustomTileListenerService"; + + private ICustomTileListenerWrapper mWrapper = null; + private ICMStatusBarManager mStatusBarService; + /** Only valid after a successful call to (@link registerAsService}. */ + private int mCurrentUser; + + @Override + public IBinder onBind(Intent intent) { + if (mWrapper == null) { + mWrapper = new ICustomTileListenerWrapper(); + } + return mWrapper; + } + + private final ICMStatusBarManager getStatusBarInterface() { + if (mStatusBarService == null) { + mStatusBarService = ICMStatusBarManager.Stub.asInterface( + ServiceManager.getService(CMContextConstants.CM_STATUS_BAR_SERVICE)); + } + return mStatusBarService; + } + + /** + * Directly register this service with the StatusBar Manager. + * + *

Only system services may use this call. It will fail for non-system callers. + * Apps should ask the user to add their listener in Settings. + * + * @param context Context required for accessing resources. Since this service isn't + * launched as a real Service when using this method, a context has to be passed in. + * @param componentName the component that will consume the custom tile information + * @param currentUser the user to use as the stream filter + * @hide + */ + public void registerAsSystemService(Context context, ComponentName componentName, + int currentUser) throws RemoteException { + if (mWrapper == null) { + mWrapper = new ICustomTileListenerWrapper(); + } + ICMStatusBarManager statusBarInterface = getStatusBarInterface(); + statusBarInterface.registerListener(mWrapper, componentName, currentUser); + mCurrentUser = currentUser; + } + + /** + * Directly unregister this service from the StatusBar Manager. + * + *

This method will fail for listeners that were not registered + * with (@link registerAsService). + * @hide + */ + public void unregisterAsSystemService() throws RemoteException { + if (mWrapper != null) { + ICMStatusBarManager statusBarInterface = getStatusBarInterface(); + statusBarInterface.unregisterListener(mWrapper, mCurrentUser); + } + } + + + private class ICustomTileListenerWrapper extends ICustomTileListener.Stub { + @Override + public void onListenerConnected() { + synchronized (mWrapper) { + try { + CustomTileListenerService.this.onListenerConnected(); + } catch (Throwable t) { + Log.w(TAG, "Error running onListenerConnected", t); + } + } + } + @Override + public void onCustomTilePosted(IStatusBarCustomTileHolder sbcHolder) { + StatusBarPanelCustomTile sbc; + try { + sbc = sbcHolder.get(); + } catch (RemoteException e) { + Log.w(TAG, "onCustomTilePosted: Error receiving StatusBarPanelCustomTile", e); + return; + } + synchronized (mWrapper) { + try { + CustomTileListenerService.this.onCustomTilePosted(sbc); + } catch (Throwable t) { + Log.w(TAG, "Error running onCustomTilePosted", t); + } + } + } + @Override + public void onCustomTileRemoved(IStatusBarCustomTileHolder sbcHolder) { + StatusBarPanelCustomTile sbc; + try { + sbc = sbcHolder.get(); + } catch (RemoteException e) { + Log.w(TAG, "onCustomTileRemoved: Error receiving StatusBarPanelCustomTile", e); + return; + } + synchronized (mWrapper) { + try { + CustomTileListenerService.this.onCustomTileRemoved(sbc); + } catch (Throwable t) { + Log.w(TAG, "Error running onCustomTileRemoved", t); + } + } + } + } + + /** + * Implement this method to learn about new custom tiles as they are posted by apps. + * + * @param sbc A data structure encapsulating the original {@link cyanogenmod.app.CustomTile} + * object as well as its identifying information (tag and id) and source + * (package name). + */ + public void onCustomTilePosted(StatusBarPanelCustomTile sbc) { + // optional + } + + /** + * Implement this method to learn when custom tiles are removed. + * + * @param sbc A data structure encapsulating at least the original information (tag and id) + * and source (package name) used to post the {@link cyanogenmod.app.CustomTile} that + * was just removed. + */ + public void onCustomTileRemoved(StatusBarPanelCustomTile sbc) { + // optional + } + + /** + * Implement this method to learn about when the listener is enabled and connected to + * the status bar manager. + * at this time. + */ + public void onListenerConnected() { + // optional + } +} diff --git a/src/java/cyanogenmod/app/ICMStatusBarManager.aidl b/src/java/cyanogenmod/app/ICMStatusBarManager.aidl new file mode 100644 index 0000000..439acce --- /dev/null +++ b/src/java/cyanogenmod/app/ICMStatusBarManager.aidl @@ -0,0 +1,37 @@ +/** + * 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 cyanogenmod.app; + +import android.content.ComponentName; + +import cyanogenmod.app.CustomTile; +import cyanogenmod.app.ICustomTileListener; + +/** @hide */ +interface ICMStatusBarManager { + // --- Methods below are for use by 3rd party applications to publish quick + // settings tiles to the status bar panel + // You need the PUBLISH_QUICK_SETTINGS_TILE permission + void createCustomTileWithTag(String pkg, String opPkg, String tag, int id, + in CustomTile tile, inout int[] idReceived, int userId); + void removeCustomTileWithTag(String pkg, String tag, int id, int userId); + + // --- Methods below are for use by 3rd party applications + // You need the BIND_QUICK_SETTINGS_TILE_LISTENER permission + void registerListener(in ICustomTileListener listener, in ComponentName component, int userid); + void unregisterListener(in ICustomTileListener listener, int userid); +} diff --git a/src/java/cyanogenmod/app/ICustomTileListener.aidl b/src/java/cyanogenmod/app/ICustomTileListener.aidl new file mode 100644 index 0000000..9f21f52 --- /dev/null +++ b/src/java/cyanogenmod/app/ICustomTileListener.aidl @@ -0,0 +1,29 @@ +/** + * 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 cyanogenmod.app; + +import cyanogenmod.app.StatusBarPanelCustomTile; + +import org.cyanogenmod.internal.statusbar.IStatusBarCustomTileHolder; + +/** @hide */ +oneway interface ICustomTileListener +{ + void onListenerConnected(); + void onCustomTilePosted(in IStatusBarCustomTileHolder customTileHolder); + void onCustomTileRemoved(in IStatusBarCustomTileHolder customTileHolder); +} diff --git a/src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl b/src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl new file mode 100644 index 0000000..96cfb6a --- /dev/null +++ b/src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl @@ -0,0 +1,20 @@ +/** + * 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 cyanogenmod.app; + +parcelable StatusBarPanelCustomTile; + diff --git a/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java b/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java new file mode 100644 index 0000000..27d8151 --- /dev/null +++ b/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java @@ -0,0 +1,193 @@ +/* + * 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 cyanogenmod.app; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; + +/** + * Class encapsulating a Custom Tile. Sent by the StatusBarManagerService to clients including + * the status bar panel and any {@link cyanogenmod.app.CustomTileListenerService} clients. + */ +public class StatusBarPanelCustomTile implements Parcelable { + + private final String pkg; + private final int id; + private final String tag; + private final String key; + + private final int uid; + private final String opPkg; + private final int initialPid; + private final CustomTile customTile; + private final UserHandle user; + private final long postTime; + + public StatusBarPanelCustomTile(String pkg, String opPkg, int id, String tag, int uid, + int initialPid, CustomTile customTile, UserHandle user) { + this(pkg, opPkg, id, tag, uid, initialPid, customTile, user, + System.currentTimeMillis()); + } + + public StatusBarPanelCustomTile(String pkg, String opPkg, int id, String tag, int uid, + int initialPid, CustomTile customTile, UserHandle user, + long postTime) { + if (pkg == null) throw new NullPointerException(); + if (customTile == null) throw new NullPointerException(); + + this.pkg = pkg; + this.opPkg = opPkg; + this.id = id; + this.tag = tag; + this.uid = uid; + this.initialPid = initialPid; + this.customTile = customTile; + this.user = user; + this.postTime = postTime; + this.key = key(); + } + + + public StatusBarPanelCustomTile(Parcel in) { + this.pkg = in.readString(); + this.opPkg = in.readString(); + this.id = in.readInt(); + if (in.readInt() != 0) { + this.tag = in.readString(); + } else { + this.tag = null; + } + this.uid = in.readInt(); + this.initialPid = in.readInt(); + this.customTile = new CustomTile(in); + this.user = UserHandle.readFromParcel(in); + this.postTime = in.readLong(); + this.key = key(); + } + + private String key() { + return user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; + } + + public static final Creator CREATOR + = new Creator() + { + public StatusBarPanelCustomTile createFromParcel(Parcel parcel) + { + return new StatusBarPanelCustomTile(parcel); + } + + public StatusBarPanelCustomTile[] newArray(int size) + { + return new StatusBarPanelCustomTile[size]; + } + }; + + /** The {@link cyanogenmod.app.CustomTile} supplied to + * {@link cyanogenmod.app.CMStatusBarManager#publishTile(int, cyanogenmod.app.CustomTile)}. + */ + public CustomTile getCustomTile() { + return customTile; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(this.pkg); + out.writeString(this.opPkg); + out.writeInt(this.id); + if (this.tag != null) { + out.writeInt(1); + out.writeString(this.tag); + } else { + out.writeInt(0); + } + out.writeInt(this.uid); + out.writeInt(this.initialPid); + this.customTile.writeToParcel(out, flags); + user.writeToParcel(out, flags); + out.writeLong(this.postTime); + } + + @Override + public StatusBarPanelCustomTile clone() { + return new StatusBarPanelCustomTile(this.pkg, this.opPkg, + this.id, this.tag, this.uid, this.initialPid, + this.customTile.clone(), this.user, this.postTime); + } + + /** + * Returns a userHandle for the instance of the app that posted this notification. + */ + public int getUserId() { + return this.user.getIdentifier(); + } + + /** The package of the app that posted the notification */ + public String getPackage() { + return pkg; + } + + /** The id supplied to CMStatusBarManager */ + public int getId() { + return id; + } + + /** The tag supplied to CMStatusBarManager or null if no tag was specified. */ + public String getTag() { + return tag; + } + + /** + * A unique instance key for this notification record. + */ + public String getKey() { + return key; + } + + /** The notifying app's calling uid. @hide */ + public int getUid() { + return uid; + } + + /** The package used for AppOps tracking. @hide */ + public String getOpPkg() { + return opPkg; + } + + /** @hide */ + public int getInitialPid() { + return initialPid; + } + + /** + * The {@link android.os.UserHandle} for whom this CustomTile is intended. + */ + public UserHandle getUser() { + return user; + } + + /** The time (in {@link System#currentTimeMillis} time) the CustomTile was published, */ + public long getPostTime() { + return postTime; + } +} diff --git a/src/java/org/cyanogenmod/internal/statusbar/ExternalQuickSettingsRecord.java b/src/java/org/cyanogenmod/internal/statusbar/ExternalQuickSettingsRecord.java new file mode 100644 index 0000000..05f8edf --- /dev/null +++ b/src/java/org/cyanogenmod/internal/statusbar/ExternalQuickSettingsRecord.java @@ -0,0 +1,53 @@ +/** + * 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.internal.statusbar; + +import android.os.UserHandle; +import com.android.internal.annotations.VisibleForTesting; + +import cyanogenmod.app.CustomTile; +import cyanogenmod.app.StatusBarPanelCustomTile; + +/** + * @hide + */ +public class ExternalQuickSettingsRecord { + public final StatusBarPanelCustomTile sbTile; + public boolean isUpdate; + public boolean isCanceled; + + @VisibleForTesting + public ExternalQuickSettingsRecord(StatusBarPanelCustomTile tile) { + sbTile = tile; + } + + public CustomTile getCustomTile() { + return sbTile.getCustomTile(); + } + + public UserHandle getUser() { + return sbTile.getUser(); + } + + public int getUserId() { + return sbTile.getUserId(); + } + + public String getKey() { + return sbTile.getKey(); + } +} diff --git a/src/java/org/cyanogenmod/internal/statusbar/IStatusBarCustomTileHolder.aidl b/src/java/org/cyanogenmod/internal/statusbar/IStatusBarCustomTileHolder.aidl new file mode 100644 index 0000000..90e04de --- /dev/null +++ b/src/java/org/cyanogenmod/internal/statusbar/IStatusBarCustomTileHolder.aidl @@ -0,0 +1,25 @@ +/** + * 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.internal.statusbar; + +import cyanogenmod.app.StatusBarPanelCustomTile; + +/** @hide */ +interface IStatusBarCustomTileHolder { + /** Fetch the held StatusBarPanelCustomTile. This method should only be called once per Holder */ + StatusBarPanelCustomTile get(); +} \ No newline at end of file