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