diff options
Diffstat (limited to 'sdk/src/java/cyanogenmod/app')
32 files changed, 6605 insertions, 0 deletions
diff --git a/sdk/src/java/cyanogenmod/app/BaseLiveLockManagerService.java b/sdk/src/java/cyanogenmod/app/BaseLiveLockManagerService.java new file mode 100644 index 0000000..feae94f --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/BaseLiveLockManagerService.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2016 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.NonNull; +import android.app.AppGlobals; +import android.app.Service; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import cyanogenmod.platform.Manifest; + +/** + * Base Live lock screen manager service to be extended by applications that implement the + * {@link LiveLockScreenManager#SERVICE_INTERFACE} + * + * @hide + */ +abstract public class BaseLiveLockManagerService extends Service + implements ILiveLockScreenManagerProvider { + private static final String TAG = BaseLiveLockManagerService.class.getSimpleName(); + + private final RemoteCallbackList<ILiveLockScreenChangeListener> mChangeListeners = + new RemoteCallbackList<>(); + + @Override + public final IBinder onBind(Intent intent) { + return mService; + } + + @Override + public final IBinder asBinder() { + return mService; + } + + @Override + abstract public void enqueueLiveLockScreen(String pkg, int id, LiveLockScreenInfo lls, + int[] idReceived, int userId) throws RemoteException; + + @Override + abstract public void cancelLiveLockScreen(String pkg, int id, int userId) + throws RemoteException; + + @Override + abstract public LiveLockScreenInfo getCurrentLiveLockScreen() throws RemoteException; + + @Override + abstract public void updateDefaultLiveLockScreen(LiveLockScreenInfo llsInfo) + throws RemoteException; + + @Override + public boolean getLiveLockScreenEnabled() throws RemoteException { + return false; + } + + @Override + public final boolean registerChangeListener( + ILiveLockScreenChangeListener listener) throws RemoteException { + return mChangeListeners.register(listener); + } + + @Override + public final boolean unregisterChangeListener( + ILiveLockScreenChangeListener listener) throws RemoteException { + return mChangeListeners.unregister(listener); + } + + /** + * This method should be called whenever there is an update to the current Live lock screen + * to be displayed. + * + * @param llsInfo LiveLockScreenInfo for the current Live lock screen + */ + protected final void notifyChangeListeners(LiveLockScreenInfo llsInfo) { + int N = mChangeListeners.beginBroadcast(); + for (int i = 0; i < N; i++) { + ILiveLockScreenChangeListener listener = mChangeListeners.getBroadcastItem(i); + try { + listener.onLiveLockScreenChanged(llsInfo); + } catch (RemoteException e) { + Log.w(TAG, "Unable to notifiy change listener", e); + } + } + mChangeListeners.finishBroadcast(); + } + + /** + * Returns true if the caller has been granted the + * {@link cyanogenmod.platform.Manifest.permission#LIVE_LOCK_SCREEN_MANAGER_ACCESS_PRIVATE} + * permission. + * + * @return + */ + private final boolean hasPrivatePermissions() { + return checkCallingPermission(Manifest.permission + .LIVE_LOCK_SCREEN_MANAGER_ACCESS_PRIVATE) == PackageManager.PERMISSION_GRANTED; + } + + /** + * Enforces the {@link cyanogenmod.platform.Manifest.permission#LIVE_LOCK_SCREEN_MANAGER_ACCESS} + * permission. + */ + protected final void enforceAccessPermission() { + if (hasPrivatePermissions()) return; + + enforceCallingPermission(Manifest.permission.LIVE_LOCK_SCREEN_MANAGER_ACCESS, + null); + } + + /** + * Enforces the + * {@link cyanogenmod.platform.Manifest.permission#LIVE_LOCK_SCREEN_MANAGER_ACCESS_PRIVATE} + * permission. + */ + protected final void enforcePrivateAccessPermission() { + enforceCallingPermission( + Manifest.permission.LIVE_LOCK_SCREEN_MANAGER_ACCESS_PRIVATE, null); + } + + /** + * Enforces the LLS being shown/canceled is from the calling package or from a system app that + * has the + * {@link cyanogenmod.platform.Manifest.permission#LIVE_LOCK_SCREEN_MANAGER_ACCESS_PRIVATE} + * permission. + * + * @param pkg Package name of caller + * @param llsInfo Live lock screen info with component to check + */ + protected final void enforceSamePackageOrSystem(String pkg, + @NonNull LiveLockScreenInfo llsInfo) { + // only apps with the private permission can show/cancel live lock screens from other + // packages + if (hasPrivatePermissions()) return; + + if (llsInfo.component != null && !llsInfo.component.getPackageName().equals(pkg)) { + throw new SecurityException("Modifying Live lock screen from different packages not " + + "allowed. Calling package: " + pkg + " LLS package: " + + llsInfo.component.getPackageName()); + } + + 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 final IBinder mService = new ILiveLockScreenManagerProvider.Stub() { + @Override + public void enqueueLiveLockScreen(String pkg, int id, LiveLockScreenInfo llsInfo, + int[] idReceived, int userId) throws RemoteException { + enforceAccessPermission(); + enforceSamePackageOrSystem(pkg, llsInfo); + BaseLiveLockManagerService.this.enqueueLiveLockScreen(pkg, id, llsInfo, idReceived, + userId); + } + + @Override + public void cancelLiveLockScreen(String pkg, int id, int userId) throws RemoteException { + enforceAccessPermission(); + BaseLiveLockManagerService.this.cancelLiveLockScreen(pkg, id, userId); + } + + @Override + public LiveLockScreenInfo getCurrentLiveLockScreen() throws RemoteException { + enforceAccessPermission(); + return BaseLiveLockManagerService.this.getCurrentLiveLockScreen(); + } + + @Override + public void updateDefaultLiveLockScreen(LiveLockScreenInfo llsInfo) throws RemoteException { + enforcePrivateAccessPermission(); + BaseLiveLockManagerService.this.updateDefaultLiveLockScreen(llsInfo); + } + + @Override + public boolean getLiveLockScreenEnabled() throws RemoteException { + enforceAccessPermission(); + return BaseLiveLockManagerService.this.getLiveLockScreenEnabled(); + } + + @Override + public boolean registerChangeListener( + ILiveLockScreenChangeListener listener) throws RemoteException { + enforcePrivateAccessPermission(); + return BaseLiveLockManagerService.this.registerChangeListener(listener); + } + + @Override + public boolean unregisterChangeListener( + ILiveLockScreenChangeListener listener) throws RemoteException { + enforcePrivateAccessPermission(); + return BaseLiveLockManagerService.this.unregisterChangeListener(listener); + } + }; +} diff --git a/sdk/src/java/cyanogenmod/app/CMContextConstants.java b/sdk/src/java/cyanogenmod/app/CMContextConstants.java new file mode 100644 index 0000000..98171b8 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/CMContextConstants.java @@ -0,0 +1,218 @@ +/** + * 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; + +/** + * @hide + * TODO: We need to somehow make these managers accessible via getSystemService + */ +public final 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"; + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.app.ProfileManager} for informing the user of + * background events. + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.app.ProfileManager + * + * @hide + */ + public static final String CM_PROFILE_SERVICE = "profile"; + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.app.PartnerInterface} interact with system settings. + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.app.PartnerInterface + * + * @hide + */ + public static final String CM_PARTNER_INTERFACE = "cmpartnerinterface"; + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.app.CMTelephonyManager} to manage the phone and + * data connection. + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.app.CMTelephonyManager + * + * @hide + */ + public static final String CM_TELEPHONY_MANAGER_SERVICE = "cmtelephonymanager"; + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.hardware.CMHardwareManager} to manage the extended + * hardware features of the device. + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.hardware.CMHardwareManager + * + * @hide + */ + public static final String CM_HARDWARE_SERVICE = "cmhardware"; + + /** + * @hide + */ + public static final String CM_APP_SUGGEST_SERVICE = "cmappsuggest"; + + /** + * Control device power profile and characteristics. + * + * @hide + */ + public static final String CM_PERFORMANCE_SERVICE = "cmperformance"; + + /** + * Controls changing and applying themes + * + * @hide + */ + public static final String CM_THEME_SERVICE = "cmthemes"; + + /** + * Manages composed icons + * + * @hide + */ + public static final String CM_ICON_CACHE_SERVICE = "cmiconcache"; + + /** + * @hide + */ + public static final String CM_LIVE_LOCK_SCREEN_SERVICE = "cmlivelockscreen"; + + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.weather.CMWeatherManager} to manage the weather service + * settings and request weather updates + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.weather.CMWeatherManager + * + * @hide + */ + public static final String CM_WEATHER_SERVICE = "cmweather"; + + /** + * Features supported by the CMSDK. + */ + public static class Features { + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the hardware abstraction + * framework service utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String HARDWARE_ABSTRACTION = "org.cyanogenmod.hardware"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm status bar service + * utilzed by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String STATUSBAR = "org.cyanogenmod.statusbar"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm profiles service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String PROFILES = "org.cyanogenmod.profiles"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm app suggest service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String APP_SUGGEST = "org.cyanogenmod.appsuggest"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm telephony service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String TELEPHONY = "org.cyanogenmod.telephony"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm theme service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String THEMES = "org.cyanogenmod.theme"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm performance service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String PERFORMANCE = "org.cyanogenmod.performance"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm partner service + * utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String PARTNER = "org.cyanogenmod.partner"; + + /* + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the Live lock screen + * feature. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String LIVE_LOCK_SCREEN = "org.cyanogenmod.livelockscreen"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm weather weather + * service utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String WEATHER_SERVICES = "org.cyanogenmod.weather"; + } +} diff --git a/sdk/src/java/cyanogenmod/app/CMStatusBarManager.java b/sdk/src/java/cyanogenmod/app/CMStatusBarManager.java new file mode 100644 index 0000000..d696a82 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/CMStatusBarManager.java @@ -0,0 +1,247 @@ +/** + * 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(); + + if (context.getPackageManager().hasSystemFeature( + cyanogenmod.app.CMContextConstants.Features.STATUSBAR) && sService == null) { + throw new RuntimeException("Unable to get CMStatusBarService. The service either" + + " crashed, was not started, or the interface has been called to early in" + + " SystemServer init"); + } + } + + /** + * 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. + * + * You will need the cyanogenmod.permission.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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. + * + * You will need the cyanogenmod.permission.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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} + * + * You will need the cyanogenmod.permission.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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. + * + * You will need the cyanogenmod.permission.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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. + * + * You will need the cyanogenmod.platform.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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} + * + * You will need the cyanogenmod.platform.PUBLISH_CUSTOM_TILE + * to utilize this functionality. + * + * @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); + if (b != null) { + sService = ICMStatusBarManager.Stub.asInterface(b); + return sService; + } + return null; + } +} diff --git a/sdk/src/java/cyanogenmod/app/CMTelephonyManager.java b/sdk/src/java/cyanogenmod/app/CMTelephonyManager.java new file mode 100644 index 0000000..4285f44 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/CMTelephonyManager.java @@ -0,0 +1,358 @@ +/** + * 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.telephony.SubscriptionInfo; +import android.util.Log; +import android.util.Slog; + +import java.util.List; + +import cyanogenmod.app.CMContextConstants; + +/** + * The CMTelephonyManager allows you to view and manage the phone state and + * the data connection, with multiple SIMs support. + * + * <p> + * To get the instance of this class, utilize CMTelephonyManager#getInstance(Context context) + */ +public class CMTelephonyManager { + + /** + * Subscription ID used to set the default Phone and SMS to "ask every time". + */ + public static final int ASK_FOR_SUBSCRIPTION_ID = 0; + + private static final String TAG = "CMTelephonyManager"; + private static boolean localLOGD = Log.isLoggable(TAG, Log.DEBUG); + + private static ICMTelephonyManager sService; + private static CMTelephonyManager sCMTelephonyManagerInstance; + private Context mContext; + + private CMTelephonyManager(Context context) { + Context appContext = context.getApplicationContext(); + if (appContext != null) { + mContext = appContext; + } else { + mContext = context; + } + sService = getService(); + + if (context.getPackageManager().hasSystemFeature(CMContextConstants.Features.TELEPHONY) + && sService == null) { + throw new RuntimeException("Unable to get CMTelephonyManagerService. " + + "The service either crashed, was not started, or the interface has been " + + "called to early in SystemServer init"); + } + } + + /** + * Get or create an instance of the {@link cyanogenmod.app.CMTelephonyManager} + * + * @return {@link cyanogenmod.app.CMTelephonyManager} + */ + public static CMTelephonyManager getInstance(Context context) { + if (sCMTelephonyManagerInstance == null) { + sCMTelephonyManagerInstance = new CMTelephonyManager(context); + } + return sCMTelephonyManagerInstance; + } + + /** @hide */ + public ICMTelephonyManager getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(CMContextConstants.CM_TELEPHONY_MANAGER_SERVICE); + if (b != null) { + sService = ICMTelephonyManager.Stub.asInterface(b); + return sService; + } + return null; + } + + /** + * Gets the list of {@link SubscriptionInfo} that are registered on the + * phone. + * + * @return The list of SIM subscriptions. The returning list can be null or empty. + * @see SubscriptionInfo + */ + public List<SubscriptionInfo> getSubInformation() { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return null; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting the SIMs information"); + } + List<SubscriptionInfo> subInfoList = null; + try { + subInfoList = sService.getSubInformation(); + if (subInfoList == null) { + Log.w(TAG, "no subscription list was returned from the service"); + } else if (subInfoList.isEmpty()) { + Log.w(TAG, "the subscription list is empty"); + } + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + + return subInfoList; + } + + /** + * Returns the state of the SIM by subscription ID. + * + * If the subscription ID is not valid the method will return {@code false}. + * + * @param subId The subscription ID to query. + * @return {@code true} if the SIM is activated (even without signal or requesting the + * PIN/PUK), {@code false} otherwise. + */ + public boolean isSubActive(int subId) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return false; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting the state of the SIM with subscription: " + subId); + } + boolean simActive = false; + try { + simActive = sService.isSubActive(subId); + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting the SIM state with subscription " + subId + " as active: " + simActive); + } + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + + return simActive; + } + + /** + * Sets the state of one of the SIMs by subscription ID. + * + * If the subscription ID is not valid or the SIM already + * is in the desired state the method will do nothing. + * + * @param subId The subscription ID to change the state. + * @param state {@code true} to activate the SIM, {@code false} to disable. + */ + public void setSubState(int subId, boolean state) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " setting the state of the SIM with subscription " + subId + " as active: " + state); + } + + try { + sService.setSubState(subId, state); + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + } + + /** + * Checks if the received subscription received has the data + * connection enabled. + * + * This method will return {@code true} (or {@code false} if inactive on the SIM) + * even when an internet connection is active through Wifi/BT. + * + * If the subscription ID is not valid the method will return false. + * + * @param subId The subscription ID to query. + * @return {@code true} if the data connection is enabled on the SIM, {@code false} otherwise. + */ + public boolean isDataConnectionSelectedOnSub(int subId) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return false; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting if the data connection is enabled for SIM for subscription: " + subId); + } + boolean dataConnectionActiveOnSim = false; + try { + dataConnectionActiveOnSim = sService.isDataConnectionSelectedOnSub(subId); + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting if the data connection is enabled for SIM with subscription " + + subId + " as active: " + dataConnectionActiveOnSim); + } + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + + return dataConnectionActiveOnSim; + } + + /** + * Checks if the network data connection is enabled. + * + * This method will return {@code true} (or {@code false} if inactive) + * even when an internet connection is active through Wifi/BT. + * + * @return {@code true} if the network data connection is enabled, {@code false} otherwise. + */ + public boolean isDataConnectionEnabled() { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return false; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting if the network data connection is enabled"); + } + boolean dataConnectionEnabled = false; + try { + dataConnectionEnabled = sService.isDataConnectionEnabled(); + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " getting if the network data connection is enabled: " + dataConnectionEnabled); + } + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + + return dataConnectionEnabled; + } + + /** + * Sets the network data conection active or inactive. + * + * @param state If {@code true} enables the network data connection, if {@code false} disables it. + */ + public void setDataConnectionState(boolean state) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " setting the network data connection enabled: " + state); + } + + try { + sService.setDataConnectionState(state); + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + } + + /** + * Sets the data connection state on one of the SIMs by subscription ID. + * + * If the subscription ID is not valid or the data connection is already + * enabled on the SIM the method will do nothing. + * + * @param subId The subscription ID to set the network data connection. + * @hide + */ + public void setDataConnectionSelectedOnSub(int subId) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " setting the network data connection for SIM with subscription: " + subId); + } + + try { + sService.setDataConnectionSelectedOnSub(subId); + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + } + + /** + * Sets the default phone used to make phone calls as the one received on subId. + * + * If ASK_FOR_SUBSCRIPTION_ID is used as a parameter, then the option to choose + * what SIM to use is selected. + * + * @param subId The subscription to set as default for phone calls. + * To select SIM when calling use ASK_FOR_SUBSCRIPTION_ID. + */ + public void setDefaultPhoneSub(int subId) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " setting the subscription used for phone calls as: " + subId); + } + + try { + sService.setDefaultPhoneSub(subId); + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + } + + /** + * Sets the default phone used to send SMS as the one received on subId. + * + * If ASK_FOR_SUBSCRIPTION_ID is used as a parameter, then the option to choose + * what SIM to use is selected. + * + * @param subId The subscription to set as default for sending SMS. + * To select SIM when sending SMS use ASK_FOR_SUBSCRIPTION_ID. + */ + public void setDefaultSmsSub(int subId) { + if (sService == null) { + Log.w(TAG, "not connected to CMTelephonyManager"); + return; + } + + if (localLOGD) { + String pkg = mContext.getPackageName(); + Log.v(TAG, pkg + " setting the subscription used for SMS as: " + subId); + } + + try { + sService.setDefaultSmsSub(subId); + } catch (RemoteException e) { + Slog.w(TAG, "warning: no cm telephony manager service"); + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/CustomTile.aidl b/sdk/src/java/cyanogenmod/app/CustomTile.aidl new file mode 100644 index 0000000..1a35ad0 --- /dev/null +++ b/sdk/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/sdk/src/java/cyanogenmod/app/CustomTile.java b/sdk/src/java/cyanogenmod/app/CustomTile.java new file mode 100644 index 0000000..dee8c37 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/CustomTile.java @@ -0,0 +1,1106 @@ +/** + * 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.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.widget.RemoteViews; +import cyanogenmod.os.Build; + +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +import java.util.ArrayList; + +/** + * 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 { + + /** Max count allowed by PseudoGridView within SystemUi **/ + public static final int PSEUDO_GRID_ITEM_MAX_COUNT = 9; + + private String resourcesPackageName = ""; + + /** + * 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. + * + * This takes priority over the onClickUri. + **/ + public PendingIntent onClick; + + /** + * An optional intent to execute when the custom tile entry is long 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. Activities will also automatically trigger + * the host panel to automatically collapse after executing the pending intent. + **/ + public PendingIntent onLongClick; + + /** + * An optional settings intent to execute when the custom tile's detail is shown + * 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 Intent onSettingsClick; + + /** + * The intent to execute when the custom tile is explicitly removed by the user. + * + * This probably shouldn't be launching an activity since several of those will be sent + * at the same time. + */ + public PendingIntent deleteIntent; + + /** + * An optional Uri to be parsed and broadcast on tile click, if an onClick pending intent + * is specified, it will take priority over the uri to be broadcasted. + **/ + 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; + + /** + * A remote icon to represent the custom tile + */ + public Bitmap remoteIcon; + + /** + * An expanded style for when the CustomTile is clicked, can either be + * a {@link GridExpandedStyle} or a {@link ListExpandedStyle} + */ + public ExpandedStyle expandedStyle; + + /** + * Boolean that forces the status bar panel to collapse when a user clicks on the + * {@link CustomTile} + * By default {@link #collapsePanel} is true + */ + public boolean collapsePanel = true; + + /** + * Indicates whether this tile has sensitive data that have to be hidden on + * secure lockscreens. + * By default {@link #sensitiveData} is false + */ + public boolean sensitiveData = false; + + /** + * Unflatten the CustomTile from a parcel. + */ + public CustomTile(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + if (parcel.readInt() != 0) { + this.onClick = PendingIntent.CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + this.onSettingsClick = Intent.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(); + } + if (parcel.readInt() != 0) { + this.expandedStyle = ExpandedStyle.CREATOR.createFromParcel(parcel); + } + this.icon = parcel.readInt(); + } + + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + this.resourcesPackageName = parcel.readString(); + this.collapsePanel = (parcel.readInt() == 1); + if (parcel.readInt() != 0) { + this.remoteIcon = Bitmap.CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + this.deleteIntent = PendingIntent.CREATOR.createFromParcel(parcel); + } + this.sensitiveData = (parcel.readInt() == 1); + } + + if (parcelableVersion >= Build.CM_VERSION_CODES.DRAGON_FRUIT) { + if (parcel.readInt() != 0) { + this.onLongClick = PendingIntent.CREATOR.createFromParcel(parcel); + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + /** + * Constructs a CustomTile object with default values. + * You might want to consider using {@link cyanogenmod.app.CustomTile.Builder} instead. + */ + public CustomTile() + { + // Empty constructor + } + + /** @hide **/ + public String getResourcesPackageName() { + return resourcesPackageName; + } + + @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 (onLongClick != null) { + b.append("onLongClick=" + onLongClick.toString() + NEW_LINE); + } + if (onSettingsClick != null) { + b.append("onSettingsClick=" + onSettingsClick.toString() + NEW_LINE); + } + if (!TextUtils.isEmpty(label)) { + b.append("label=" + label + NEW_LINE); + } + if (!TextUtils.isEmpty(contentDescription)) { + b.append("contentDescription=" + contentDescription + NEW_LINE); + } + if (expandedStyle != null) { + b.append("expandedStyle=" + expandedStyle + NEW_LINE); + } + + b.append("icon=" + icon + NEW_LINE); + b.append("resourcesPackageName=" + resourcesPackageName + NEW_LINE); + b.append("collapsePanel=" + collapsePanel + NEW_LINE); + if (remoteIcon != null) { + b.append("remoteIcon=" + remoteIcon.getGenerationId() + NEW_LINE); + } + if (deleteIntent != null) { + b.append("deleteIntent=" + deleteIntent.toString() + NEW_LINE); + } + b.append("sensitiveData=" + sensitiveData + NEW_LINE); + return b.toString(); + } + + /** + * Copy all of this into that + * @hide + */ + public void cloneInto(CustomTile that) { + that.resourcesPackageName = this.resourcesPackageName; + that.onClick = this.onClick; + that.onLongClick = this.onLongClick; + that.onSettingsClick = this.onSettingsClick; + that.onClickUri = this.onClickUri; + that.label = this.label; + that.contentDescription = this.contentDescription; + that.expandedStyle = this.expandedStyle; + that.icon = this.icon; + that.collapsePanel = this.collapsePanel; + that.remoteIcon = this.remoteIcon; + that.deleteIntent = this.deleteIntent; + that.sensitiveData = this.sensitiveData; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(out); + + // ==== APRICOT ===== + if (onClick != null) { + out.writeInt(1); + onClick.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + if (onSettingsClick != null) { + out.writeInt(1); + onSettingsClick.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); + } + if (expandedStyle != null) { + out.writeInt(1); + expandedStyle.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + out.writeInt(icon); + + // ==== BOYSENBERRY ===== + out.writeString(resourcesPackageName); + out.writeInt(collapsePanel ? 1 : 0); + if (remoteIcon != null) { + out.writeInt(1); + remoteIcon.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + if (deleteIntent != null) { + out.writeInt(1); + deleteIntent.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + out.writeInt(sensitiveData ? 1 : 0); + + // ==== DRAGONFRUIT ==== + if (onLongClick != null) { + out.writeInt(1); + onLongClick.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + /** + * An object that can apply an expanded view style to a {@link CustomTile.Builder} + * object. + */ + public static class ExpandedStyle implements Parcelable { + /** + * @hide + */ + public static final int NO_STYLE = -1; + + /** + * Identifier for a grid style expanded view + */ + public static final int GRID_STYLE = 0; + + /** + * Identifier for a list style expanded view + */ + public static final int LIST_STYLE = 1; + + /** + * Identifier for a remote view style expanded view + */ + public static final int REMOTE_STYLE = 2; + + private ExpandedStyle() { + styleId = NO_STYLE; + } + + private RemoteViews contentViews; + private ExpandedItem[] expandedItems; + private int styleId; + + private ExpandedStyle(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + if (parcel.readInt() != 0) { + expandedItems = parcel.createTypedArray(ExpandedItem.CREATOR); + } + styleId = parcel.readInt(); + } + + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + if (parcel.readInt() != 0) { + contentViews = RemoteViews.CREATOR.createFromParcel(parcel); + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + /** + * @hide + */ + public void setBuilder(Builder builder) { + if (builder != null) { + builder.setExpandedStyle(this); + } + } + + /** + * @hide + */ + protected void internalSetExpandedItems(ArrayList<? extends ExpandedItem> items) { + if (styleId == GRID_STYLE && items.size() > PSEUDO_GRID_ITEM_MAX_COUNT) { + Log.w(CustomTile.class.getName(), + "Attempted to publish greater than max grid item count"); + } + expandedItems = new ExpandedItem[items.size()]; + items.toArray(expandedItems); + } + + /** + * @hide + */ + protected void internalSetRemoteViews(RemoteViews remoteViews) { + contentViews = remoteViews; + } + + /** + * @hide + */ + protected void internalStyleId(int id) { + styleId = id; + } + + /** + * Retrieve the {@link ExpandedItem}s that have been set on this expanded style + * @return array of {@link ExpandedItem} + */ + public ExpandedItem[] getExpandedItems() { + return expandedItems; + } + + /** + * Retrieve the RemoteViews that have been set on this expanded style + * @return RemoteViews + */ + public RemoteViews getContentViews() { + return contentViews; + } + + /** + * Retrieve the style id associated with the {@link ExpandedStyle} + * @return id for style + */ + public int getStyle() { + return styleId; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(parcel); + + // ==== APRICOT ==== + if (expandedItems != null) { + parcel.writeInt(1); + parcel.writeTypedArray(expandedItems, 0); + } else { + parcel.writeInt(0); + } + parcel.writeInt(styleId); + + // ==== BOYSENBERRY ==== + if (contentViews != null) { + parcel.writeInt(1); + contentViews.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + String NEW_LINE = System.getProperty("line.separator"); + if (expandedItems != null) { + b.append("expandedItems= "+ NEW_LINE); + for (ExpandedItem item : expandedItems) { + b.append(" item=" + item.toString() + NEW_LINE); + } + } + b.append("styleId=" + styleId + NEW_LINE); + return b.toString(); + } + + /** + * Parcelable.Creator that instantiates ExpandedStyle objects + */ + public static final Creator<ExpandedStyle> CREATOR = + new Creator<ExpandedStyle>() { + public ExpandedStyle createFromParcel(Parcel in) { + return new ExpandedStyle(in); + } + + @Override + public ExpandedStyle[] newArray(int size) { + return new ExpandedStyle[size]; + } + }; + } + + /** + * An instance of {@link ExpandedStyle} that shows the {@link ExpandedGridItem}s in a + * non-scrollable grid. + */ + public static class GridExpandedStyle extends ExpandedStyle { + /** + * Constructs a GridExpandedStyle object with default values. + */ + public GridExpandedStyle() { + internalStyleId(GRID_STYLE); + } + + /** + * Sets an {@link ArrayList} of {@link ExpandedGridItem}'s to be utilized by + * the PseudoGridView for presentation. + * + * Since the PseudoGridView is not a Grid with an adapter instance, there's a hard + * limit specified by {@link #PSEUDO_GRID_ITEM_MAX_COUNT} + * @param expandedGridItems an array list of {@link ExpandedGridItem}s + */ + public void setGridItems(ArrayList<ExpandedGridItem> expandedGridItems) { + internalSetExpandedItems(expandedGridItems); + } + } + + /** + * An instance of {@link ExpandedStyle} that shows the {@link ExpandedListItem}'s in a + * scrollable ListView. + */ + public static class ListExpandedStyle extends ExpandedStyle { + /** + * Constructs a ListExpandedStyle object with default values. + */ + public ListExpandedStyle() { + internalStyleId(LIST_STYLE); + } + + /** + * Sets an {@link ArrayList} of {@link ExpandedListItem}s to be utilized by + * the ListView for presentation. + * @param expandedListItems an array list of {@link ExpandedListItem}s + */ + public void setListItems(ArrayList<ExpandedListItem> expandedListItems) { + internalSetExpandedItems(expandedListItems); + } + } + + /** + * An instance of {@link ExpandedStyle} that shows a remote view in the remote process + */ + public static class RemoteExpandedStyle extends ExpandedStyle { + /** + * Constructs a RemoteExpandedStyle object with default values. + */ + public RemoteExpandedStyle() { + internalStyleId(REMOTE_STYLE); + } + + /** + * Sets the RemoteViews for the {@link RemoteExpandedStyle} + * @param remoteViews a remote view + */ + public void setRemoteViews(RemoteViews remoteViews) { + internalSetRemoteViews(remoteViews); + } + } + + /** + * A container object that is utilized by {@link ExpandedStyle} to show specific items in either + * a PseudoGridView or a ListView via {@link GridExpandedStyle} and {@link ListExpandedStyle} + */ + public static class ExpandedItem implements Parcelable { + + /** + * A {@link PendingIntent} associated with the item. + * Triggers a {@link PendingIntent#send()} when the item is clicked. + */ + public PendingIntent onClickPendingIntent; + + /** + * A drawable resource id associated with the {@link ExpandedItem} + */ + public int itemDrawableResourceId; + + /** + * A bitmap to be utilized instead of #itemDrawableResourceId + */ + public Bitmap itemBitmapResource; + + /** + * The title of the item + */ + public String itemTitle; + + /** + * The summary associated with the item, may be null + */ + public String itemSummary = null; + + private ExpandedItem() { + // Don't want to have this baseclass be instantiable + } + + /** + * @hide + */ + protected void internalSetItemDrawable(int resourceId) { + itemDrawableResourceId = resourceId; + } + + /** + * @hide + */ + protected void internalSetItemBitmap(Bitmap bitmap) { + itemBitmapResource = bitmap; + } + + /** + * @hide + */ + protected void internalSetItemSummary(String resourceId) { + itemSummary = resourceId; + } + + /** + * @hide + */ + protected void internalSetItemTitle(String title) { + itemTitle = title; + } + + /** + * @hide + */ + protected void internalSetOnClickPendingIntent(PendingIntent pendingIntent) { + onClickPendingIntent = pendingIntent; + } + + /** + * Unflatten the ExpandedItem from a parcel. + */ + protected ExpandedItem(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + if (parcel.readInt() != 0) { + onClickPendingIntent = PendingIntent.CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + itemTitle = parcel.readString(); + } + if (parcel.readInt() != 0) { + itemSummary = parcel.readString(); + } + itemDrawableResourceId = parcel.readInt(); + } + + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + if (parcel.readInt() != 0) { + itemBitmapResource = Bitmap.CREATOR.createFromParcel(parcel); + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(out); + + // ==== APRICOT ==== + if (onClickPendingIntent != null) { + out.writeInt(1); + onClickPendingIntent.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + if (!TextUtils.isEmpty(itemTitle)) { + out.writeInt(1); + out.writeString(itemTitle); + } else { + out.writeInt(0); + } + if (!TextUtils.isEmpty(itemSummary)) { + out.writeInt(1); + out.writeString(itemSummary); + } else { + out.writeInt(0); + } + out.writeInt(itemDrawableResourceId); + + // ==== BOYSENBERRY ==== + if (itemBitmapResource != null) { + out.writeInt(1); + itemBitmapResource.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + String NEW_LINE = System.getProperty("line.separator"); + if (onClickPendingIntent != null) { + b.append("onClickPendingIntent= " + onClickPendingIntent.toString() + NEW_LINE); + } + if (itemTitle != null) { + b.append("itemTitle= " + itemTitle.toString() + NEW_LINE); + } + if (itemSummary != null) { + b.append("itemSummary= " + itemSummary.toString() + NEW_LINE); + } + b.append("itemDrawableResourceId=" + itemDrawableResourceId + NEW_LINE); + if (itemBitmapResource != null) { + b.append("itemBitmapResource=" + itemBitmapResource.getGenerationId() + NEW_LINE); + } + return b.toString(); + } + + public static final Creator<ExpandedItem> CREATOR = + new Creator<ExpandedItem>() { + @Override + public ExpandedItem createFromParcel(Parcel in) { + return new ExpandedItem(in); + } + + @Override + public ExpandedItem[] newArray(int size) { + return new ExpandedItem[size]; + } + }; + } + + /** + * An instance of {@link ExpandedItem} to be utilized within a {@link GridExpandedStyle} + */ + public static class ExpandedGridItem extends ExpandedItem { + /** + * Constructor for the ExpandedGridItem + */ + public ExpandedGridItem() { + } + + /** + * Sets the title for the {@link ExpandedGridItem} + * @param title a string title + */ + public void setExpandedGridItemTitle(String title) { + internalSetItemTitle(title); + } + + /** + * Sets the {@link PendingIntent} associated with the {@link ExpandedGridItem} + * @param intent a pending intent to be triggered on click + */ + public void setExpandedGridItemOnClickIntent(PendingIntent intent) { + internalSetOnClickPendingIntent(intent); + } + + /** + * Sets the drawable resource id associated with the {@link ExpandedGridItem} + * @param resourceId a resource id that maps to a drawable + */ + public void setExpandedGridItemDrawable(int resourceId) { + internalSetItemDrawable(resourceId); + } + + /** + * Sets the bitmap associated with the {@link ExpandedGridItem} to be utilized instead of + * the {@link ExpandedItem#itemDrawableResourceId} + * + * Note, sending many items with bitmaps over IPC may result in a + * TransactionTooLargeException. + * @param bitmap + */ + public void setExpandedGridItemBitmap(Bitmap bitmap) { + internalSetItemBitmap(bitmap); + } + } + + /** + * An instance of {@link ExpandedItem} to be utilized within a {@link ListExpandedStyle} + */ + public static class ExpandedListItem extends ExpandedItem { + /** + * Constructor fot the ExpandedListItem + */ + public ExpandedListItem() { + } + + /** + * Sets the title for the {@link ExpandedListItem} + * @param title a string title + */ + public void setExpandedListItemTitle(String title) { + internalSetItemTitle(title); + } + + /** + * Sets the title for the {@link ExpandedListItem} + * @param summary a string summary + */ + public void setExpandedListItemSummary(String summary) { + internalSetItemSummary(summary); + } + + /** + * Sets the {@link PendingIntent} associated with the {@link ExpandedListItem} + * @param intent a pending intent to be triggered on click + */ + public void setExpandedListItemOnClickIntent(PendingIntent intent) { + internalSetOnClickPendingIntent(intent); + } + + /** + * Sets the drawable resource id associated with the {@link ExpandedListItem} + * @param resourceId a resource id that maps to a drawable + */ + public void setExpandedListItemDrawable(int resourceId) { + internalSetItemDrawable(resourceId); + } + + /** + * Sets the bitmap associated with the {@link ExpandedListItem} to be utilized instead of + * the {@link ExpandedItem#itemDrawableResourceId} + * + * Note, sending many items with bitmaps over IPC may result in a + * TransactionTooLargeException. + * @param bitmap + */ + public void setExpandedListItemBitmap(Bitmap bitmap) { + internalSetItemBitmap(bitmap); + } + } + + /** + * 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) + * .setOnSettingsClickIntent(intent) + * .setOnClickUri(Uri.parse("custom uri")) + * .setIcon(R.drawable.ic_launcher) + * .build(); + * </pre> + */ + public static class Builder { + private PendingIntent mOnClick; + private PendingIntent mOnLongClick; + private Intent mOnSettingsClick; + private Uri mOnClickUri; + private String mLabel; + private String mContentDescription; + private int mIcon; + private Bitmap mRemoteIcon; + private Context mContext; + private ExpandedStyle mExpandedStyle; + private boolean mCollapsePanel = true; + private PendingIntent mDeleteIntent; + private boolean mSensitiveData = false; + + /** + * 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.app.PendingIntent} to be fired on custom tile long press. + * Note: if this is an activity, the host panel will automatically collapse. + * @param intent + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setOnLongClickIntent(PendingIntent intent) { + mOnLongClick = intent; + return this; + } + + /** + * Set a settings {@link android.content.Intent} to be fired on custom + * tile detail pane click + * @param intent + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setOnSettingsClickIntent(Intent intent) { + mOnSettingsClick = 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; + } + + /** + * Set a bitmap icon to the custom tile to be utilized instead of {@link CustomTile#icon} + * + * This will unset {@link #setIcon(int)} if utilized together. + * @see CustomTile#remoteIcon + * @param remoteIcon + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setIcon(Bitmap remoteIcon) { + mIcon = 0; // empty + mRemoteIcon = remoteIcon; + return this; + } + + /** + * Set an {@link ExpandedStyle} to to be displayed when a user clicks the custom tile + * @param expandedStyle + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setExpandedStyle(ExpandedStyle expandedStyle) { + if (mExpandedStyle != expandedStyle) { + mExpandedStyle = expandedStyle; + if (mExpandedStyle != null) { + expandedStyle.setBuilder(this); + } + } + return this; + } + + /** + * Set whether or not the Statusbar Panel should be collapsed when an + * {@link #onClick} or {@link #onClickUri} event is fired. + * @param bool + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder shouldCollapsePanel(boolean bool) { + mCollapsePanel = bool; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the custom tile is cleared explicitly + * by the user. + * + * @see CustomTile#deleteIntent + * @param intent + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder setDeleteIntent(PendingIntent intent) { + mDeleteIntent = intent; + return this; + } + + /** + * Indicates whether this tile has sensitive data that have to be hidden + * on secure lockscreens. + * @param bool + * @return {@link cyanogenmod.app.CustomTile.Builder} + */ + public Builder hasSensitiveData(boolean bool) { + mSensitiveData = bool; + return this; + } + + /** + * Create a {@link cyanogenmod.app.CustomTile} object + * @return {@link cyanogenmod.app.CustomTile} + */ + public CustomTile build() { + CustomTile tile = new CustomTile(); + tile.resourcesPackageName = mContext.getPackageName(); + tile.onClick = mOnClick; + tile.onLongClick = mOnLongClick; + tile.onSettingsClick = mOnSettingsClick; + tile.onClickUri = mOnClickUri; + tile.label = mLabel; + tile.contentDescription = mContentDescription; + tile.expandedStyle = mExpandedStyle; + tile.icon = mIcon; + tile.collapsePanel = mCollapsePanel; + tile.remoteIcon = mRemoteIcon; + tile.deleteIntent = mDeleteIntent; + tile.sensitiveData = mSensitiveData; + return tile; + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/CustomTileListenerService.java b/sdk/src/java/cyanogenmod/app/CustomTileListenerService.java new file mode 100644 index 0000000..2c8036f --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/CustomTileListenerService.java @@ -0,0 +1,232 @@ +/** + * 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 cyanogenmod.permission.BIND_CUSTOM_TILE_LISTENER_SERVICE + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> + * <pre> + * <service android:name=".CustomTileListener" + * android:label="@string/service_name" + * android:permission="cyanogenmod.permission.BIND_CUSTOM_TILE_LISTENER_SERVICE"> + * <intent-filter> + * <action android:name="cyanogenmod.app.CustomTileListenerService" /> + * </intent-filter> + * </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 (isBound()) { + return; + } + ICMStatusBarManager statusBarInterface = mStatusBarService; + if (mStatusBarService != null) { + mWrapper = new ICustomTileListenerWrapper(); + 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 (isBound()) { + ICMStatusBarManager statusBarInterface = mStatusBarService; + statusBarInterface.unregisterListener(mWrapper, mCurrentUser); + mWrapper = null; + mStatusBarService = null; + } + } + + + 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 + } + + /** + * Inform the {@link cyanogenmod.app.CMStatusBarManager} about dismissal of a single custom tile. + * <p> + * Use this if your listener has a user interface that allows the user to dismiss individual + * custom tiles, similar to the behavior of Android's status bar and notification panel. + * It should be called after the user dismisses a single custom tile using your UI; + * upon being informed, the cmstatusbar manager will actually remove the custom tile + * and you will get an {@link #onCustomTileRemoved(StatusBarPanelCustomTile)} callback. + * <P> + * + * @param pkg Package of the notifying app. + * @param tag Tag of the custom tile as specified by the notifying app + * @param id ID of the custom tile as specified by the notifying app + * <p> + */ + public final void removeCustomTile(String pkg, String tag, int id) { + if (!isBound()) return; + try { + mStatusBarService.removeCustomTileFromListener( + mWrapper, pkg, tag, id); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact cmstautusbar manager", ex); + } + } + + private boolean isBound() { + if (getStatusBarInterface() == null || mWrapper == null) { + Log.w(TAG, "CustomTile listener service not yet bound."); + return false; + } + return true; + } +} diff --git a/sdk/src/java/cyanogenmod/app/ICMStatusBarManager.aidl b/sdk/src/java/cyanogenmod/app/ICMStatusBarManager.aidl new file mode 100644 index 0000000..62283a5 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ICMStatusBarManager.aidl @@ -0,0 +1,38 @@ +/** + * 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_CUSTOM_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); + void removeCustomTileFromListener(in ICustomTileListener listener, String pkg, String tag, int id); +} diff --git a/sdk/src/java/cyanogenmod/app/ICMTelephonyManager.aidl b/sdk/src/java/cyanogenmod/app/ICMTelephonyManager.aidl new file mode 100644 index 0000000..743d61c --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ICMTelephonyManager.aidl @@ -0,0 +1,38 @@ +/** + * 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.telephony.SubscriptionInfo; + +import java.util.List; + +/** @hide */ +interface ICMTelephonyManager { + // --- Methods below are for use by 3rd party applications to manage phone and data connection + // You need the READ_MSIM_PHONE_STATE permission + List<SubscriptionInfo> getSubInformation(); + boolean isSubActive(int subId); + boolean isDataConnectionSelectedOnSub(int subId); + boolean isDataConnectionEnabled(); + + // You need the MODIFY_MSIM_PHONE_STATE permission + void setSubState(int subId, boolean state); + void setDataConnectionSelectedOnSub(int subId); + void setDataConnectionState(boolean state); + void setDefaultPhoneSub(int subId); + void setDefaultSmsSub(int subId); +} diff --git a/sdk/src/java/cyanogenmod/app/ICustomTileListener.aidl b/sdk/src/java/cyanogenmod/app/ICustomTileListener.aidl new file mode 100644 index 0000000..9f21f52 --- /dev/null +++ b/sdk/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/sdk/src/java/cyanogenmod/app/ILiveLockScreenChangeListener.aidl b/sdk/src/java/cyanogenmod/app/ILiveLockScreenChangeListener.aidl new file mode 100644 index 0000000..48e7f36 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ILiveLockScreenChangeListener.aidl @@ -0,0 +1,27 @@ +/* +** Copyright (C) 2016 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.LiveLockScreenInfo; + +/** + * Listener interface for notifying clients that the current Live lock screen has changed. + * @hide + */ +interface ILiveLockScreenChangeListener { + void onLiveLockScreenChanged(in LiveLockScreenInfo llsInfo); +}
\ No newline at end of file diff --git a/sdk/src/java/cyanogenmod/app/ILiveLockScreenManager.aidl b/sdk/src/java/cyanogenmod/app/ILiveLockScreenManager.aidl new file mode 100644 index 0000000..15142c1 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ILiveLockScreenManager.aidl @@ -0,0 +1,73 @@ +/* +** Copyright (C) 2016 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.ILiveLockScreenChangeListener; +import cyanogenmod.app.LiveLockScreenInfo; + +/** @hide */ +interface ILiveLockScreenManager { + + /** + * Enqueue a Live lock screen to be displayed. + */ + void enqueueLiveLockScreen(String pkg, int id, in LiveLockScreenInfo lls, + inout int[] idReceived, int userId); + + /** + * Cancel displaying a Live lock screen. + */ + void cancelLiveLockScreen(String pkg, int id, int userId); + + /** + * Get the current Live lock screen that should be displayed. + */ + LiveLockScreenInfo getCurrentLiveLockScreen(); + + /** + * Get the default Live lock screen. This is the Live lock screen that should be displayed + * when no other Live lock screens are queued. + */ + LiveLockScreenInfo getDefaultLiveLockScreen(); + + /** + * Set the default Live lock screen. This is the Live lock screen that should be displayed + * when no other Live lock screens are queued. + */ + void setDefaultLiveLockScreen(in LiveLockScreenInfo llsInfo); + + /** + * Set whether Live lock screen feature is enabled. + */ + oneway void setLiveLockScreenEnabled(boolean enabled); + + /** + * Get the enabled state of the Live lock screen feature. + */ + boolean getLiveLockScreenEnabled(); + + /** + * Registers an ILiveLockScreenChangeListener that will be called when the current Live lock + * screen changes. + */ + boolean registerChangeListener(in ILiveLockScreenChangeListener listener); + + /** + * Unregisters a previously registered ILiveLockScreenChangeListener. + */ + boolean unregisterChangeListener(in ILiveLockScreenChangeListener listener); +}
\ No newline at end of file diff --git a/sdk/src/java/cyanogenmod/app/ILiveLockScreenManagerProvider.aidl b/sdk/src/java/cyanogenmod/app/ILiveLockScreenManagerProvider.aidl new file mode 100644 index 0000000..933eb97 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ILiveLockScreenManagerProvider.aidl @@ -0,0 +1,65 @@ +/* +** Copyright (C) 2016 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.ILiveLockScreenChangeListener; +import cyanogenmod.app.LiveLockScreenInfo; + +/** + * Interface to be implemented by services that support the + * {@link LiveLockScreenManager#SERVICE_INTERFACE} + * @hide + */ +interface ILiveLockScreenManagerProvider { + + /** + * Enqueue a Live lock screen to be displayed. + */ + void enqueueLiveLockScreen(String pkg, int id, in LiveLockScreenInfo lls, + inout int[] idReceived, int userId); + + /** + * Cancel displaying a Live lock screen. + */ + void cancelLiveLockScreen(String pkg, int id, int userId); + + /** + * Get the current Live lock screen that should be displayed. + */ + LiveLockScreenInfo getCurrentLiveLockScreen(); + + /** + * Called when the default Live lock screen has changed. + */ + oneway void updateDefaultLiveLockScreen(in LiveLockScreenInfo llsInfo); + + /** + * Get the enabled state of the Live lock screen feature. + */ + boolean getLiveLockScreenEnabled(); + + /** + * Registers an ILiveLockScreenChangeListener that will be called when the current Live lock + * screen changes. + */ + boolean registerChangeListener(in ILiveLockScreenChangeListener listener); + + /** + * Unregisters a previously registered ILiveLockScreenChangeListener. + */ + boolean unregisterChangeListener(in ILiveLockScreenChangeListener listener); +}
\ No newline at end of file diff --git a/sdk/src/java/cyanogenmod/app/IPartnerInterface.aidl b/sdk/src/java/cyanogenmod/app/IPartnerInterface.aidl new file mode 100644 index 0000000..1b02dd1 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/IPartnerInterface.aidl @@ -0,0 +1,30 @@ +/* //device/java/android/android/app/IProfileManager.aidl +** +** 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; + +/** {@hide} */ +interface IPartnerInterface +{ + void setAirplaneModeEnabled(boolean enabled); + void setMobileDataEnabled(boolean enabled); + boolean setZenMode(int mode); + void shutdown(); + void reboot(); + String getCurrentHotwordPackageName(); + boolean setZenModeWithDuration(int mode, long durationMillis); +} diff --git a/sdk/src/java/cyanogenmod/app/IProfileManager.aidl b/sdk/src/java/cyanogenmod/app/IProfileManager.aidl new file mode 100644 index 0000000..091ba55 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/IProfileManager.aidl @@ -0,0 +1,51 @@ +/* //device/java/android/android/app/IProfileManager.aidl +** +** 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.Profile; +import android.app.NotificationGroup; +import android.os.ParcelUuid; + +/** {@hide} */ +interface IProfileManager +{ + boolean setActiveProfile(in ParcelUuid profileParcelUuid); + boolean setActiveProfileByName(String profileName); + Profile getActiveProfile(); + + boolean addProfile(in Profile profile); + boolean removeProfile(in Profile profile); + void updateProfile(in Profile profile); + + Profile getProfile(in ParcelUuid profileParcelUuid); + Profile getProfileByName(String profileName); + Profile[] getProfiles(); + boolean profileExists(in ParcelUuid profileUuid); + boolean profileExistsByName(String profileName); + boolean notificationGroupExistsByName(String notificationGroupName); + + NotificationGroup[] getNotificationGroups(); + void addNotificationGroup(in NotificationGroup group); + void removeNotificationGroup(in NotificationGroup group); + void updateNotificationGroup(in NotificationGroup group); + NotificationGroup getNotificationGroupForPackage(in String pkg); + NotificationGroup getNotificationGroup(in ParcelUuid groupParcelUuid); + + void resetAll(); + boolean isEnabled(); +} diff --git a/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.aidl b/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.aidl new file mode 100644 index 0000000..bffa3b0 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 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 LiveLockScreenInfo; diff --git a/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.java b/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.java new file mode 100644 index 0000000..5ac2220 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/LiveLockScreenInfo.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016 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.NonNull; +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; + +import cyanogenmod.os.Build; +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +/** + * Data structure defining a Live lock screen. + */ +public class LiveLockScreenInfo implements Parcelable { + + /** + * Default Live lock screen {@link #priority}. + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Lower {@link #priority}, for items that are less important. + */ + public static final int PRIORITY_LOW = -1; + + /** + * Lowest {@link #priority}. + */ + public static final int PRIORITY_MIN = -2; + + /** + * Higher {@link #priority}, for items that are more important + */ + public static final int PRIORITY_HIGH = 1; + + /** + * Highest {@link #priority}. + */ + public static final int PRIORITY_MAX = 2; + + /** + * The component, which implements + * {@link cyanogenmod.externalviews.KeyguardExternalViewProviderService}, to display for this + * live lock screen. + */ + public ComponentName component; + + /** + * Relative priority for this Live lock screen. + */ + public int priority; + + /** + * Constructs a LiveLockScreenInfo object with the given values. + * You might want to consider using {@link Builder} instead. + */ + public LiveLockScreenInfo(@NonNull ComponentName component, int priority) { + this.component = component; + this.priority = priority; + } + + /** + * Constructs a LiveLockScreenInfo object with default values. + * You might want to consider using {@link Builder} instead. + */ + public LiveLockScreenInfo() + { + this.component = null; + this.priority = PRIORITY_DEFAULT; + } + + private LiveLockScreenInfo(Parcel source) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(source); + int parcelableVersion = parcelInfo.getParcelVersion(); + + this.priority = source.readInt(); + String component = source.readString(); + this.component = !TextUtils.isEmpty(component) + ? ComponentName.unflattenFromString(component) + : null; + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + dest.writeInt(priority); + dest.writeString(component != null ? component.flattenToString() : ""); + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + return "LiveLockScreenInfo: priority=" + priority + + ", component=" + component; + } + + @Override + public LiveLockScreenInfo clone() { + LiveLockScreenInfo that = new LiveLockScreenInfo(); + cloneInto(that); + return that; + } + + /** + * Copy all (or if heavy is false, all except Bitmaps and RemoteViews) members + * of this into that. + * @hide + */ + public void cloneInto(LiveLockScreenInfo that) { + that.component = this.component.clone(); + that.priority = this.priority; + } + + public static final Parcelable.Creator<LiveLockScreenInfo> CREATOR = + new Parcelable.Creator<LiveLockScreenInfo>() { + @Override + public LiveLockScreenInfo createFromParcel(Parcel source) { + return new LiveLockScreenInfo(source); + } + + @Override + public LiveLockScreenInfo[] newArray(int size) { + return new LiveLockScreenInfo[0]; + } + }; + + /** + * Builder class for {@link LiveLockScreenInfo} objects. Provides a convenient way to set + * various fields of a {@link LiveLockScreenInfo}. + */ + public static class Builder { + private int mPriority; + private ComponentName mComponent; + + public Builder setPriority(int priority) { + if (priority < PRIORITY_MIN || priority > PRIORITY_MAX) { + throw new IllegalArgumentException("Invalid priorty given (" + priority + "): " + + PRIORITY_MIN + " <= priority <= " + PRIORITY_MIN); + } + mPriority = priority; + return this; + } + + public Builder setComponent(@NonNull ComponentName component) { + if (component == null) { + throw new IllegalArgumentException( + "Cannot call setComponent with a null component"); + } + mComponent = component; + return this; + } + + public LiveLockScreenInfo build() { + return new LiveLockScreenInfo(mComponent, mPriority); + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/LiveLockScreenManager.java b/sdk/src/java/cyanogenmod/app/LiveLockScreenManager.java new file mode 100644 index 0000000..c5fa4ce --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/LiveLockScreenManager.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016 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.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Log; + +/** + * Manages enabling/disabling Live lock screens as well as what Live lock screen to display when + * enabled. + */ +public class LiveLockScreenManager { + private static final String TAG = LiveLockScreenManager.class.getSimpleName(); + private static ILiveLockScreenManager sService; + private static LiveLockScreenManager sInstance; + + private Context mContext; + + /** + * 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.LiveLockScreenManagerService"; + + private LiveLockScreenManager(Context context) { + mContext = context; + sService = getService(); + if (context.getPackageManager().hasSystemFeature( + CMContextConstants.Features.LIVE_LOCK_SCREEN) && sService == null) { + throw new RuntimeException("Unable to get LiveLockScreenManagerService. " + + "The service either crashed, was not started, or the interface has " + + "been called to early in SystemServer init"); + } + } + + private ILiveLockScreenManager getService() { + if (sService == null) { + IBinder b = ServiceManager.getService(CMContextConstants.CM_LIVE_LOCK_SCREEN_SERVICE); + if (b != null) { + sService = ILiveLockScreenManager.Stub.asInterface(b); + } + } + + return sService; + } + + private void logServiceException(Exception e) { + Log.w(TAG, "Unable to access LiveLockScreenServiceBroker", e); + } + + public static LiveLockScreenManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new LiveLockScreenManager(context); + } + + return sInstance; + } + + /** + * Requests a Live lock screen, defined in {@param lls}, to be displayed with the given id. + * @param id An identifier for this notification unique within your application. + * @param llsInfo A {@link LiveLockScreenInfo} object describing what Live lock screen to show + * the user. + * @return True if the Live lock screen was successfully received by the backing service + */ + public boolean show(int id, @NonNull final LiveLockScreenInfo llsInfo) { + int[] idOut = new int[1]; + String pkg = mContext.getPackageName(); + boolean success = true; + try { + sService.enqueueLiveLockScreen(pkg, id, llsInfo, idOut, UserHandle.myUserId()); + if (id != idOut[0]) { + Log.w(TAG, "show: id corrupted: sent " + id + ", got back " + idOut[0]); + success = false; + } + } catch (RemoteException e) { + logServiceException(e); + success = false; + } + + return success; + } + + /** + * Cancels a previously shown Live lock screen. + * @param id An identifier for this notification unique within your application. + */ + public void cancel(int id) { + String pkg = mContext.getPackageName(); + try { + sService.cancelLiveLockScreen(pkg, id, UserHandle.myUserId()); + } catch (RemoteException e) { + logServiceException(e); + } + } + + /** + * Sets the default Live lock screen to display when no other Live lock screens are queued + * up for display. + * <p> + * This is not available to third party applications. + * </p> + */ + public void setDefaultLiveLockScreen(@Nullable LiveLockScreenInfo llsInfo) { + try { + sService.setDefaultLiveLockScreen(llsInfo); + } catch (RemoteException e) { + logServiceException(e); + } + } + + /** + * Gets the default Live lock screen that is displayed when no other Live lock screens are + * queued up for display. + * <p> + * This is not available to third party applications. + * </p> + */ + public LiveLockScreenInfo getDefaultLiveLockScreen() { + try { + return sService.getDefaultLiveLockScreen(); + } catch (RemoteException e) { + logServiceException(e); + } + + return null; + } + + /** @hide */ + public LiveLockScreenInfo getCurrentLiveLockScreen() { + LiveLockScreenInfo lls = null; + try { + lls = sService.getCurrentLiveLockScreen(); + } catch (RemoteException e) { + logServiceException(e); + } + + return lls; + } + + /** @hide */ + public boolean getLiveLockScreenEnabled() { + try { + return sService.getLiveLockScreenEnabled(); + } catch (RemoteException e) { + logServiceException(e); + } + + return false; + } + + /** @hide */ + public void setLiveLockScreenEnabled(boolean enabled) { + try { + sService.setLiveLockScreenEnabled(enabled); + } catch (RemoteException e) { + logServiceException(e); + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/PartnerInterface.java b/sdk/src/java/cyanogenmod/app/PartnerInterface.java new file mode 100644 index 0000000..a7661ff --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/PartnerInterface.java @@ -0,0 +1,262 @@ +/* + * 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.util.Log; + +/** + * <p> + * The PartnerInterface allows applications to interact with a subset of system settings. + * </p> + */ +public class PartnerInterface { + /** + * Turn off zen mode. This restores the original ring and interruption + * settings that the user had set before zen mode was enabled. + * + * @see #setZenMode + */ + public static final int ZEN_MODE_OFF = 0; + /** + * Sets zen mode to important interruptions mode, so that + * only notifications that the user has chosen in Settings + * to be of high importance will cause the user's device to notify them. + * + * This condition is held indefinitely until changed again. + * + * @see #setZenMode and #setZenModeWithDuration + */ + public static final int ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1; + /** + * Sets zen mode so that no interruptions will be allowed. The device is + * effectively silenced indefinitely, until the mode is changed again. + * + * @see #setZenMode and #setZenModeWithDuration + */ + public static final int ZEN_MODE_NO_INTERRUPTIONS = 2; + + + private static IPartnerInterface sService; + + private Context mContext; + + /** + * Allows an application to change network settings, + * such as enabling or disabling airplane mode. + */ + public static final String MODIFY_NETWORK_SETTINGS_PERMISSION = + "cyanogenmod.permission.MODIFY_NETWORK_SETTINGS"; + + /** + * Allows an application to change system sound settings, such as the zen mode. + */ + public static final String MODIFY_SOUND_SETTINGS_PERMISSION = + "cyanogenmod.permission.MODIFY_SOUND_SETTINGS"; + + private static final String TAG = "PartnerInterface"; + + private static PartnerInterface sPartnerInterfaceInstance; + + private PartnerInterface(Context context) { + Context appContext = context.getApplicationContext(); + if (appContext != null) { + mContext = appContext; + } else { + mContext = context; + } + sService = getService(); + if (context.getPackageManager().hasSystemFeature( + CMContextConstants.Features.PARTNER) && sService == null) { + throw new RuntimeException("Unable to get PartnerInterfaceService. The service" + + " either crashed, was not started, or the interface has been called to early" + + " in SystemServer init"); + } + } + + /** + * Get or create an instance of the {@link cyanogenmod.app.PartnerInterface} + * @param context + * @return {@link PartnerInterface} + */ + public static PartnerInterface getInstance(Context context) { + if (sPartnerInterfaceInstance == null) { + sPartnerInterfaceInstance = new PartnerInterface(context); + } + return sPartnerInterfaceInstance; + } + + /** @hide */ + static public IPartnerInterface getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(CMContextConstants.CM_PARTNER_INTERFACE); + if (b != null) { + sService = IPartnerInterface.Stub.asInterface(b); + return sService; + } else { + return null; + } + } + + /** + * Turns on or off airplane mode. + * + * You will need {@link #MODIFY_NETWORK_SETTINGS_PERMISSION} + * to utilize this functionality. + * @param enabled if true, sets airplane mode to enabled, otherwise disables airplane mode. + */ + public void setAirplaneModeEnabled(boolean enabled) { + if (sService == null) { + return; + } + try { + sService.setAirplaneModeEnabled(enabled); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Turns on or off mobile network. + * + * You will need {@link #MODIFY_NETWORK_SETTINGS_PERMISSION} + * to utilize this functionality. + * @param enabled if true, sets mobile network to enabled, otherwise disables mobile network. + */ + public void setMobileDataEnabled(boolean enabled) { + if (sService == null) { + return; + } + try { + sService.setMobileDataEnabled(enabled); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Set the zen mode for the device. + * + * You will need {@link #MODIFY_SOUND_SETTINGS_PERMISSION} + * to utilize this functionality. + * + * @see #ZEN_MODE_IMPORTANT_INTERRUPTIONS + * @see #ZEN_MODE_NO_INTERRUPTIONS + * @see #ZEN_MODE_OFF + * @param mode The zen mode to set the device to. + * One of {@link #ZEN_MODE_IMPORTANT_INTERRUPTIONS}, + * {@link #ZEN_MODE_NO_INTERRUPTIONS} or + * {@link #ZEN_MODE_OFF}. + */ + public boolean setZenMode(int mode) { + if (sService == null) { + return false; + } + try { + return sService.setZenMode(mode); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return false; + } + + /** + * Set the zen mode for the device, allow a duration parameter + * + * You will need {@link #MODIFY_SOUND_SETTINGS_PERMISSION} + * to utilize this functionality. + * + * @see #ZEN_MODE_IMPORTANT_INTERRUPTIONS + * @see #ZEN_MODE_NO_INTERRUPTIONS + * @param mode The zen mode to set the device to. + * One of {@link #ZEN_MODE_IMPORTANT_INTERRUPTIONS}, + * {@link #ZEN_MODE_NO_INTERRUPTIONS}. + * If using {@link #ZEN_MODE_OFF}, the duration will be ignored + * @param durationMillis The duration in milliseconds, + * if duration + System.currentTimeMillis() results in + * long overflow, duration will be "indefinitely". + * all durationMillis < 0 will mean "indefinitely". + * + */ + public boolean setZenModeWithDuration(int mode, long durationMillis) { + if (sService == null) { + return false; + } + try { + return sService.setZenModeWithDuration(mode, durationMillis); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return false; + } + + /** + * Shuts down the device, immediately. + * + * You will need {@link android.Manifest.permission.REBOOT} + * to utilize this functionality. + */ + public void shutdownDevice() { + if (sService == null) { + return; + } + try { + sService.shutdown(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Reboots the device, immediately. + * + * You will need {@link android.Manifest.permission.REBOOT} + * to utilize this functionality. + */ + public void rebootDevice() { + if (sService == null) { + return; + } + try { + sService.reboot(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Retrieves the package name of the application that currently holds the + * {@link cyanogenmod.media.MediaRecorder.AudioSource#HOTWORD} input. + * @return The package name or null if no application currently holds the HOTWORD input. + */ + public String getCurrentHotwordPackageName() { + if (sService == null) { + return null; + } + try { + return sService.getCurrentHotwordPackageName(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } +} diff --git a/sdk/src/java/cyanogenmod/app/Profile.aidl b/sdk/src/java/cyanogenmod/app/Profile.aidl new file mode 100644 index 0000000..ff6c54e --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/Profile.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 Profile; diff --git a/sdk/src/java/cyanogenmod/app/Profile.java b/sdk/src/java/cyanogenmod/app/Profile.java new file mode 100755 index 0000000..9a4666d --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/Profile.java @@ -0,0 +1,1365 @@ +/* + * 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.media.AudioManager; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.policy.IKeyguardService; +import cyanogenmod.os.Build; +import cyanogenmod.profiles.AirplaneModeSettings; +import cyanogenmod.profiles.BrightnessSettings; +import cyanogenmod.profiles.ConnectionSettings; +import cyanogenmod.profiles.LockSettings; +import cyanogenmod.profiles.RingModeSettings; +import cyanogenmod.profiles.StreamSettings; + +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +/** + * A class that represents a device profile. + * + * A {@link Profile} can serve a multitude of purposes, allowing the creator(user) + * to set overrides for streams, triggers, screen lock, brightness, various other + * settings. + */ +public final class Profile implements Parcelable, Comparable { + + private String mName; + + private int mNameResId; + + private UUID mUuid; + + private ArrayList<UUID> mSecondaryUuids = new ArrayList<UUID>(); + + private Map<UUID, ProfileGroup> profileGroups = new HashMap<UUID, ProfileGroup>(); + + private ProfileGroup mDefaultGroup; + + private boolean mStatusBarIndicator = false; + + private boolean mDirty; + + private static final String TAG = "Profile"; + + private int mProfileType; + + private Map<Integer, StreamSettings> streams = new HashMap<Integer, StreamSettings>(); + + private Map<String, ProfileTrigger> mTriggers = new HashMap<String, ProfileTrigger>(); + + private Map<Integer, ConnectionSettings> connections = new HashMap<Integer, ConnectionSettings>(); + + private Map<Integer, ConnectionSettings> networkConnectionSubIds = new HashMap<>(); + + private RingModeSettings mRingMode = new RingModeSettings(); + + private AirplaneModeSettings mAirplaneMode = new AirplaneModeSettings(); + + private BrightnessSettings mBrightness = new BrightnessSettings(); + + private LockSettings mScreenLockMode = new LockSettings(); + + private int mExpandedDesktopMode = ExpandedDesktopMode.DEFAULT; + + private int mDozeMode = DozeMode.DEFAULT; + + private int mNotificationLightMode = NotificationLightMode.DEFAULT; + + /** + * Lock modes of a device + */ + public static class LockMode { + /** Represents a default state lock mode (user choice) */ + public static final int DEFAULT = 0; + /** Represents an insecure state lock mode, where the device has no security screen */ + public static final int INSECURE = 1; + /** Represents a disabled state lock mode, where the devices lock screen can be removed */ + public static final int DISABLE = 2; + } + + /** + * Expanded desktop modes available on a device + */ + public static class ExpandedDesktopMode { + /** Represents a default state expanded desktop mode (user choice) */ + public static final int DEFAULT = 0; + /** Represents an enabled expanded desktop mode */ + public static final int ENABLE = 1; + /** Represents a disabled expanded desktop mode */ + public static final int DISABLE = 2; + } + + /** + * Doze modes available on a device + */ + public static class DozeMode { + /** Represents a default Doze mode (user choice) */ + public static final int DEFAULT = 0; + /** Represents an enabled Doze mode */ + public static final int ENABLE = 1; + /** Represents an disabled Doze mode */ + public static final int DISABLE = 2; + } + + /** + * Notification light modes available on a device + */ + public static class NotificationLightMode { + /** Represents a default Notification light mode (user choice) */ + public static final int DEFAULT = 0; + /** Represents an enabled Notification light mode */ + public static final int ENABLE = 1; + /** Represents a disabled Notification light mode */ + public static final int DISABLE = 2; + } + + /** + * Available trigger types on the device, usually hardware + */ + public static class TriggerType { + /** Represents a WiFi trigger type */ + public static final int WIFI = 0; + /** Represents a Bluetooth trigger type */ + public static final int BLUETOOTH = 1; + } + + /** + * Various trigger states associated with a {@link TriggerType} + */ + public static class TriggerState { + /** A {@link TriggerState) for when the {@link TriggerType} connects */ + public static final int ON_CONNECT = 0; + /** A {@link TriggerState) for when the {@link TriggerType} disconnects */ + public static final int ON_DISCONNECT = 1; + /** A {@link TriggerState) for when the {@link TriggerType} is disabled */ + public static final int DISABLED = 2; + /** + * A {@link TriggerState) for when the {@link TriggerType#BLUETOOTH} + * connects for A2DP session + */ + public static final int ON_A2DP_CONNECT = 3; + /** + * A {@link TriggerState) for when the {@link TriggerType#BLUETOOTH} + * disconnects from A2DP session + */ + public static final int ON_A2DP_DISCONNECT = 4; + } + + /** + * A {@link Profile} type + */ + public static class Type { + /** Profile type which represents a toggle {@link Profile} */ + public static final int TOGGLE = 0; + /** Profile type which represents a conditional {@link Profile} */ + public static final int CONDITIONAL = 1; + } + + /** + * A {@link ProfileTrigger} is a {@link TriggerType} which can be queried from the OS + */ + public static class ProfileTrigger implements Parcelable { + private int mType; + private String mId; + private String mName; + private int mState; + + + /** + * Construct a {@link ProfileTrigger} based on its type {@link Profile.TriggerType} and if + * the trigger should fire on a {@link Profile.TriggerState} change. + * + * Example: + * <pre class="prettyprint"> + * triggerId = trigger.getSSID(); // Use the AP's SSID as identifier + * triggerName = trigger.getTitle(); // Use the AP's name as the trigger name + * triggerType = Profile.TriggerType.WIFI; // This is a wifi trigger + * triggerState = Profile.TriggerState.ON_CONNECT; // On Connect of this, trigger + * + * Profile.ProfileTrigger profileTrigger = + * new Profile.ProfileTrigger(triggerType, triggerId, triggerState, triggerName); + * </pre> + * + * @param type a {@link Profile.TriggerType} + * @param id an identifier for the ProfileTrigger + * @param state {@link Profile.TriggerState} depending on the TriggerType + * @param name an identifying name for the ProfileTrigger + */ + public ProfileTrigger(int type, String id, int state, String name) { + mType = type; + mId = id; + mState = state; + mName = name; + } + + private ProfileTrigger(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + mType = in.readInt(); + mId = in.readString(); + mState = in.readInt(); + mName = in.readString(); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + dest.writeInt(mType); + dest.writeString(mId); + dest.writeInt(mState); + dest.writeString(mName); + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Get the {@link ProfileTrigger} {@link TriggerType} + * @return {@link TriggerType} + */ + public int getType() { + return mType; + } + + /** + * Get the name associated with the {@link ProfileTrigger} + * @return a string name + */ + public String getName() { + return mName; + } + + /** + * Get the id associated with the {@link ProfileTrigger} + * @return an string identifier + */ + public String getId() { + return mId; + } + + /** + * Get the state associated with the {@link ProfileTrigger} + * @return an integer indicating the state + */ + public int getState() { + return mState; + } + + /** + * @hide + */ + public void getXmlString(StringBuilder builder, Context context) { + final String itemType = mType == TriggerType.WIFI ? "wifiAP" : "btDevice"; + + builder.append("<"); + builder.append(itemType); + builder.append(" "); + builder.append(getIdType(mType)); + builder.append("=\""); + builder.append(mId); + builder.append("\" state=\""); + builder.append(mState); + builder.append("\" name=\""); + builder.append(mName); + builder.append("\"></"); + builder.append(itemType); + builder.append(">\n"); + } + + /** + * @hide + */ + public static ProfileTrigger fromXml(XmlPullParser xpp, Context context) { + final String name = xpp.getName(); + final int type; + + if (name.equals("wifiAP")) { + type = TriggerType.WIFI; + } else if (name.equals("btDevice")) { + type = TriggerType.BLUETOOTH; + } else { + return null; + } + + String id = xpp.getAttributeValue(null, getIdType(type)); + int state = Integer.valueOf(xpp.getAttributeValue(null, "state")); + String triggerName = xpp.getAttributeValue(null, "name"); + if (triggerName == null) { + triggerName = id; + } + + return new ProfileTrigger(type, id, state, triggerName); + } + + private static String getIdType(int type) { + return type == TriggerType.WIFI ? "ssid" : "address"; + } + + /** + * @hide + */ + public static final Parcelable.Creator<ProfileTrigger> CREATOR + = new Parcelable.Creator<ProfileTrigger>() { + public ProfileTrigger createFromParcel(Parcel in) { + return new ProfileTrigger(in); + } + + @Override + public ProfileTrigger[] newArray(int size) { + return new ProfileTrigger[size]; + } + }; + } + + /** @hide */ + public static final Parcelable.Creator<Profile> CREATOR = new Parcelable.Creator<Profile>() { + public Profile createFromParcel(Parcel in) { + return new Profile(in); + } + + @Override + public Profile[] newArray(int size) { + return new Profile[size]; + } + }; + + public Profile(String name) { + this(name, -1, UUID.randomUUID()); + } + + /** @hide */ + public Profile(String name, int nameResId, UUID uuid) { + mName = name; + mNameResId = nameResId; + mUuid = uuid; + mProfileType = Type.TOGGLE; //Default to toggle type + mDirty = false; + } + + private Profile(Parcel in) { + readFromParcel(in); + } + + /** + * Get the {@link TriggerState} for a {@link ProfileTrigger} with a given id + * @param type {@link TriggerType} + * @param id string id of {@link ProfileTrigger} + * @return {@link TriggerState} + */ + public int getTriggerState(int type, String id) { + ProfileTrigger trigger = id != null ? mTriggers.get(id) : null; + if (trigger != null) { + return trigger.mState; + } + return TriggerState.DISABLED; + } + + /** + * Get all the {@link ProfileTrigger}s for a given {@link TriggerType} + * @param type {@link TriggerType} + * @return an array list of {@link ProfileTrigger}s + */ + public ArrayList<ProfileTrigger> getTriggersFromType(int type) { + ArrayList<ProfileTrigger> result = new ArrayList<ProfileTrigger>(); + for (Entry<String, ProfileTrigger> profileTrigger: mTriggers.entrySet()) { + ProfileTrigger trigger = profileTrigger.getValue(); + if (trigger.getType() == type) { + result.add(trigger); + } + } + return result; + } + + /** + * Set a custom {@link ProfileTrigger} + * @hide + */ + public void setTrigger(int type, String id, int state, String name) { + if (id == null + || type < TriggerType.WIFI || type > TriggerType.BLUETOOTH + || state < TriggerState.ON_CONNECT || state > TriggerState.ON_A2DP_DISCONNECT) { + return; + } + + ProfileTrigger trigger = mTriggers.get(id); + + if (state == TriggerState.DISABLED) { + if (trigger != null) { + mTriggers.remove(id); + } + } else if (trigger != null) { + trigger.mState = state; + } else { + mTriggers.put(id, new ProfileTrigger(type, id, state, name)); + } + + mDirty = true; + } + + /** + * Set a {@link ProfileTrigger} on the {@link Profile} + * @param trigger a {@link ProfileTrigger} + */ + public void setTrigger(ProfileTrigger trigger) { + setTrigger(trigger.getType(), trigger.getId(), trigger.getState(), trigger.getName()); + } + + public int compareTo(Object obj) { + Profile tmp = (Profile) obj; + if (mName.compareTo(tmp.mName) < 0) { + return -1; + } else if (mName.compareTo(tmp.mName) > 0) { + return 1; + } + return 0; + } + + /** + * Add a {@link ProfileGroup} to the {@link Profile} + * @param profileGroup + * @hide + */ + public void addProfileGroup(ProfileGroup profileGroup) { + if (profileGroup == null) { + return; + } + + if (profileGroup.isDefaultGroup()) { + /* we must not have more than one default group */ + if (mDefaultGroup != null) { + return; + } + mDefaultGroup = profileGroup; + } + profileGroups.put(profileGroup.getUuid(), profileGroup); + mDirty = true; + } + + /** + * Remove a {@link ProfileGroup} with a given {@link UUID} + * @param uuid + * @hide + */ + public void removeProfileGroup(UUID uuid) { + if (!profileGroups.get(uuid).isDefaultGroup()) { + profileGroups.remove(uuid); + } else { + Log.e(TAG, "Cannot remove default group: " + uuid); + } + } + + /** + * Get {@link ProfileGroup}s associated with the {@link Profile} + * @return {@link ProfileGroup[]} + * @hide + */ + public ProfileGroup[] getProfileGroups() { + return profileGroups.values().toArray(new ProfileGroup[profileGroups.size()]); + } + + /** + * Get a {@link ProfileGroup} with a given {@link UUID} + * @param uuid + * @return a {@link ProfileGroup} + * @hide + */ + public ProfileGroup getProfileGroup(UUID uuid) { + return profileGroups.get(uuid); + } + + /** + * Get the default {@link ProfileGroup} associated with the {@link Profile} + * @return the default {@link ProfileGroup} + * @hide + */ + public ProfileGroup getDefaultGroup() { + return mDefaultGroup; + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // === BOYSENBERRY === + if (!TextUtils.isEmpty(mName)) { + dest.writeInt(1); + dest.writeString(mName); + } else { + dest.writeInt(0); + } + if (mNameResId != 0) { + dest.writeInt(1); + dest.writeInt(mNameResId); + } else { + dest.writeInt(0); + } + if (mUuid != null) { + dest.writeInt(1); + new ParcelUuid(mUuid).writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSecondaryUuids != null && !mSecondaryUuids.isEmpty()) { + ArrayList<ParcelUuid> uuids = new ArrayList<ParcelUuid>(mSecondaryUuids.size()); + for (UUID u : mSecondaryUuids) { + uuids.add(new ParcelUuid(u)); + } + dest.writeInt(1); + dest.writeParcelableArray(uuids.toArray(new Parcelable[uuids.size()]), flags); + } else { + dest.writeInt(0); + } + dest.writeInt(mStatusBarIndicator ? 1 : 0); + dest.writeInt(mProfileType); + dest.writeInt(mDirty ? 1 : 0); + if (profileGroups != null && !profileGroups.isEmpty()) { + dest.writeInt(1); + dest.writeTypedArray(profileGroups.values().toArray( + new ProfileGroup[0]), flags); + } else { + dest.writeInt(0); + } + if (streams != null && !streams.isEmpty()) { + dest.writeInt(1); + dest.writeTypedArray(streams.values().toArray( + new StreamSettings[0]), flags); + } else { + dest.writeInt(0); + } + if (connections != null && !connections.isEmpty()) { + dest.writeInt(1); + dest.writeTypedArray(connections.values().toArray( + new ConnectionSettings[0]), flags); + } else { + dest.writeInt(0); + } + if (mRingMode != null) { + dest.writeInt(1); + mRingMode.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mAirplaneMode != null) { + dest.writeInt(1); + mAirplaneMode.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mBrightness != null) { + dest.writeInt(1); + mBrightness.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mScreenLockMode != null) { + dest.writeInt(1); + mScreenLockMode.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + dest.writeTypedArray(mTriggers.values().toArray(new ProfileTrigger[0]), flags); + dest.writeInt(mExpandedDesktopMode); + dest.writeInt(mDozeMode); + + // === ELDERBERRY === + dest.writeInt(mNotificationLightMode); + + if (networkConnectionSubIds != null && !networkConnectionSubIds.isEmpty()) { + dest.writeInt(1); + dest.writeTypedArray(networkConnectionSubIds.values().toArray( + new ConnectionSettings[0]), flags); + } else { + dest.writeInt(0); + } + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + /** @hide */ + public void readFromParcel(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + if (in.readInt() != 0) { + mName = in.readString(); + } + if (in.readInt() != 0) { + mNameResId = in.readInt(); + } + if (in.readInt() != 0) { + mUuid = ParcelUuid.CREATOR.createFromParcel(in).getUuid(); + } + if (in.readInt() != 0) { + for (Parcelable parcel : in.readParcelableArray(null)) { + ParcelUuid u = (ParcelUuid) parcel; + mSecondaryUuids.add(u.getUuid()); + } + } + mStatusBarIndicator = (in.readInt() == 1); + mProfileType = in.readInt(); + mDirty = (in.readInt() == 1); + if (in.readInt() != 0) { + for (ProfileGroup group : in.createTypedArray(ProfileGroup.CREATOR)) { + profileGroups.put(group.getUuid(), group); + if (group.isDefaultGroup()) { + mDefaultGroup = group; + } + } + } + if (in.readInt() != 0) { + for (StreamSettings stream : in.createTypedArray(StreamSettings.CREATOR)) { + streams.put(stream.getStreamId(), stream); + } + } + if (in.readInt() != 0) { + for (ConnectionSettings connection : + in.createTypedArray(ConnectionSettings.CREATOR)) { + connections.put(connection.getConnectionId(), connection); + } + } + if (in.readInt() != 0) { + mRingMode = RingModeSettings.CREATOR.createFromParcel(in); + } + if (in.readInt() != 0) { + mAirplaneMode = AirplaneModeSettings.CREATOR.createFromParcel(in); + } + if (in.readInt() != 0) { + mBrightness = BrightnessSettings.CREATOR.createFromParcel(in); + } + if (in.readInt() != 0) { + mScreenLockMode = LockSettings.CREATOR.createFromParcel(in); + } + for (ProfileTrigger trigger : in.createTypedArray(ProfileTrigger.CREATOR)) { + mTriggers.put(trigger.mId, trigger); + } + mExpandedDesktopMode = in.readInt(); + mDozeMode = in.readInt(); + } + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mNotificationLightMode = in.readInt(); + if (in.readInt() != 0) { + for (ConnectionSettings connection : + in.createTypedArray(ConnectionSettings.CREATOR)) { + // elderberry can do msim connections + networkConnectionSubIds.put(connection.getSubId(), connection); + } + } + } + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + /** + * Get the name associated with the {@link Profile} + * @return a string name of the profile + */ + public String getName() { + return mName; + } + + /** + * Set a name for the {@link Profile} + * @param name a string for the {@link Profile} + */ + public void setName(String name) { + mName = name; + mNameResId = -1; + mDirty = true; + } + + /** + * Get the {@link Type} of the {@link Profile} + * @return + */ + public int getProfileType() { + return mProfileType; + } + + /** + * Set the {@link Type} for the {@link Profile} + * @param type a type of profile + */ + public void setProfileType(int type) { + mProfileType = type; + mDirty = true; + } + + /** + * Get the {@link UUID} associated with the {@link Profile} + * @return the uuid for the profile + */ + public UUID getUuid() { + if (this.mUuid == null) this.mUuid = UUID.randomUUID(); + return this.mUuid; + } + + /** + * Get the secondary {@link UUID}s for the {@link Profile} + * @return the secondary uuids for the Profile + */ + public UUID[] getSecondaryUuids() { + return mSecondaryUuids.toArray(new UUID[mSecondaryUuids.size()]); + } + + /** + * Set a list of secondary {@link UUID}s for the {@link Profile} + * @param uuids + */ + public void setSecondaryUuids(List<UUID> uuids) { + mSecondaryUuids.clear(); + if (uuids != null) { + mSecondaryUuids.addAll(uuids); + mDirty = true; + } + } + + /** + * Add a secondary {@link UUID} to the {@link Profile} + * @param uuid + */ + public void addSecondaryUuid(UUID uuid) { + if (uuid != null) { + mSecondaryUuids.add(uuid); + mDirty = true; + } + } + + /** + * @hide + */ + public boolean getStatusBarIndicator() { + return mStatusBarIndicator; + } + + /** + * @hide + */ + public void setStatusBarIndicator(boolean newStatusBarIndicator) { + mStatusBarIndicator = newStatusBarIndicator; + mDirty = true; + } + + /** + * Check if the given {@link Profile} is a {@link Type#CONDITIONAL} + * @return true if conditional + */ + public boolean isConditionalType() { + return(mProfileType == Type.CONDITIONAL ? true : false); + } + + /** + * @hide + */ + public void setConditionalType() { + mProfileType = Type.CONDITIONAL; + mDirty = true; + } + + /** + * Get the {@link RingModeSettings} for the {@link Profile} + * @return + */ + public RingModeSettings getRingMode() { + return mRingMode; + } + + /** + * Set the {@link RingModeSettings} for the {@link Profile} + * @param descriptor + */ + public void setRingMode(RingModeSettings descriptor) { + mRingMode = descriptor; + mDirty = true; + } + + /** + * Get the {@link LockSettings} for the {@link Profile} + * @return + */ + public LockSettings getScreenLockMode() { + return mScreenLockMode; + } + + /** + * Set the {@link LockSettings} for the {@link Profile} + * @param screenLockMode + */ + public void setScreenLockMode(LockSettings screenLockMode) { + mScreenLockMode = screenLockMode; + mDirty = true; + } + + /** + * Get the {@link ExpandedDesktopMode} for the {@link Profile} + * @return + */ + public int getExpandedDesktopMode() { + return mExpandedDesktopMode; + } + + /** + * Set the {@link ExpandedDesktopMode} for the {@link Profile} + * @return + */ + public void setExpandedDesktopMode(int expandedDesktopMode) { + if (expandedDesktopMode < ExpandedDesktopMode.DEFAULT + || expandedDesktopMode > ExpandedDesktopMode.DISABLE) { + mExpandedDesktopMode = ExpandedDesktopMode.DEFAULT; + } else { + mExpandedDesktopMode = expandedDesktopMode; + } + mDirty = true; + } + + /** + * Get the {@link DozeMode} associated with the {@link Profile} + * @return + */ + public int getDozeMode() { + return mDozeMode; + } + + /** + * Set the {@link DozeMode} associated with the {@link Profile} + * @return + */ + public void setDozeMode(int dozeMode) { + if (dozeMode < DozeMode.DEFAULT + || dozeMode > DozeMode.DISABLE) { + mDozeMode = DozeMode.DEFAULT; + } else { + mDozeMode = dozeMode; + } + mDirty = true; + } + + /** + * Get the {@link NotificationLightMode} associated with the {@link Profile} + * @return + */ + public int getNotificationLightMode() { + return mNotificationLightMode; + } + + /** + * Set the {@link NotificationLightMode} associated with the {@link Profile} + * @return + */ + public void setNotificationLightMode(int notificationLightMode) { + if (notificationLightMode < NotificationLightMode.DEFAULT + || notificationLightMode > NotificationLightMode.DISABLE) { + mNotificationLightMode = NotificationLightMode.DEFAULT; + } else { + mNotificationLightMode = notificationLightMode; + } + mDirty = true; + } + + /** + * Get the {@link AirplaneModeSettings} associated with the {@link Profile} + * @return + */ + public AirplaneModeSettings getAirplaneMode() { + return mAirplaneMode; + } + + /** + * Set the {@link AirplaneModeSettings} associated with the {@link Profile} + * @param descriptor + */ + public void setAirplaneMode(AirplaneModeSettings descriptor) { + mAirplaneMode = descriptor; + mDirty = true; + } + + /** + * Get the {@link BrightnessSettings} associated with the {@link Profile} + * @return + */ + public BrightnessSettings getBrightness() { + return mBrightness; + } + + /** + * Set the {@link BrightnessSettings} associated with the {@link Profile} + * @return + */ + public void setBrightness(BrightnessSettings descriptor) { + mBrightness = descriptor; + mDirty = true; + } + + /** @hide */ + public boolean isDirty() { + if (mDirty) { + return true; + } + for (ProfileGroup group : profileGroups.values()) { + if (group.isDirty()) { + return true; + } + } + for (StreamSettings stream : streams.values()) { + if (stream.isDirty()) { + return true; + } + } + for (ConnectionSettings conn : connections.values()) { + if (conn.isDirty()) { + return true; + } + } + for (ConnectionSettings conn : networkConnectionSubIds.values()) { + if (conn.isDirty()) { + return true; + } + } + if (mRingMode.isDirty()) { + return true; + } + if (mAirplaneMode.isDirty()) { + return true; + } + if (mBrightness.isDirty()) { + return true; + } + return false; + } + + /** @hide */ + public void getXmlString(StringBuilder builder, Context context) { + builder.append("<profile "); + if (mNameResId > 0) { + builder.append("nameres=\""); + builder.append(context.getResources().getResourceEntryName(mNameResId)); + } else { + builder.append("name=\""); + builder.append(TextUtils.htmlEncode(getName())); + } + builder.append("\" uuid=\""); + builder.append(TextUtils.htmlEncode(getUuid().toString())); + builder.append("\">\n"); + + builder.append("<uuids>"); + for (UUID u : mSecondaryUuids) { + builder.append("<uuid>"); + builder.append(TextUtils.htmlEncode(u.toString())); + builder.append("</uuid>"); + } + builder.append("</uuids>\n"); + + builder.append("<profiletype>"); + builder.append(getProfileType() == Type.TOGGLE ? "toggle" : "conditional"); + builder.append("</profiletype>\n"); + + builder.append("<statusbar>"); + builder.append(getStatusBarIndicator() ? "yes" : "no"); + builder.append("</statusbar>\n"); + + if (mScreenLockMode != null) { + builder.append("<screen-lock-mode>"); + mScreenLockMode.writeXmlString(builder, context); + builder.append("</screen-lock-mode>\n"); + } + + builder.append("<expanded-desktop-mode>"); + builder.append(mExpandedDesktopMode); + builder.append("</expanded-desktop-mode>\n"); + + builder.append("<doze-mode>"); + builder.append(mDozeMode); + builder.append("</doze-mode>\n"); + + builder.append("<notification-light-mode>"); + builder.append(mNotificationLightMode); + builder.append("</notification-light-mode>\n"); + + mAirplaneMode.getXmlString(builder, context); + + mBrightness.getXmlString(builder, context); + + mRingMode.getXmlString(builder, context); + + for (ProfileGroup pGroup : profileGroups.values()) { + pGroup.getXmlString(builder, context); + } + for (StreamSettings sd : streams.values()) { + sd.getXmlString(builder, context); + } + for (ConnectionSettings cs : connections.values()) { + cs.getXmlString(builder, context); + } + for (ConnectionSettings cs : networkConnectionSubIds.values()) { + cs.getXmlString(builder, context); + } + if (!mTriggers.isEmpty()) { + builder.append("<triggers>\n"); + for (ProfileTrigger trigger : mTriggers.values()) { + trigger.getXmlString(builder, context); + } + builder.append("</triggers>\n"); + } + + builder.append("</profile>\n"); + mDirty = false; + } + + private static List<UUID> readSecondaryUuidsFromXml(XmlPullParser xpp, Context context) + throws XmlPullParserException, + IOException { + ArrayList<UUID> uuids = new ArrayList<UUID>(); + int event = xpp.next(); + while (event != XmlPullParser.END_TAG || !xpp.getName().equals("uuids")) { + if (event == XmlPullParser.START_TAG) { + String name = xpp.getName(); + if (name.equals("uuid")) { + try { + uuids.add(UUID.fromString(xpp.nextText())); + } catch (NullPointerException e) { + Log.w(TAG, "Null Pointer - invalid UUID"); + } catch (IllegalArgumentException e) { + Log.w(TAG, "UUID not recognized"); + } + } + } + event = xpp.next(); + } + return uuids; + } + + private static void readTriggersFromXml(XmlPullParser xpp, Context context, Profile profile) + throws XmlPullParserException, IOException { + int event = xpp.next(); + while (event != XmlPullParser.END_TAG || !xpp.getName().equals("triggers")) { + if (event == XmlPullParser.START_TAG) { + ProfileTrigger trigger = ProfileTrigger.fromXml(xpp, context); + if (trigger != null) { + profile.mTriggers.put(trigger.mId, trigger); + } + } else if (event == XmlPullParser.END_DOCUMENT) { + throw new IOException("Premature end of file while parsing triggers"); + } + event = xpp.next(); + } + } + + /** @hide */ + public void validateRingtones(Context context) { + for (ProfileGroup pg : profileGroups.values()) { + pg.validateOverrideUris(context); + } + } + + /** @hide */ + public static Profile fromXml(XmlPullParser xpp, Context context) + throws XmlPullParserException, IOException { + String value = xpp.getAttributeValue(null, "nameres"); + int profileNameResId = -1; + String profileName = null; + + if (value != null) { + profileNameResId = context.getResources().getIdentifier(value, "string", + "cyanogenmod.platform"); + if (profileNameResId > 0) { + profileName = context.getResources().getString(profileNameResId); + } + } + + if (profileName == null) { + profileName = xpp.getAttributeValue(null, "name"); + } + + UUID profileUuid = UUID.randomUUID(); + try { + profileUuid = UUID.fromString(xpp.getAttributeValue(null, "uuid")); + } catch (NullPointerException e) { + Log.w(TAG, + "Null Pointer - UUID not found for " + + profileName + + ". New UUID generated: " + + profileUuid.toString() + ); + } catch (IllegalArgumentException e) { + Log.w(TAG, + "UUID not recognized for " + + profileName + + ". New UUID generated: " + + profileUuid.toString() + ); + } + + Profile profile = new Profile(profileName, profileNameResId, profileUuid); + int event = xpp.next(); + while (event != XmlPullParser.END_TAG) { + if (event == XmlPullParser.START_TAG) { + String name = xpp.getName(); + if (name.equals("uuids")) { + profile.setSecondaryUuids(readSecondaryUuidsFromXml(xpp, context)); + } + if (name.equals("statusbar")) { + profile.setStatusBarIndicator(xpp.nextText().equals("yes")); + } + if (name.equals("profiletype")) { + profile.setProfileType(xpp.nextText().equals("toggle") + ? Type.TOGGLE : Type.CONDITIONAL); + } + if (name.equals("ringModeDescriptor")) { + RingModeSettings smd = RingModeSettings.fromXml(xpp, context); + profile.setRingMode(smd); + } + if (name.equals("airplaneModeDescriptor")) { + AirplaneModeSettings amd = AirplaneModeSettings.fromXml(xpp, context); + profile.setAirplaneMode(amd); + } + if (name.equals("brightnessDescriptor")) { + BrightnessSettings bd = BrightnessSettings.fromXml(xpp, context); + profile.setBrightness(bd); + } + if (name.equals("screen-lock-mode")) { + LockSettings lockMode = new LockSettings(Integer.valueOf(xpp.nextText())); + profile.setScreenLockMode(lockMode); + } + if (name.equals("expanded-desktop-mode")) { + profile.setExpandedDesktopMode(Integer.valueOf(xpp.nextText())); + } + if (name.equals("doze-mode")) { + profile.setDozeMode(Integer.valueOf(xpp.nextText())); + } + if (name.equals("notification-light-mode")) { + profile.setNotificationLightMode(Integer.valueOf(xpp.nextText())); + } + if (name.equals("profileGroup")) { + ProfileGroup pg = ProfileGroup.fromXml(xpp, context); + profile.addProfileGroup(pg); + } + if (name.equals("streamDescriptor")) { + StreamSettings sd = StreamSettings.fromXml(xpp, context); + profile.setStreamSettings(sd); + } + if (name.equals("connectionDescriptor")) { + ConnectionSettings cs = ConnectionSettings.fromXml(xpp, context); + if (Build.CM_VERSION.SDK_INT >= Build.CM_VERSION_CODES.ELDERBERRY + && cs.getConnectionId() == ConnectionSettings.PROFILE_CONNECTION_2G3G4G) { + profile.networkConnectionSubIds.put(cs.getSubId(), cs); + } else { + profile.connections.put(cs.getConnectionId(), cs); + } + } + if (name.equals("triggers")) { + readTriggersFromXml(xpp, context, profile); + } + } else if (event == XmlPullParser.END_DOCUMENT) { + throw new IOException("Premature end of file while parsing profle:" + profileName); + } + event = xpp.next(); + } + + /* we just loaded from XML, so nothing needs saving */ + profile.mDirty = false; + + return profile; + } + + /** @hide */ + public void doSelect(Context context, IKeyguardService keyguardService) { + // Set stream volumes + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + for (StreamSettings sd : streams.values()) { + if (sd.isOverride()) { + am.setStreamVolume(sd.getStreamId(), sd.getValue(), 0); + } + } + // Set connections + for (ConnectionSettings cs : connections.values()) { + if (cs.isOverride()) { + cs.processOverride(context); + } + } + for (ConnectionSettings cs : networkConnectionSubIds.values()) { + if (cs.isOverride()) { + cs.processOverride(context); + } + } + + // Set ring mode + mRingMode.processOverride(context); + // Set airplane mode + mAirplaneMode.processOverride(context); + + // Set brightness + mBrightness.processOverride(context); + + if (keyguardService != null) { + // Set lock screen mode + mScreenLockMode.processOverride(context, keyguardService); + } else { + Log.e(TAG, "cannot process screen lock override without a keyguard service."); + } + + // Set expanded desktop + // if (mExpandedDesktopMode != ExpandedDesktopMode.DEFAULT) { + // Settings.System.putIntForUser(context.getContentResolver(), + // Settings.System.EXPANDED_DESKTOP_STATE, + // mExpandedDesktopMode == ExpandedDesktopMode.ENABLE ? 1 : 0, + // UserHandle.USER_CURRENT); + // } + + // Set doze mode + if (mDozeMode != DozeMode.DEFAULT) { + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.DOZE_ENABLED, + mDozeMode == DozeMode.ENABLE ? 1 : 0, + UserHandle.USER_CURRENT); + } + + // Set notification light mode + if (mNotificationLightMode != NotificationLightMode.DEFAULT) { + Settings.System.putIntForUser(context.getContentResolver(), + Settings.System.NOTIFICATION_LIGHT_PULSE, + mNotificationLightMode == NotificationLightMode.ENABLE ? 1 : 0, + UserHandle.USER_CURRENT); + } + } + + /** + * Get the settings for a stream id in the {@link Profile} + * @return {@link StreamSettings} + */ + public StreamSettings getSettingsForStream(int streamId){ + return streams.get(streamId); + } + + /** + * Set the {@link StreamSettings} for the {@link Profile} + * @param descriptor + */ + public void setStreamSettings(StreamSettings descriptor){ + streams.put(descriptor.getStreamId(), descriptor); + mDirty = true; + } + + /** + * Get the {@link StreamSettings} for the {@link Profile} + * @return {@link Collection<StreamSettings>} + */ + public Collection<StreamSettings> getStreamSettings(){ + return streams.values(); + } + + /** + * Get the settings for a connection id in the {@link Profile} + * @return {@link ConnectionSettings} + */ + public ConnectionSettings getSettingsForConnection(int connectionId){ + if (connectionId == ConnectionSettings.PROFILE_CONNECTION_2G3G4G) { + if (networkConnectionSubIds.size() > 1) { + throw new UnsupportedOperationException("Use getConnectionSettingsWithSubId for MSIM devices!"); + } else { + return networkConnectionSubIds.values().iterator().next(); + } + } + return connections.get(connectionId); + } + + /** + * Get the settings for a {@link ConnectionSettings#PROFILE_CONNECTION_2G3G4G} by sub id. + * + * @param subId the sub id to lookup. Can be {@link android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID} + * @return {@link ConnectionSettings} + */ + public ConnectionSettings getConnectionSettingWithSubId(int subId) { + return networkConnectionSubIds.get(subId); + } + + /** + * Set the {@link ConnectionSettings} for the {@link Profile} + * @param descriptor + */ + public void setConnectionSettings(ConnectionSettings descriptor) { + if (descriptor.getConnectionId() == ConnectionSettings.PROFILE_CONNECTION_2G3G4G) { + networkConnectionSubIds.put(descriptor.getSubId(), descriptor); + } else { + connections.put(descriptor.getConnectionId(), descriptor); + } + mDirty = true; + } + + /** + * Get the {@link ConnectionSettings} for the {@link Profile} + * @return {@link Collection<ConnectionSettings>} + */ + public Collection<ConnectionSettings> getConnectionSettings(){ + List<ConnectionSettings> combinedList = new ArrayList<>(); + combinedList.addAll(connections.values()); + combinedList.addAll(networkConnectionSubIds.values()); + return combinedList; + } +} diff --git a/sdk/src/java/cyanogenmod/app/ProfileGroup.java b/sdk/src/java/cyanogenmod/app/ProfileGroup.java new file mode 100644 index 0000000..56ec507 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ProfileGroup.java @@ -0,0 +1,395 @@ +/* + * 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.Notification; +import android.app.NotificationGroup; + +import android.content.Context; +import android.database.Cursor; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ParcelUuid; +import android.text.TextUtils; +import android.util.Log; + +import cyanogenmod.os.Build; + +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +import java.io.IOException; +import java.util.UUID; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * @hide + * TODO: This isn't ready for public use + */ +public final class ProfileGroup implements Parcelable { + private static final String TAG = "ProfileGroup"; + + private String mName; + private int mNameResId; + + private UUID mUuid; + + private Uri mSoundOverride = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + private Uri mRingerOverride = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); + + private Mode mSoundMode = Mode.DEFAULT; + private Mode mRingerMode = Mode.DEFAULT; + private Mode mVibrateMode = Mode.DEFAULT; + private Mode mLightsMode = Mode.DEFAULT; + + private boolean mDefaultGroup = false; + private boolean mDirty; + + /** @hide */ + public static final Parcelable.Creator<ProfileGroup> CREATOR + = new Parcelable.Creator<ProfileGroup>() { + public ProfileGroup createFromParcel(Parcel in) { + return new ProfileGroup(in); + } + + @Override + public ProfileGroup[] newArray(int size) { + return new ProfileGroup[size]; + } + }; + + /** @hide */ + public ProfileGroup(UUID uuid, boolean defaultGroup) { + this(null, uuid, defaultGroup); + } + + private ProfileGroup(String name, UUID uuid, boolean defaultGroup) { + mName = name; + mUuid = (uuid != null) ? uuid : UUID.randomUUID(); + mDefaultGroup = defaultGroup; + mDirty = uuid == null; + } + + /** @hide */ + private ProfileGroup(Parcel in) { + readFromParcel(in); + } + + /** @hide */ + public boolean matches(NotificationGroup group, boolean defaultGroup) { + if (mUuid.equals(group.getUuid())) { + return true; + } + + /* fallback matches for backwards compatibility */ + boolean matches = false; + + /* fallback attempt 1: match name */ + if (mName != null && mName.equals(group.getName())) { + matches = true; + /* fallback attempt 2: match for the 'defaultGroup' flag to match the wildcard group */ + } else if (mDefaultGroup && defaultGroup) { + matches = true; + } + + if (!matches) { + return false; + } + + mName = null; + mUuid = group.getUuid(); + mDirty = true; + + return true; + } + + public UUID getUuid() { + return mUuid; + } + + public boolean isDefaultGroup() { + return mDefaultGroup; + } + + /** @hide */ + public boolean isDirty() { + return mDirty; + } + + /** @hide */ + public void setSoundOverride(Uri sound) { + mSoundOverride = sound; + mDirty = true; + } + + public Uri getSoundOverride() { + return mSoundOverride; + } + + /** @hide */ + public void setRingerOverride(Uri ringer) { + mRingerOverride = ringer; + mDirty = true; + } + + public Uri getRingerOverride() { + return mRingerOverride; + } + + /** @hide */ + public void setSoundMode(Mode soundMode) { + mSoundMode = soundMode; + mDirty = true; + } + + public Mode getSoundMode() { + return mSoundMode; + } + + /** @hide */ + public void setRingerMode(Mode ringerMode) { + mRingerMode = ringerMode; + mDirty = true; + } + + public Mode getRingerMode() { + return mRingerMode; + } + + /** @hide */ + public void setVibrateMode(Mode vibrateMode) { + mVibrateMode = vibrateMode; + mDirty = true; + } + + public Mode getVibrateMode() { + return mVibrateMode; + } + + /** @hide */ + public void setLightsMode(Mode lightsMode) { + mLightsMode = lightsMode; + mDirty = true; + } + + public Mode getLightsMode() { + return mLightsMode; + } + + // TODO : add support for LEDs / screen etc. + + /** @hide */ + public void applyOverridesToNotification(Notification notification) { + switch (mSoundMode) { + case OVERRIDE: + notification.sound = mSoundOverride; + break; + case SUPPRESS: + notification.defaults &= ~Notification.DEFAULT_SOUND; + notification.sound = null; + break; + case DEFAULT: + break; + } + switch (mVibrateMode) { + case OVERRIDE: + notification.defaults |= Notification.DEFAULT_VIBRATE; + break; + case SUPPRESS: + notification.defaults &= ~Notification.DEFAULT_VIBRATE; + notification.vibrate = null; + break; + case DEFAULT: + break; + } + switch (mLightsMode) { + case OVERRIDE: + notification.defaults |= Notification.DEFAULT_LIGHTS; + break; + case SUPPRESS: + notification.defaults &= ~Notification.DEFAULT_LIGHTS; + notification.flags &= ~Notification.FLAG_SHOW_LIGHTS; + break; + case DEFAULT: + break; + } + } + + private boolean validateOverrideUri(Context context, Uri uri) { + if (RingtoneManager.isDefault(uri)) { + return true; + } + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + boolean valid = false; + + if (cursor != null) { + valid = cursor.moveToFirst(); + cursor.close(); + } + return valid; + } + + void validateOverrideUris(Context context) { + if (!validateOverrideUri(context, mSoundOverride)) { + mSoundOverride = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + mSoundMode = Mode.DEFAULT; + mDirty = true; + } + if (!validateOverrideUri(context, mRingerOverride)) { + mRingerOverride = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); + mRingerMode = Mode.DEFAULT; + mDirty = true; + } + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // === BOYSENBERRY === + dest.writeString(mName); + new ParcelUuid(mUuid).writeToParcel(dest, 0); + dest.writeInt(mDefaultGroup ? 1 : 0); + dest.writeInt(mDirty ? 1 : 0); + dest.writeParcelable(mSoundOverride, flags); + dest.writeParcelable(mRingerOverride, flags); + dest.writeString(mSoundMode.name()); + dest.writeString(mRingerMode.name()); + dest.writeString(mVibrateMode.name()); + dest.writeString(mLightsMode.name()); + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + /** @hide */ + public void readFromParcel(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + mName = in.readString(); + mUuid = ParcelUuid.CREATOR.createFromParcel(in).getUuid(); + mDefaultGroup = in.readInt() != 0; + mDirty = in.readInt() != 0; + mSoundOverride = in.readParcelable(null); + mRingerOverride = in.readParcelable(null); + + mSoundMode = Mode.valueOf(Mode.class, in.readString()); + mRingerMode = Mode.valueOf(Mode.class, in.readString()); + mVibrateMode = Mode.valueOf(Mode.class, in.readString()); + mLightsMode = Mode.valueOf(Mode.class, in.readString()); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public enum Mode { + SUPPRESS, DEFAULT, OVERRIDE; + } + + /** @hide */ + public void getXmlString(StringBuilder builder, Context context) { + builder.append("<profileGroup uuid=\""); + builder.append(TextUtils.htmlEncode(mUuid.toString())); + if (mName != null) { + builder.append("\" name=\""); + builder.append(mName); + } + builder.append("\" default=\""); + builder.append(isDefaultGroup()); + builder.append("\">\n<sound>"); + builder.append(TextUtils.htmlEncode(mSoundOverride.toString())); + builder.append("</sound>\n<ringer>"); + builder.append(TextUtils.htmlEncode(mRingerOverride.toString())); + builder.append("</ringer>\n<soundMode>"); + builder.append(mSoundMode); + builder.append("</soundMode>\n<ringerMode>"); + builder.append(mRingerMode); + builder.append("</ringerMode>\n<vibrateMode>"); + builder.append(mVibrateMode); + builder.append("</vibrateMode>\n<lightsMode>"); + builder.append(mLightsMode); + builder.append("</lightsMode>\n</profileGroup>\n"); + mDirty = false; + } + + /** @hide */ + public static ProfileGroup fromXml(XmlPullParser xpp, Context context) + throws XmlPullParserException, IOException { + String name = xpp.getAttributeValue(null, "name"); + UUID uuid = null; + String value = xpp.getAttributeValue(null, "uuid"); + + if (value != null) { + try { + uuid = UUID.fromString(value); + } catch (IllegalArgumentException e) { + Log.w(TAG, "UUID not recognized for " + name + ", using new one."); + } + } + + value = xpp.getAttributeValue(null, "default"); + boolean defaultGroup = TextUtils.equals(value, "true"); + + ProfileGroup profileGroup = new ProfileGroup(name, uuid, defaultGroup); + int event = xpp.next(); + while (event != XmlPullParser.END_TAG || !xpp.getName().equals("profileGroup")) { + if (event == XmlPullParser.START_TAG) { + name = xpp.getName(); + if (name.equals("sound")) { + profileGroup.setSoundOverride(Uri.parse(xpp.nextText())); + } else if (name.equals("ringer")) { + profileGroup.setRingerOverride(Uri.parse(xpp.nextText())); + } else if (name.equals("soundMode")) { + profileGroup.setSoundMode(Mode.valueOf(xpp.nextText())); + } else if (name.equals("ringerMode")) { + profileGroup.setRingerMode(Mode.valueOf(xpp.nextText())); + } else if (name.equals("vibrateMode")) { + profileGroup.setVibrateMode(Mode.valueOf(xpp.nextText())); + } else if (name.equals("lightsMode")) { + profileGroup.setLightsMode(Mode.valueOf(xpp.nextText())); + } + } else if (event == XmlPullParser.END_DOCUMENT) { + throw new IOException("Premature end of file while parsing profleGroup:" + name); + } + event = xpp.next(); + } + + /* we just loaded from XML, no need to save */ + profileGroup.mDirty = false; + + return profileGroup; + } +} diff --git a/sdk/src/java/cyanogenmod/app/ProfileManager.java b/sdk/src/java/cyanogenmod/app/ProfileManager.java new file mode 100644 index 0000000..c2470cb --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ProfileManager.java @@ -0,0 +1,555 @@ +/* + * 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 java.util.UUID; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.NotificationGroup; +import android.content.Context; +import android.os.IBinder; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import cyanogenmod.app.IProfileManager; + +import com.android.internal.R; +import cyanogenmod.providers.CMSettings; + + +/** + * <p> + * The ProfileManager allows you to create {@link Profile}s and {@link ProfileGroup}s to create + * specific behavior states depending on triggers from hardware devices changing states, such as: + * + * <pre class="prettyprint"> + * WiFi being enabled + * WiFi connecting to a certain AP + * Bluetooth connecting to a certain device + * Bluetooth disconnecting to a certain device + * NFC tag being scanned + * </pre> + * + * <p> + * Depending on these triggers, you can override connection settings, lockscreen modes, media + * stream volumes and various other settings. + * + * <p> + * To get the instance of this class, utilize ProfileManager#getInstance(Context context) + * + * <p> + * This manager requires the MODIFY_PROFILES permission. + * + * @see cyanogenmod.app.Profile + * @see cyanogenmod.app.ProfileGroup + */ +public class ProfileManager { + + private static IProfileManager sService; + + private Context mContext; + + private static final String TAG = "ProfileManager"; + + /** + * <p>Broadcast Action: A new profile has been selected. This can be triggered by the user + * or by calls to the ProfileManagerService / Profile.</p> + */ + public static final String INTENT_ACTION_PROFILE_SELECTED = + "cyanogenmod.platform.intent.action.PROFILE_SELECTED"; + + /** + * <p>Broadcast Action: Current profile has been updated. This is triggered every time the + * currently active profile is updated, instead of selected.</p> + * <p> For instance, this includes profile updates caused by a locale change, which doesn't + * trigger a profile selection, but causes its name to change.</p> + */ + public static final String INTENT_ACTION_PROFILE_UPDATED = + "cyanogenmod.platform.intent.action.PROFILE_UPDATED"; + + /** + * Extra for {@link #INTENT_ACTION_PROFILE_SELECTED} and {@link #INTENT_ACTION_PROFILE_UPDATED}: + * The name of the newly activated or updated profile + */ + public static final String EXTRA_PROFILE_NAME = "name"; + + /** + * Extra for {@link #INTENT_ACTION_PROFILE_SELECTED} and {@link #INTENT_ACTION_PROFILE_UPDATED}: + * The string representation of the UUID of the newly activated or updated profile + */ + public static final String EXTRA_PROFILE_UUID = "uuid"; + + /** + * Extra for {@link #INTENT_ACTION_PROFILE_SELECTED}: + * The name of the previously active profile + */ + public static final String EXTRA_LAST_PROFILE_NAME = "lastName"; + + /** + * Extra for {@link #INTENT_ACTION_PROFILE_SELECTED}: + * The string representation of the UUID of the previously active profile + */ + public static final String EXTRA_LAST_PROFILE_UUID = "lastUuid"; + + /** + * Activity Action: Shows a profile picker. + * <p> + * Input: {@link #EXTRA_PROFILE_EXISTING_UUID}, {@link #EXTRA_PROFILE_SHOW_NONE}, + * {@link #EXTRA_PROFILE_TITLE}. + * <p> + * Output: {@link #EXTRA_PROFILE_PICKED_UUID}. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PROFILE_PICKER = + "cyanogenmod.platform.intent.action.PROFILE_PICKER"; + + /** + * Constant for NO_PROFILE + */ + public static final UUID NO_PROFILE = + UUID.fromString("00000000-0000-0000-0000-000000000000"); + + /** + * Given to the profile picker as a boolean. Whether to show an item for + * deselect the profile. If the "None" item is picked, + * {@link #EXTRA_PROFILE_PICKED_UUID} will be {@link #NO_PROFILE}. + * + * @see #ACTION_PROFILE_PICKER + */ + public static final String EXTRA_PROFILE_SHOW_NONE = + "cyanogenmod.platform.intent.extra.profile.SHOW_NONE"; + + /** + * Given to the profile picker as a {@link UUID} string representation. The {@link UUID} + * representation of the current profile, which will be used to show a checkmark next to + * the item for this {@link UUID}. If the item is {@link #NO_PROFILE} then "None" item + * is selected if {@link #EXTRA_PROFILE_SHOW_NONE} is enabled. Otherwise, the current + * profile is selected. + * + * @see #ACTION_PROFILE_PICKER + */ + public static final String EXTRA_PROFILE_EXISTING_UUID = + "cyanogenmod.platform.extra.profile.EXISTING_UUID"; + + /** + * Given to the profile picker as a {@link CharSequence}. The title to + * show for the profile picker. This has a default value that is suitable + * in most cases. + * + * @see #ACTION_PROFILE_PICKER + */ + public static final String EXTRA_PROFILE_TITLE = + "cyanogenmod.platform.intent.extra.profile.TITLE"; + + /** + * Returned from the profile picker as a {@link UUID} string representation. + * <p> + * It will be one of: + * <li> the picked profile, + * <li> null if the "None" item was picked. + * + * @see #ACTION_PROFILE_PICKER + */ + public static final String EXTRA_PROFILE_PICKED_UUID = + "cyanogenmod.platform.intent.extra.profile.PICKED_UUID"; + + /** + * Broadcast intent action indicating that Profiles has been enabled or disabled. + * One extra provides this state as an int. + * + * @see #EXTRA_PROFILES_STATE + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String PROFILES_STATE_CHANGED_ACTION = + "cyanogenmod.platform.app.profiles.PROFILES_STATE_CHANGED"; + + /** + * The lookup key for an int that indicates whether Profiles are enabled or + * disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}. + * + * @see #PROFILES_STATE_DISABLED + * @see #PROFILES_STATE_ENABLED + */ + public static final String EXTRA_PROFILES_STATE = "profile_state"; + + /** + * Set the resource id theme to use for the dialog picker activity.<br/> + * The default theme is <code>com.android.internal.R.Theme_Holo_Dialog_Alert</code>. + * + * @see #ACTION_PROFILE_PICKER + */ + public static final String EXTRA_PROFILE_DIALOG_THEME = + "cyanogenmod.platform.intent.extra.profile.DIALOG_THEME"; + + /** + * Profiles are disabled. + * + * @see #PROFILES_STATE_CHANGED_ACTION + */ + public static final int PROFILES_STATE_DISABLED = 0; + + /** + * Profiles are enabled. + * + * @see #PROFILES_STATE_CHANGED_ACTION + */ + public static final int PROFILES_STATE_ENABLED = 1; + + private static ProfileManager sProfileManagerInstance; + private ProfileManager(Context context) { + Context appContext = context.getApplicationContext(); + if (appContext != null) { + mContext = appContext; + } else { + mContext = context; + } + sService = getService(); + + if (context.getPackageManager().hasSystemFeature( + cyanogenmod.app.CMContextConstants.Features.PROFILES) && sService == null) { + throw new RuntimeException("Unable to get ProfileManagerService. The service either" + + " crashed, was not started, or the interface has been called to early in" + + " SystemServer init"); + } + } + + /** + * Get or create an instance of the {@link cyanogenmod.app.ProfileManager} + * @param context + * @return {@link ProfileManager} + */ + public static ProfileManager getInstance(Context context) { + if (sProfileManagerInstance == null) { + sProfileManagerInstance = new ProfileManager(context); + } + return sProfileManagerInstance; + } + + /** @hide */ + static public IProfileManager getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(CMContextConstants.CM_PROFILE_SERVICE); + sService = IProfileManager.Stub.asInterface(b); + return sService; + } + + @Deprecated + public void setActiveProfile(String profileName) { + try { + getService().setActiveProfileByName(profileName); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Set the active {@link Profile} by {@link UUID} + * @param profileUuid the {@link UUID} associated with the profile + */ + public void setActiveProfile(UUID profileUuid) { + try { + getService().setActiveProfile(new ParcelUuid(profileUuid)); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Get the active {@link Profile} + * @return active {@link Profile} + */ + public Profile getActiveProfile() { + try { + return getService().getActiveProfile(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Add a {@link Profile} that can be selected by the user + * @param profile a {@link Profile} object + */ + public void addProfile(Profile profile) { + try { + getService().addProfile(profile); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Remove a {@link Profile} from user selection + * @param profile a {@link Profile} object + */ + public void removeProfile(Profile profile) { + try { + getService().removeProfile(profile); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Update a {@link Profile} object + * @param profile a {@link Profile} object + */ + public void updateProfile(Profile profile) { + try { + getService().updateProfile(profile); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Get the {@link Profile} object by its literal name + * @param profileName name associated with the profile + * @return profile a {@link Profile} object + */ + @Deprecated + public Profile getProfile(String profileName) { + try { + return getService().getProfileByName(profileName); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Get a {@link Profile} via {@link UUID} + * @param profileUuid {@link UUID} associated with the profile + * @return {@link Profile} + */ + public Profile getProfile(UUID profileUuid) { + try { + return getService().getProfile(new ParcelUuid(profileUuid)); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Get the profile names currently available to the user + * @return {@link String[]} of profile names + */ + public String[] getProfileNames() { + try { + Profile[] profiles = getService().getProfiles(); + String[] names = new String[profiles.length]; + for (int i = 0; i < profiles.length; i++) { + names[i] = profiles[i].getName(); + } + return names; + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Get the {@link Profile}s currently available to the user + * @return {@link Profile[]} + */ + public Profile[] getProfiles() { + try { + return getService().getProfiles(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Check if a {@link Profile} exists via its literal name + * @param profileName a profile name + * @return whether or not the profile exists + */ + public boolean profileExists(String profileName) { + try { + return getService().profileExistsByName(profileName); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + // To be on the safe side, we'll return "true", to prevent duplicate profiles + // from being created. + return true; + } + } + + /** + * Check if a {@link Profile} exists via its {@link UUID} + * @param profileUuid the profiles {@link UUID} + * @return whether or not the profile exists + */ + public boolean profileExists(UUID profileUuid) { + try { + return getService().profileExists(new ParcelUuid(profileUuid)); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + // To be on the safe side, we'll return "true", to prevent duplicate profiles + // from being created. + return true; + } + } + + /** + * Check if a NotificationGroup exists + * @param notificationGroupName the name of the notification group + * @return whether or not the notification group exists + * @hide + */ + public boolean notificationGroupExists(String notificationGroupName) { + try { + return getService().notificationGroupExistsByName(notificationGroupName); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + // To be on the safe side, we'll return "true", to prevent duplicate notification + // groups from being created. + return true; + } + } + + /** + * Get the currently available NotificationGroups + * @return NotificationGroup + * @hide + */ + public NotificationGroup[] getNotificationGroups() { + try { + return getService().getNotificationGroups(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Add a NotificationGroup to the available list + * @param group NotificationGroup + * @hide + */ + public void addNotificationGroup(NotificationGroup group) { + try { + getService().addNotificationGroup(group); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Remove a NotificationGroup from the available list + * @param group NotificationGroup + * @hide + */ + public void removeNotificationGroup(NotificationGroup group) { + try { + getService().removeNotificationGroup(group); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Update a NotificationGroup from the available list + * @param group NotificationGroup + * @hide + */ + public void updateNotificationGroup(NotificationGroup group) { + try { + getService().updateNotificationGroup(group); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Get a NotificationGroup for a specific package + * @param pkg name of the package + * @hide + */ + public NotificationGroup getNotificationGroupForPackage(String pkg) { + try { + return getService().getNotificationGroupForPackage(pkg); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Get a NotificationGroup from the available list via {@link UUID} + * @param uuid {@link UUID} of the notification group + * @hide + */ + public NotificationGroup getNotificationGroup(UUID uuid) { + try { + return getService().getNotificationGroup(new ParcelUuid(uuid)); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return null; + } + + /** + * Get an active {@link ProfileGroup} via its package name + * @param packageName the package name associated to the profile group + * @return {@link ProfileGroup} + * @hide + */ + public ProfileGroup getActiveProfileGroup(String packageName) { + NotificationGroup notificationGroup = getNotificationGroupForPackage(packageName); + if (notificationGroup == null) { + ProfileGroup defaultGroup = getActiveProfile().getDefaultGroup(); + return defaultGroup; + } + return getActiveProfile().getProfileGroup(notificationGroup.getUuid()); + } + + /** + * Reset all profiles, groups, and notification groups to default state + */ + public void resetAll() { + try { + getService().resetAll(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } catch (SecurityException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + } + + /** + * Check if profiles are currently activated in the system + * @return whether profiles are enabled + */ + public boolean isProfilesEnabled() { + try { + return getService().isEnabled(); + } catch (RemoteException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + return false; + } +} diff --git a/sdk/src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl b/sdk/src/java/cyanogenmod/app/StatusBarPanelCustomTile.aidl new file mode 100644 index 0000000..96cfb6a --- /dev/null +++ b/sdk/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/sdk/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java b/sdk/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java new file mode 100644 index 0000000..7710e5a --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/StatusBarPanelCustomTile.java @@ -0,0 +1,256 @@ +/* + * 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; + +import cyanogenmod.os.Build; + +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +/** + * 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 resPkg; + 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 resPkg, String opPkg, int id, String tag, + int uid, int initialPid, CustomTile customTile, UserHandle user) { + this(pkg, resPkg, opPkg, id, tag, uid, initialPid, customTile, user, + System.currentTimeMillis()); + } + + public StatusBarPanelCustomTile(String pkg, String resPkg, 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.resPkg = resPkg; + 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) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + // tmp variables for final + String tmpResPkg = null; + String tmpPkg = null; + String tmpOpPkg = null; + int tmpId = -1; + String tmpTag = null; + int tmpUid = -1; + int tmpPid = -1; + CustomTile tmpCustomTile = null; + UserHandle tmpUser = null; + long tmpPostTime = -1; + + // Pattern here is that all new members should be added to the end of + // the writeToParcel method. Then we step through each version, until the latest + // API release to help unravel this parcel + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + // default + tmpPkg = in.readString(); + tmpOpPkg = in.readString(); + tmpId = in.readInt(); + if (in.readInt() != 0) { + tmpTag = in.readString(); + } else { + tmpTag = null; + } + tmpUid = in.readInt(); + tmpPid = in.readInt(); + tmpCustomTile = new CustomTile(in); + tmpUser = UserHandle.readFromParcel(in); + tmpPostTime = in.readLong(); + } + + if (parcelableVersion >= Build.CM_VERSION_CODES.BOYSENBERRY) { + tmpResPkg = in.readString(); + } + + // Assign finals + this.resPkg = tmpResPkg; + this.pkg = tmpPkg; + this.opPkg = tmpOpPkg; + this.id = tmpId; + this.tag = tmpTag; + this.uid = tmpUid; + this.initialPid = tmpPid; + this.customTile = tmpCustomTile; + this.user = tmpUser; + this.postTime = tmpPostTime; + this.key = key(); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + private String key() { + return user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; + } + + public static final Creator<StatusBarPanelCustomTile> CREATOR + = new Creator<StatusBarPanelCustomTile>() + { + 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) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(out); + + // ==== APRICOT === + 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); + + // ==== BOYSENBERRY ===== + out.writeString(this.resPkg); + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public StatusBarPanelCustomTile clone() { + return new StatusBarPanelCustomTile(this.pkg, this.resPkg, 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 tile. + */ + public int getUserId() { + return this.user.getIdentifier(); + } + + /** The package of the app that posted the tile */ + 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 tile record. + */ + public String getKey() { + return key; + } + + /** The notifying app's calling uid. @hide */ + public int getUid() { + return uid; + } + + /** The package used for load resources from. @hide */ + public String getResPkg() { + return resPkg; + } + + /** 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/sdk/src/java/cyanogenmod/app/ThemeComponent.java b/sdk/src/java/cyanogenmod/app/ThemeComponent.java new file mode 100644 index 0000000..d3f6625 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ThemeComponent.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * The id value here matches the framework. Unknown is given a -1 value since future + * framework components will always be positive. + * @hide + */ +public enum ThemeComponent { + UNKNOWN(-1), + OVERLAY(0), + BOOT_ANIM(1), + WALLPAPER(2), + LOCKSCREEN(3), + FONT(4), + ICON(5), + SOUND(6); + + public int id; + ThemeComponent(int id) { + this.id = id; + } + +} diff --git a/sdk/src/java/cyanogenmod/app/ThemeVersion.java b/sdk/src/java/cyanogenmod/app/ThemeVersion.java new file mode 100644 index 0000000..b9846c6 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/ThemeVersion.java @@ -0,0 +1,206 @@ +/* + * 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.Build; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * @hide + */ +public class ThemeVersion { + private static final String THEME_VERSION_CLASS_NAME = "android.content.ThemeVersion"; + private static final String THEME_VERSION_FIELD_NAME = "THEME_VERSION"; + private static final String MIN_SUPPORTED_THEME_VERSION_FIELD_NAME = + "MIN_SUPPORTED_THEME_VERSION"; + private static final int CM11 = 1; + private static final int CM12_PRE_VERSIONING = 2; + + public static int getVersion() { + int version; + try { + Class<?> themeVersionClass = Class.forName(THEME_VERSION_CLASS_NAME); + Field themeVersionField = themeVersionClass.getField(THEME_VERSION_FIELD_NAME); + version = (Integer) themeVersionField.get(null); + } catch(Exception e) { + // Field doesn't exist. Fallback to SDK level + version = Build.VERSION.SDK_INT < 21 ? CM11 : + CM12_PRE_VERSIONING; + } + return version; + } + + public static int getMinSupportedVersion() { + int getMinSupportedVersion; + try { + Class<?> themeVersionClass = Class.forName(THEME_VERSION_CLASS_NAME); + Field themeVersionField = + themeVersionClass.getField(MIN_SUPPORTED_THEME_VERSION_FIELD_NAME); + getMinSupportedVersion = (Integer) themeVersionField.get(null); + } catch(Exception e) { + // Field doesn't exist. Fallback to SDK level + getMinSupportedVersion = Build.VERSION.SDK_INT < 21 ? CM11 : + CM12_PRE_VERSIONING; + } + return getMinSupportedVersion; + } + + public static ComponentVersion getComponentVersion(ThemeComponent component) { + int version = getVersion(); + if (version == 1) { + throw new UnsupportedOperationException(); + } else if (version == 2) { + return ThemeVersionImpl2.getDeviceComponentVersion(component); + } else { + return ThemeVersionImpl3.getDeviceComponentVersion(component); + } + } + + public static List<ComponentVersion> getComponentVersions() { + int version = getVersion(); + if (version == 1) { + throw new UnsupportedOperationException(); + } else if (version == 2) { + return ThemeVersionImpl2.getDeviceComponentVersions(); + } else { + return ThemeVersionImpl3.getDeviceComponentVersions(); + } + } + + public static class ComponentVersion { + protected int id; + protected String name; + protected ThemeComponent component; + protected int minVersion; + protected int currentVersion; + + protected ComponentVersion(int id, ThemeComponent component, int targetVersion) { + this(id, component, component.name(), targetVersion, targetVersion); + } + + protected ComponentVersion(int id, + ThemeComponent component, + String name, + int minVersion, + int targetVersion) { + this.id = id; + this.component = component; + this.name = name; + this.minVersion = minVersion; + this.currentVersion = targetVersion; + } + + public ComponentVersion(ComponentVersion copy) { + this(copy.id, copy.component, copy.name, copy.minVersion, copy.currentVersion); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public ThemeComponent getComponent() { + return component; + } + + public int getMinVersion() { + return minVersion; + } + + public int getCurrentVersion() { + return currentVersion; + } + } + + private static class ThemeVersionImpl2 { + private static ArrayList<ComponentVersion> cVersions = new ArrayList<ComponentVersion>() { + { + add(new ComponentVersion(0, ThemeComponent.OVERLAY, 2)); + add(new ComponentVersion(1, ThemeComponent.BOOT_ANIM, 1)); + add(new ComponentVersion(2, ThemeComponent.WALLPAPER, 1)); + add(new ComponentVersion(3, ThemeComponent.LOCKSCREEN, 1)); + add(new ComponentVersion(4, ThemeComponent.ICON, 1)); + add(new ComponentVersion(5, ThemeComponent.FONT, 1)); + add(new ComponentVersion(6, ThemeComponent.SOUND, 1)); + } + }; + + public static ComponentVersion getDeviceComponentVersion(ThemeComponent component) { + for(ComponentVersion compVersion : cVersions) { + if (compVersion.component.equals(component)) { + return new ComponentVersion(compVersion); + } + } + return null; + } + + public static List<ComponentVersion> getDeviceComponentVersions() { + ArrayList<ComponentVersion> versions = new ArrayList<ComponentVersion>(); + versions.addAll(cVersions); + return versions; + } + } + + private static class ThemeVersionImpl3 { + public static ComponentVersion getDeviceComponentVersion(ThemeComponent component) { + for(android.content.ThemeVersion.ComponentVersion version : + android.content.ThemeVersion.ComponentVersion.values()) { + ComponentVersion sdkVersionInfo = fwCompVersionToSdkVersion(version); + if (sdkVersionInfo.component.equals(component)) { + return sdkVersionInfo; + } + } + return null; + } + + public static List<ComponentVersion> getDeviceComponentVersions() { + List<ComponentVersion> versions = new ArrayList<ComponentVersion>(); + + for(android.content.ThemeVersion.ComponentVersion version : + android.content.ThemeVersion.ComponentVersion.values()) { + versions.add(fwCompVersionToSdkVersion(version)); + } + + return versions; + } + + public static ComponentVersion fwCompVersionToSdkVersion( + android.content.ThemeVersion.ComponentVersion version) { + // Find the SDK component with the matching id + // If no ID matches then the FW must have a newer component that we don't + // know anything about. We can still return the id and name + ThemeComponent component = ThemeComponent.UNKNOWN; + for(ThemeComponent aComponent : ThemeComponent.values()) { + if (aComponent.id == version.id) { + component = aComponent; + } + } + + int id = version.id; + String name = version.name(); + int minVersion = version.minSupportedVersion; + int targetVersion = version.currentVersion; + + return new ComponentVersion(id, component, name, minVersion, targetVersion); + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/suggest/AppSuggestManager.java b/sdk/src/java/cyanogenmod/app/suggest/AppSuggestManager.java new file mode 100644 index 0000000..667eaa7 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/suggest/AppSuggestManager.java @@ -0,0 +1,150 @@ +/** + * 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.suggest; + +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * Provides an interface to get information about suggested apps for an intent which may include + * applications not installed on the device. This is used by the CMResolver in order to provide + * suggestions when an intent is fired but no application exists for the given intent. + * + * @hide + */ +public class AppSuggestManager { + private static final String TAG = AppSuggestManager.class.getSimpleName(); + private static final boolean DEBUG = true; + + private static IAppSuggestManager sImpl; + + private static AppSuggestManager sInstance; + + private Context mContext; + + /** + * Gets an instance of the AppSuggestManager. + * + * @param context + * + * @return An instance of the AppSuggestManager + */ + public static synchronized AppSuggestManager getInstance(Context context) { + if (sInstance != null) { + return sInstance; + } + + context = context.getApplicationContext() != null ? + context.getApplicationContext() : context; + + sInstance = new AppSuggestManager(context); + + if (context.getPackageManager().hasSystemFeature(CMContextConstants.Features.APP_SUGGEST) + && sImpl == null) { + throw new RuntimeException("Unable to get AppSuggestManagerService. " + + "The service either crashed, was not started, or the interface has been" + + " called to early in SystemServer init"); + } + + return sInstance; + } + + private AppSuggestManager(Context context) { + mContext = context.getApplicationContext(); + sImpl = getService(); + } + + /** @hide */ + public static synchronized IAppSuggestManager getService() { + if (sImpl == null) { + IBinder b = ServiceManager.getService(CMContextConstants.CM_APP_SUGGEST_SERVICE); + if (b != null) { + sImpl = IAppSuggestManager.Stub.asInterface(b); + } else { + Log.e(TAG, "Unable to find implementation for app suggest service"); + } + } + + return sImpl; + } + + /** + * Checks to see if an intent is handled by the App Suggestions Service. This should be + * implemented in such a way that it is safe to call inline on the UI Thread. + * + * @param intent The intent + * @return true if the App Suggestions Service has suggestions for this intent, false otherwise + */ + public boolean handles(Intent intent) { + IAppSuggestManager mgr = getService(); + if (mgr == null) return false; + try { + return mgr.handles(intent); + } catch (RemoteException e) { + return false; + } + } + + /** + * + * Gets a list of the suggestions for the given intent. + * + * @param intent The intent + * @return A list of application suggestions or an empty list if none. + */ + public List<ApplicationSuggestion> getSuggestions(Intent intent) { + IAppSuggestManager mgr = getService(); + if (mgr == null) return new ArrayList<>(0); + try { + return mgr.getSuggestions(intent); + } catch (RemoteException e) { + return new ArrayList<>(0); + } + } + + /** + * Loads the icon for the given suggestion. + * + * @param suggestion The suggestion to load the icon for + * + * @return A {@link Drawable} or null if one cannot be found + */ + public Drawable loadIcon(ApplicationSuggestion suggestion) { + try { + InputStream is = mContext.getContentResolver() + .openInputStream(suggestion.getThumbailUri()); + return Drawable.createFromStream(is, null); + } catch (FileNotFoundException e) { + return null; + } + } +} diff --git a/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl b/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl new file mode 100644 index 0000000..7ab8584 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl @@ -0,0 +1,22 @@ +/** + * 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.suggest; + +/** + * @hide + */ +parcelable ApplicationSuggestion; diff --git a/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java b/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java new file mode 100644 index 0000000..17e40b9 --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java @@ -0,0 +1,110 @@ +/** + * 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.suggest; + +import android.annotation.NonNull; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import cyanogenmod.os.Build; +import cyanogenmod.os.Concierge; +import cyanogenmod.os.Concierge.ParcelInfo; + +/** + * @hide + */ +public class ApplicationSuggestion implements Parcelable { + + public static final Creator<ApplicationSuggestion> CREATOR = + new Creator<ApplicationSuggestion>() { + public ApplicationSuggestion createFromParcel(Parcel in) { + return new ApplicationSuggestion(in); + } + + public ApplicationSuggestion[] newArray(int size) { + return new ApplicationSuggestion[size]; + } + }; + + private String mName; + + private String mPackage; + + private Uri mDownloadUri; + + private Uri mThumbnailUri; + + public ApplicationSuggestion(@NonNull String name, @NonNull String pkg, + @NonNull Uri downloadUri, @NonNull Uri thumbnailUri) { + mName = name; + mPackage = pkg; + mDownloadUri = downloadUri; + mThumbnailUri = thumbnailUri; + } + + private ApplicationSuggestion(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + mName = in.readString(); + mPackage = in.readString(); + mDownloadUri = in.readParcelable(Uri.class.getClassLoader()); + mThumbnailUri = in.readParcelable(Uri.class.getClassLoader()); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(out); + + out.writeString(mName); + out.writeString(mPackage); + out.writeParcelable(mDownloadUri, flags); + out.writeParcelable(mThumbnailUri, flags); + + // Complete the parcel info for the concierge + parcelInfo.complete(); + } + + public String getName() { + return mName; + } + + public String getPackageName() { + return mPackage; + } + + public Uri getDownloadUri() { + return mDownloadUri; + } + + public Uri getThumbailUri() { + return mThumbnailUri; + } +} diff --git a/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl b/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl new file mode 100644 index 0000000..68ab87f --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl @@ -0,0 +1,30 @@ +/** + * 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.suggest; + +import android.content.Intent; + +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * @hide + */ +interface IAppSuggestManager { + boolean handles(in Intent intent); + + List<ApplicationSuggestion> getSuggestions(in Intent intent); +}
\ No newline at end of file diff --git a/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl b/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl new file mode 100644 index 0000000..759880d --- /dev/null +++ b/sdk/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl @@ -0,0 +1,30 @@ +/** + * 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.suggest; + +import android.content.Intent; + +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * @hide + */ +interface IAppSuggestProvider { + boolean handles(in Intent intent); + + List<ApplicationSuggestion> getSuggestions(in Intent intent); +}
\ No newline at end of file |