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
This commit is contained in:
Darrell Shi
2022-01-11 17:45:16 -08:00
parent d2d8485be9
commit b0a0bfdf10
6 changed files with 234 additions and 77 deletions

View File

@@ -31,6 +31,12 @@ import android.content.ComponentName;
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.Looper;
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 android.view.accessibility.AccessibilityEvent;
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 @@ import java.util.function.Consumer;
* </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.
@@ -190,6 +203,11 @@ public class DreamService extends Service implements Window.Callback {
*/
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
@@ -1080,6 +1098,82 @@ public class DreamService extends Service implements Window.Callback {
// 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.
*
@@ -1302,4 +1396,22 @@ public class DreamService extends Service implements Window.Callback {
onWindowCreated(a.getWindow());
}
}
/**
* Represents metadata defined in {@link android.R.styleable#Dream &lt;dream&gt;}.
*
* @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;
}
}
}

View File

@@ -26,8 +26,6 @@ import android.content.pm.PackageManager;
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.provider.Settings;
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 @@ public class DreamBackend {
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 @@ public class DreamBackend {
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 @@ public class DreamBackend {
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));

View File

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

View File

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

View File

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

View File

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