cmparts: Create PartsCatalog
* Add a service which can tell a remote application (like Settings)
some various information about available parts and get callbacks
when state changes.
Change-Id: I71ad7bc7b282bc831c0b20ac47df7910c0a59337
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 513b9f0..9b49025 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -33,6 +33,8 @@
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
+ <uses-permission android:name="cyanogenmod.permission.BIND_CORE_SERVICE" />
+
<application android:label="@string/cmparts_title"
android:theme="@style/Theme.Settings"
android:hardwareAccelerated="true"
@@ -40,6 +42,24 @@
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true">
+ <activity android:name=".PartsActivity">
+ <intent-filter>
+ <action android:name="org.cyanogenmod.cmparts.PART" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <service android:name="org.cyanogenmod.cmparts.PartsCatalog"
+ android:permission="cyanogenmod.permission.BIND_CORE_SERVICE"
+ android:enabled="true"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="org.cyanogenmod.cmparts.CATALOG" />
+ </intent-filter>
+ </service>
+
+
+ <!-- Privacy settings header -->
<activity
android:name=".PrivacySettings"
android:label="@string/privacy_settings_title">
@@ -54,12 +74,5 @@
android:resource="@drawable/ic_settings_privacy" />
</activity>
- <activity android:name=".PartsActivity">
- <intent-filter>
- <action android:name="org.cyanogenmod.cmparts.PART" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
</application>
</manifest>
diff --git a/proguard.flags b/proguard.flags
index ecbb6e0..4d0b1d0 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,60 +1,17 @@
--optimizationpasses 5
--dontusemixedcaseclassnames
--dontskipnonpubliclibraryclasses
--dontpreverify
--verbose
--optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+# Keep all Fragments in this package, which are used by reflection.
+-keep class org.cyanogenmod.cmparts.*Fragment
+-keep class org.cyanogenmod.cmparts.*Picker
+-keep class org.cyanogenmod.cmparts.*Settings
+-keep class org.cyanogenmod.cmparts.notificationlight.*
+-keep class org.cyanogenmod.cmparts.livedisplay.*
+-keep class org.cyanogenmod.cmparts.privacyguard.*
--keep public class * extends android.app.Activity
--keep public class * extends android.app.Application
--keep public class * extends android.app.Service
--keep public class * extends android.content.BroadcastReceiver
--keep public class * extends android.content.ContentProvider
--keep public class * extends android.app.backup.BackupAgentHelper
--keep public class * extends android.preference.Preference
--keep public class android.support.v7.preference.Preference {
- public <init>(android.content.Context, android.util.AttributeSet);
-}
--keep public class * extends android.support.v7.preference.Preference {
- public <init>(android.content.Context, android.util.AttributeSet);
-}
--keep public class com.android.vending.licensing.ILicensingService
-
--keepclasseswithmembernames class * {
- native <methods>;
+# Keep click responders
+-keepclassmembers class com.android.settings.inputmethod.UserDictionaryAddWordActivity {
+ *** onClick*(...);
}
--keepclasseswithmembers class * {
+-keep public class * extends android.support.v7.preference.* {
public <init>(android.content.Context, android.util.AttributeSet);
}
--keepclasseswithmembers class * {
- public <init>(android.content.Context, android.util.AttributeSet, int);
-}
-
--keepclassmembers class * extends android.app.Activity {
- public void *(android.view.View);
-}
-
--keepclassmembers enum * {
- public static **[] values();
- public static ** valueOf(java.lang.String);
-}
-
--keep class * implements android.os.Parcelable {
- public static final android.os.Parcelable$Creator *;
-}
-
--keep @android.support.annotation.Keep class *
--keepclassmembers class * {
- @android.support.annotation.Keep *;
-}
-
--dontwarn org.bouncycastle.x509.util.LDAPStoreHelper
--dontwarn org.bouncycastle.jce.provider.X509LDAPCertStoreSpi
--dontwarn org.bouncycastle.util.io.pem.AllTests
--dontwarn org.bouncycastle.util.AllTests
--dontwarn android.support.v13.app.FragmentCompatICSMR1
--dontwarn android.support.v4.view.ViewCompatJellybeanMr1
--dontwarn org.bouncycastle.x509.X509V3CertificateGenerator
--dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e871ff3..aa7a340 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -14,7 +14,9 @@
limitations under the License.
-->
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:cm="http://schemas.android.com/apk/res-auto">
+
<attr name="preferenceBackgroundColor" format="color" />
<declare-styleable name="IntervalSeekBar">
@@ -23,4 +25,14 @@
<attr name="defaultValue" />
<attr name="digits" format="integer" />
</declare-styleable>
+
+ <declare-styleable name="PartsCatalog">
+ <attr name="key" format="string" />
+ <attr name="title" />
+ <attr name="summary" format="string" />
+ <attr name="fragment" format="string" />
+ <attr name="enabled" format="boolean" />
+ <attr name="icon" />
+ <attr name="alias" format="string" />
+ </declare-styleable>
</resources>
diff --git a/res/xml/parts_catalog.xml b/res/xml/parts_catalog.xml
new file mode 100644
index 0000000..4eee180
--- /dev/null
+++ b/res/xml/parts_catalog.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<parts-catalog xmlns:cm="http://schemas.android.com/apk/res-auto">
+
+ <part cm:key="battery_lights"
+ cm:title="@*cyanogenmod.platform:string/battery_light_title"
+ cm:fragment="org.cyanogenmod.cmparts.notificationlight.BatteryLightSettings" />
+
+ <part cm:key="notification_lights"
+ cm:title="@*cyanogenmod.platform:string/notification_light_title"
+ cm:fragment="org.cyanogenmod.cmparts.notificationlight.NotificationLightSettings" />
+
+ <part android:key="livedisplay"
+ android:title="@*cyanogenmod.platform:string/live_display_title"
+ android:summary="@string/live_display_summary"
+ android:fragment="org.cyanogenmod.cmparts.livedisplay.LiveDisplay" />
+
+</parts-catalog>
diff --git a/src/org/cyanogenmod/cmparts/PartsActivity.java b/src/org/cyanogenmod/cmparts/PartsActivity.java
index 65dbd01..a51cb41 100644
--- a/src/org/cyanogenmod/cmparts/PartsActivity.java
+++ b/src/org/cyanogenmod/cmparts/PartsActivity.java
@@ -17,70 +17,96 @@
package org.cyanogenmod.cmparts;
import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Fragment;
import android.app.FragmentTransaction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Bundle;
-import android.preference.PreferenceActivity;
+import android.os.IBinder;
import android.util.Log;
-import org.cyanogenmod.cmparts.livedisplay.LiveDisplay;
-import org.cyanogenmod.cmparts.notificationlight.BatteryLightSettings;
-import org.cyanogenmod.cmparts.notificationlight.NotificationLightSettings;
+import org.cyanogenmod.internal.cmparts.IPartsCatalog;
+import org.cyanogenmod.internal.cmparts.PartInfo;
-public class PartsActivity extends PreferenceActivity {
+public class PartsActivity extends Activity {
- public static final String TAG = "PartsActivity";
+ private static final String TAG = "PartsActivity";
public static final String EXTRA_PART = "part";
public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+ public static final String ACTION_PART = "org.cyanogenmod.cmparts.PART";
- public static final String FRAGMENT_PREFIX = "cmparts:";
-
- public static final String FRAGMENT_BATTERY_LIGHTS = "battery_lights";
- public static final String FRAGMENT_NOTIFICATION_LIGHTS = "notification_lights";
- public static final String FRAGMENT_LIVEDISPLAY = "livedisplay";
-
- private ActionBar mActionBar;
+ private IPartsCatalog mCatalog;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
- String partExtra = getIntent().getStringExtra(EXTRA_PART);
- if (partExtra != null && partExtra.startsWith(FRAGMENT_PREFIX)) {
- String[] keys = partExtra.split(":");
- if (keys.length < 2) {
- return;
- }
- String part = keys[1];
- Log.d(TAG, "Launching fragment: " + partExtra);
+ connectCatalog();
- SettingsPreferenceFragment fragment = null;
- if (part.equals(FRAGMENT_NOTIFICATION_LIGHTS)) {
- fragment = new NotificationLightSettings();
- } else if (part.equals(FRAGMENT_BATTERY_LIGHTS)) {
- fragment = new BatteryLightSettings();
- } else if (part.equals(FRAGMENT_LIVEDISPLAY)) {
- fragment = new LiveDisplay();
- } else {
- Log.d(TAG, "Unknown fragment: " + part);
- }
+ Log.d(TAG, "Launched with: " + getIntent().toString() + " action: " +
+ getIntent().getAction() + " component: " + getIntent().getComponent().flattenToString() +
+ " extras: " + getIntent().getExtras().toString());
- mActionBar = getActionBar();
- if (mActionBar != null) {
- mActionBar.setDisplayHomeAsUpEnabled(true);
- mActionBar.setHomeButtonEnabled(true);
- }
-
- if (fragment != null) {
- getFragmentManager().beginTransaction()
- .replace(android.R.id.content, fragment)
- .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
- .commitAllowingStateLoss();
- getFragmentManager().executePendingTransactions();
- }
+ PartInfo info = null;
+ String extra = getIntent().getStringExtra(EXTRA_PART);
+ if (ACTION_PART.equals(getIntent().getAction()) && extra != null) {
+ info = PartsCatalog.getPartInfo(getResources(), extra);
+ } else {
+ info = PartsCatalog.getPartInfoForClass(getResources(),
+ getIntent().getComponent().getClassName());
}
+
+ if (info == null) {
+ throw new UnsupportedOperationException("Unable to get part: " + getIntent().toString());
+ }
+
+ Log.d(TAG, "Launching fragment: " + info.getFragmentClass());
+
+ Fragment fragment = Fragment.instantiate(this, info.getFragmentClass());
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+
+ actionBar.setTitle(info.getTitle());
+
+ getFragmentManager().beginTransaction().replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ .commitAllowingStateLoss();
+ getFragmentManager().executePendingTransactions();
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ disconnectCatalog();
+ }
+ private void connectCatalog() {
+ Intent i = new Intent(this, PartsCatalog.class);
+ bindService(i, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void disconnectCatalog() {
+ unbindService(mConnection);
+ }
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ mCatalog = IPartsCatalog.Stub.asInterface(iBinder);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mCatalog = null;
+ }
+ };
}
diff --git a/src/org/cyanogenmod/cmparts/PartsCatalog.java b/src/org/cyanogenmod/cmparts/PartsCatalog.java
new file mode 100644
index 0000000..b479cb9
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/PartsCatalog.java
@@ -0,0 +1,266 @@
+package org.cyanogenmod.cmparts;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.support.annotation.XmlRes;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+
+import org.cyanogenmod.internal.cmparts.IPartChangedCallback;
+import org.cyanogenmod.internal.cmparts.IPartsCatalog;
+import org.cyanogenmod.internal.cmparts.PartInfo;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class PartsCatalog extends Service {
+
+ private static final String TAG = "PartsCatalog";
+
+ private static final Map<String, PartInfo> sParts = new ArrayMap<String, PartInfo>();
+
+ private static final Map<String, RemoteCallbackList<IPartChangedCallback>> sCallbacks =
+ new ArrayMap<String, RemoteCallbackList<IPartChangedCallback>>();
+
+ private static final AtomicBoolean mCatalogLoaded = new AtomicBoolean(false);
+
+ private final IPartsCatalog.Stub mBinder = new IPartsCatalog.Stub() {
+
+ @Override
+ public boolean isPartAvailable(String key) throws RemoteException {
+ synchronized (sParts) {
+ PartInfo info = sParts.get(key);
+ return info != null && info.isAvailable();
+ }
+ }
+
+ @Override
+ public PartInfo getPartInfo(String key) throws RemoteException {
+ synchronized (sParts) {
+ return sParts.get(key);
+ }
+ }
+
+ @Override
+ public void registerCallback(String key, IPartChangedCallback cb) throws RemoteException {
+ synchronized (sParts) {
+ if (sParts.containsKey(key)) {
+ RemoteCallbackList<IPartChangedCallback> cbs = sCallbacks.get(key);
+ if (cbs == null) {
+ cbs = new RemoteCallbackList<>();
+ sCallbacks.put(key, cbs);
+ }
+ cbs.register(cb);
+ }
+ }
+ }
+
+ @Override
+ public void unregisterCallback(String key, IPartChangedCallback cb) throws RemoteException {
+ synchronized (sParts) {
+ if (sParts.containsKey(key)) {
+ RemoteCallbackList<IPartChangedCallback> cbs = sCallbacks.get(key);
+ if (cbs != null) {
+ cbs.unregister(cb);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String[] getPartsList() throws RemoteException {
+ return sParts.keySet().toArray(new String[sParts.size()]);
+ }
+
+ public void notifyPartChanged(String key) {
+ synchronized (sParts) {
+ if (sParts.containsKey(key) && sCallbacks.containsKey(key)) {
+ final RemoteCallbackList<IPartChangedCallback> cb = sCallbacks.get(key);
+ int i = cb.beginBroadcast();
+ while (i > 0) {
+ i--;
+ try {
+ cb.getBroadcastItem(i).onPartChanged(sParts.get(key));
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ cb.finishBroadcast();
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ synchronized (sParts) {
+ loadPartsFromResourceLocked(getResources(), R.xml.parts_catalog, sParts);
+ }
+ }
+
+ public void notifyPartChanged(String key) {
+ synchronized (sParts) {
+ if (sParts.containsKey(key) && sCallbacks.containsKey(key)) {
+ final RemoteCallbackList<IPartChangedCallback> cb = sCallbacks.get(key);
+ int i = cb.beginBroadcast();
+ while (i > 0) {
+ i--;
+ try {
+ cb.getBroadcastItem(i).onPartChanged(sParts.get(key));
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ cb.finishBroadcast();
+ }
+ }
+ }
+
+ static final PartInfo getPartInfo(Resources res, String key) {
+ synchronized (sParts) {
+ loadPartsFromResourceLocked(res, R.xml.parts_catalog, sParts);
+ return sParts.get(key);
+ }
+ }
+
+ static final PartInfo getPartInfoForClass(Resources res, String clazz) {
+ synchronized (sParts) {
+ loadPartsFromResourceLocked(res, R.xml.parts_catalog, sParts);
+ for (PartInfo info : sParts.values()) {
+ if (info.getFragmentClass() != null && info.getFragmentClass().equals(clazz)) {
+ return info;
+ }
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ synchronized (sParts) {
+ for (Map.Entry<String, RemoteCallbackList<IPartChangedCallback>> entry : sCallbacks.entrySet()) {
+ if (entry.getValue() != null) {
+ entry.getValue().kill();
+ }
+ }
+ sCallbacks.clear();
+ }
+ }
+
+ private static void loadPartsFromResourceLocked(Resources res, @XmlRes int resid,
+ Map<String, PartInfo> target) {
+ if (mCatalogLoaded.get()) {
+ return;
+ }
+
+ XmlResourceParser parser = null;
+
+ try {
+ parser = res.getXml(resid);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ // Parse next until start tag is found
+ }
+
+ String nodeName = parser.getName();
+ if (!"parts-catalog".equals(nodeName)) {
+ throw new RuntimeException(
+ "XML document must start with <parts-catalog> tag; found"
+ + nodeName + " at " + parser.getPositionDescription());
+ }
+
+ final int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ nodeName = parser.getName();
+ if ("part".equals(nodeName)) {
+ TypedArray sa = res.obtainAttributes(attrs, R.styleable.PartsCatalog);
+
+ String key = null;
+ TypedValue tv = sa.peekValue(R.styleable.PartsCatalog_key);
+ if (tv != null && tv.type == TypedValue.TYPE_STRING) {
+ if (tv.resourceId != 0) {
+ key = res.getString(tv.resourceId);
+ } else {
+ key = String.valueOf(tv.string);
+ }
+ }
+ if (key == null) {
+ throw new RuntimeException("Attribute 'key' is required");
+ }
+
+ final PartInfo info = new PartInfo(key);
+
+ tv = sa.peekValue(R.styleable.PartsCatalog_title);
+ if (tv != null && tv.type == TypedValue.TYPE_STRING) {
+ if (tv.resourceId != 0) {
+ info.setTitle(res.getString(tv.resourceId));
+ } else {
+ info.setTitle(String.valueOf(tv.string));
+ }
+ }
+
+ tv = sa.peekValue(R.styleable.PartsCatalog_summary);
+ if (tv != null && tv.type == TypedValue.TYPE_STRING) {
+ if (tv.resourceId != 0) {
+ info.setSummary(res.getString(tv.resourceId));
+ } else {
+ info.setSummary(String.valueOf(tv.string));
+ }
+ }
+
+ info.setFragmentClass(sa.getString(R.styleable.PartsCatalog_fragment));
+ info.setIconRes(sa.getResourceId(R.styleable.PartsCatalog_icon, 0));
+ sa.recycle();
+
+ target.put(key, info);
+
+ } else {
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Error parsing catalog", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing catalog", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ mCatalogLoaded.set(true);
+ }
+}
diff --git a/src/org/cyanogenmod/cmparts/SettingsPreferenceFragment.java b/src/org/cyanogenmod/cmparts/SettingsPreferenceFragment.java
index f2825e5..f691916 100644
--- a/src/org/cyanogenmod/cmparts/SettingsPreferenceFragment.java
+++ b/src/org/cyanogenmod/cmparts/SettingsPreferenceFragment.java
@@ -714,6 +714,18 @@
getActivity().setResult(result);
}
+ public String getDashboardTitle() {
+ return null;
+ }
+
+ public String getDashboardSummary() {
+ return null;
+ }
+
+ public boolean isAvailable() {
+ return true;
+ }
+
protected final Context getPrefContext() {
return getPreferenceManager().getContext();
}