Browse Source

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
replicant-6.0
Adnan Begovic 5 years ago
parent
commit
aa8614e39b
15 changed files with 2385 additions and 0 deletions
  1. +131
    -0
      Android.mk
  2. +477
    -0
      cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java
  3. +634
    -0
      cm/lib/java/org/cyanogenmod/platform/internal/ManagedServices.java
  4. +20
    -0
      org.cyanogenmod.platform.xml
  5. +42
    -0
      src/java/cyanogenmod/app/CMContextConstants.java
  6. +215
    -0
      src/java/cyanogenmod/app/CMStatusBarManager.java
  7. +19
    -0
      src/java/cyanogenmod/app/CustomTile.aidl
  8. +296
    -0
      src/java/cyanogenmod/app/CustomTile.java
  9. +194
    -0
      src/java/cyanogenmod/app/CustomTileListenerService.java
  10. +37
    -0
      src/java/cyanogenmod/app/ICMStatusBarManager.aidl
  11. +29
    -0
      src/java/cyanogenmod/app/ICustomTileListener.aidl
  12. +20
    -0
      src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl
  13. +193
    -0
      src/java/cyanogenmod/app/StatusBarPanelCustomTile.java
  14. +53
    -0
      src/java/org/cyanogenmod/internal/statusbar/ExternalQuickSettingsRecord.java
  15. +25
    -0
      src/java/org/cyanogenmod/internal/statusbar/IStatusBarCustomTileHolder.aidl

+ 131
- 0
Android.mk View File

@@ -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))

+ 477
- 0
cm/lib/java/org/cyanogenmod/platform/internal/CMStatusBarManagerService.java View File

@@ -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;
}
}
}

+ 634
- 0
cm/lib/java/org/cyanogenmod/platform/internal/ManagedServices.java View File

@@ -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;
}
}

+ 20
- 0
org.cyanogenmod.platform.xml View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->

<permissions>
<library name="org.cyanogenmod.platform"
file="/system/framework/org.cyanogenmod.platform.jar" />
</permissions>

+ 42
- 0
src/java/cyanogenmod/app/CMContextConstants.java View File

@@ -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";

}

+ 215
- 0
src/java/cyanogenmod/app/CMStatusBarManager.java View File

@@ -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.
*
* <p>
* 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.
*
* <p>
* 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;
}
}

+ 19
- 0
src/java/cyanogenmod/app/CustomTile.aidl View File

@@ -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;

+ 296
- 0
src/java/cyanogenmod/app/CustomTile.java View File

@@ -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
*
* <p>The {@link cyanogenmod.app.CustomTile.Builder} has been added to make it
* easier to construct CustomTiles.</p>
*/
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<CustomTile> CREATOR =
new Creator<CustomTile>() {
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}
*
* <p>Example:
*
* <pre class="prettyprint">
* 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();
* </pre>
*/
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;
}
}
}

+ 194
- 0
src/java/cyanogenmod/app/CustomTileListenerService.java View File

@@ -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.
* <p>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:</p>
* <pre>
* &lt;service android:name=".CustomTileListener"
* android:label="&#64;string/service_name"
* android:permission="TODO: Add me">
* &lt;intent-filter>
* &lt;action android:name="cyanogenmod.app.CustomTileListenerService" />
* &lt;/intent-filter>
* &lt;/service></pre>
*/
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.
*
* <p>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.
*
* <P>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
}
}

+ 37
- 0
src/java/cyanogenmod/app/ICMStatusBarManager.aidl View File

@@ -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);
}

+ 29
- 0
src/java/cyanogenmod/app/ICustomTileListener.aidl View File

@@ -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);
}

+ 20
- 0
src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl View File

@@ -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.
*/