Move dream metadata parsing to framework.
Bug: 214117287
Test: atest com.android.server.dreams.DreamServiceTest
Test: manually on device by opening Screensaver settings because it
calls into the metadata parsing from DreamBackend
Change-Id: Iecb2f48e1f8cbfc2e3d45fd847947fab740577a4
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index bb1f393..d27bc9b 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -31,6 +31,12 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -39,9 +45,11 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.AttributeSet;
import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
+import android.util.Xml;
import android.view.ActionMode;
import android.view.Display;
import android.view.KeyEvent;
@@ -59,7 +67,11 @@
import com.android.internal.util.DumpUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.FileDescriptor;
+import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.function.Consumer;
@@ -159,8 +171,9 @@
* </pre>
*/
public class DreamService extends Service implements Window.Callback {
- private final String mTag =
- DreamService.class.getSimpleName() + "[" + getClass().getSimpleName() + "]";
+ private static final String TAG = DreamService.class.getSimpleName();
+ private final String mTag = TAG + "[" + getClass().getSimpleName() + "]";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
/**
* The name of the dream manager service.
@@ -191,6 +204,11 @@
public static final String DREAM_META_DATA = "android.service.dream";
/**
+ * Name of the root tag under which a Dream defines its metadata in an XML file.
+ */
+ private static final String DREAM_META_DATA_ROOT_TAG = "dream";
+
+ /**
* Extra containing a boolean for whether to show complications on the overlay.
* @hide
*/
@@ -1081,6 +1099,82 @@
// end public api
/**
+ * Parses and returns metadata of the dream service indicated by the service info. Returns null
+ * if metadata cannot be found.
+ *
+ * Note that {@link ServiceInfo} must be fetched with {@link PackageManager#GET_META_DATA} flag.
+ *
+ * @hide
+ */
+ @Nullable
+ public static DreamMetadata getDreamMetadata(Context context, ServiceInfo serviceInfo) {
+ final PackageManager pm = context.getPackageManager();
+
+ final TypedArray rawMetadata = readMetadata(pm, serviceInfo);
+ if (rawMetadata == null) return null;
+
+ final DreamMetadata metadata = new DreamMetadata(
+ convertToComponentName(rawMetadata.getString(
+ com.android.internal.R.styleable.Dream_settingsActivity), serviceInfo),
+ rawMetadata.getDrawable(
+ com.android.internal.R.styleable.Dream_previewImage));
+ rawMetadata.recycle();
+ return metadata;
+ }
+
+ /**
+ * Returns the raw XML metadata fetched from the ${@link ServiceInfo}.
+ *
+ * Returns <code>null</code> if the ${@link ServiceInfo} doesn't contain valid dream metadata.
+ */
+ @Nullable
+ private static TypedArray readMetadata(PackageManager pm, ServiceInfo serviceInfo) {
+ if (serviceInfo == null || serviceInfo.metaData == null) {
+ return null;
+ }
+
+ try (XmlResourceParser parser =
+ serviceInfo.loadXmlMetaData(pm, DreamService.DREAM_META_DATA)) {
+ if (parser == null) {
+ if (DEBUG) Log.w(TAG, "No " + DreamService.DREAM_META_DATA + " metadata");
+ return null;
+ }
+
+ final Resources res = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ while (true) {
+ final int type = parser.next();
+ if (type == XmlPullParser.END_DOCUMENT || type == XmlPullParser.START_TAG) {
+ break;
+ }
+ }
+
+ if (!parser.getName().equals(DREAM_META_DATA_ROOT_TAG)) {
+ if (DEBUG) {
+ Log.w(TAG, "Metadata does not start with " + DREAM_META_DATA_ROOT_TAG + " tag");
+ }
+ return null;
+ }
+
+ return res.obtainAttributes(attrs, com.android.internal.R.styleable.Dream);
+ } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+ if (DEBUG) Log.e(TAG, "Error parsing: " + serviceInfo.packageName, e);
+ return null;
+ }
+ }
+
+ private static ComponentName convertToComponentName(String flattenedString,
+ ServiceInfo serviceInfo) {
+ if (flattenedString == null) return null;
+
+ if (!flattenedString.contains("/")) {
+ return new ComponentName(serviceInfo.packageName, flattenedString);
+ }
+
+ return ComponentName.unflattenFromString(flattenedString);
+ }
+
+ /**
* Called by DreamController.stopDream() when the Dream is about to be unbound and destroyed.
*
* Must run on mHandler.
@@ -1302,4 +1396,22 @@
onWindowCreated(a.getWindow());
}
}
+
+ /**
+ * Represents metadata defined in {@link android.R.styleable#Dream <dream>}.
+ *
+ * @hide
+ */
+ public static final class DreamMetadata {
+ @Nullable
+ public final ComponentName settingsActivity;
+
+ @Nullable
+ public final Drawable previewImage;
+
+ DreamMetadata(ComponentName settingsActivity, Drawable previewImage) {
+ this.settingsActivity = settingsActivity;
+ this.previewImage = previewImage;
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 6bf43e5..98a3386 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -26,8 +26,6 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -35,21 +33,14 @@
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
import android.text.TextUtils;
-import android.util.AttributeSet;
import android.util.Log;
-import android.util.Xml;
import com.android.settingslib.R;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -170,7 +161,7 @@
PackageManager pm = mContext.getPackageManager();
Intent dreamIntent = new Intent(DreamService.SERVICE_INTERFACE);
List<ResolveInfo> resolveInfos = pm.queryIntentServices(dreamIntent,
- PackageManager.GET_META_DATA);
+ PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA));
List<DreamInfo> dreamInfos = new ArrayList<>(resolveInfos.size());
for (ResolveInfo resolveInfo : resolveInfos) {
final ComponentName componentName = getDreamComponentName(resolveInfo);
@@ -185,15 +176,18 @@
dreamInfo.componentName = componentName;
dreamInfo.isActive = dreamInfo.componentName.equals(activeDream);
- final DreamMetadata dreamMetadata = getDreamMetadata(pm, resolveInfo);
- dreamInfo.settingsComponentName = dreamMetadata.mSettingsActivity;
- dreamInfo.previewImage = dreamMetadata.mPreviewImage;
+ final DreamService.DreamMetadata dreamMetadata = DreamService.getDreamMetadata(mContext,
+ resolveInfo.serviceInfo);
+ if (dreamMetadata != null) {
+ dreamInfo.settingsComponentName = dreamMetadata.settingsActivity;
+ dreamInfo.previewImage = dreamMetadata.previewImage;
+ }
if (dreamInfo.previewImage == null) {
dreamInfo.previewImage = mDreamPreviewDefault;
}
dreamInfos.add(dreamInfo);
}
- Collections.sort(dreamInfos, mComparator);
+ dreamInfos.sort(mComparator);
return dreamInfos;
}
@@ -483,67 +477,6 @@
return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
}
- private static final class DreamMetadata {
- @Nullable
- Drawable mPreviewImage;
- @Nullable
- ComponentName mSettingsActivity;
- }
-
- @Nullable
- private static TypedArray readMetadata(PackageManager pm, ServiceInfo serviceInfo) {
- if (serviceInfo == null || serviceInfo.metaData == null) {
- return null;
- }
- try (XmlResourceParser parser =
- serviceInfo.loadXmlMetaData(pm, DreamService.DREAM_META_DATA)) {
- if (parser == null) {
- Log.w(TAG, "No " + DreamService.DREAM_META_DATA + " meta-data");
- return null;
- }
- Resources res = pm.getResourcesForApplication(serviceInfo.applicationInfo);
- AttributeSet attrs = Xml.asAttributeSet(parser);
- while (true) {
- final int type = parser.next();
- if (type == XmlPullParser.END_DOCUMENT || type == XmlPullParser.START_TAG) {
- break;
- }
- }
- String nodeName = parser.getName();
- if (!"dream".equals(nodeName)) {
- Log.w(TAG, "Meta-data does not start with dream tag");
- return null;
- }
- return res.obtainAttributes(attrs, com.android.internal.R.styleable.Dream);
- } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
- Log.w(TAG, "Error parsing : " + serviceInfo.packageName, e);
- return null;
- }
- }
-
- private static ComponentName convertToComponentName(String flattenedString,
- ServiceInfo serviceInfo) {
- if (flattenedString == null) return null;
-
- if (flattenedString.indexOf('/') < 0) {
- flattenedString = serviceInfo.packageName + "/" + flattenedString;
- }
- return ComponentName.unflattenFromString(flattenedString);
- }
-
- private static DreamMetadata getDreamMetadata(PackageManager pm, ResolveInfo resolveInfo) {
- DreamMetadata result = new DreamMetadata();
- if (resolveInfo == null) return result;
- TypedArray rawMetadata = readMetadata(pm, resolveInfo.serviceInfo);
- if (rawMetadata == null) return result;
- result.mSettingsActivity = convertToComponentName(rawMetadata.getString(
- com.android.internal.R.styleable.Dream_settingsActivity), resolveInfo.serviceInfo);
- result.mPreviewImage = rawMetadata.getDrawable(
- com.android.internal.R.styleable.Dream_previewImage);
- rawMetadata.recycle();
- return result;
- }
-
private static void logd(String msg, Object... args) {
if (DEBUG) {
Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args));
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index d9f73d9..53cab9e 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -130,6 +130,19 @@
android:resource="@xml/test_account_type2_authenticator"/>
</service>
+ <service
+ android:name="com.android.server.dreams.TestDreamService"
+ android:exported="false"
+ android:label="Test Dream" >
+ <intent-filter>
+ <action android:name="android.service.dreams.DreamService" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data
+ android:name="android.service.dream"
+ android:resource="@xml/test_dream_metadata" />
+ </service>
+
<receiver android:name="com.android.server.devicepolicy.ApplicationRestrictionsTest$AdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
diff --git a/services/tests/servicestests/res/xml/test_dream_metadata.xml b/services/tests/servicestests/res/xml/test_dream_metadata.xml
new file mode 100644
index 0000000..8d76591
--- /dev/null
+++ b/services/tests/servicestests/res/xml/test_dream_metadata.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.android.server.dreams/.TestDreamSettingsActivity" />
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamServiceTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamServiceTest.java
new file mode 100644
index 0000000..305d6f4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamServiceTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.dreams;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.service.dreams.DreamService;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamServiceTest {
+ @Test
+ public void testMetadataParsing() {
+ final String testDreamServiceComponent = "com.android.server.dreams/.TestDreamService";
+ final String testSettingsActivity = "com.android.server.dreams/.TestDreamSettingsActivity";
+
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ try {
+ final ServiceInfo si = context.getPackageManager().getServiceInfo(
+ ComponentName.unflattenFromString(testDreamServiceComponent),
+ PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA));
+ final DreamService.DreamMetadata metadata = DreamService.getDreamMetadata(context, si);
+
+ assertEquals(0, metadata.settingsActivity.compareTo(
+ ComponentName.unflattenFromString(testSettingsActivity)));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/dreams/TestDreamService.java b/services/tests/servicestests/src/com/android/server/dreams/TestDreamService.java
new file mode 100644
index 0000000..3c99a98
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/TestDreamService.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.dreams;
+
+import android.service.dreams.DreamService;
+
+/**
+ * Dream service implementation for unit testing.
+ */
+public class TestDreamService extends DreamService {
+}