diff options
14 files changed, 1286 insertions, 62 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 85b452c88887..8bd4367dece8 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9665,6 +9665,47 @@ package android.app.usage { } +package android.app.wallpaper { + + @FlaggedApi("android.app.live_wallpaper_content_handling") public final class WallpaperDescription implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.content.ComponentName getComponent(); + method @NonNull public android.os.PersistableBundle getContent(); + method @Nullable public CharSequence getContextDescription(); + method @Nullable public android.net.Uri getContextUri(); + method @NonNull public java.util.List<java.lang.CharSequence> getDescription(); + method @Nullable public String getId(); + method @Nullable public android.net.Uri getThumbnail(); + method @Nullable public CharSequence getTitle(); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder toBuilder(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @Nullable public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperDescription> CREATOR; + } + + public static final class WallpaperDescription.Builder { + ctor public WallpaperDescription.Builder(); + method @NonNull public android.app.wallpaper.WallpaperDescription build(); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContent(@NonNull android.os.PersistableBundle); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContextDescription(@Nullable CharSequence); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContextUri(@Nullable android.net.Uri); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setDescription(@NonNull java.util.List<java.lang.CharSequence>); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setId(@Nullable String); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setThumbnail(@Nullable android.net.Uri); + method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setTitle(@Nullable CharSequence); + } + + @FlaggedApi("android.app.live_wallpaper_content_handling") public final class WallpaperInstance implements android.os.Parcelable { + ctor public WallpaperInstance(@Nullable android.app.WallpaperInfo, @NonNull android.app.wallpaper.WallpaperDescription); + method public int describeContents(); + method @NonNull public android.app.wallpaper.WallpaperDescription getDescription(); + method @NonNull public String getId(); + method @Nullable public android.app.WallpaperInfo getInfo(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperInstance> CREATOR; + } + +} + package android.appwidget { public class AppWidgetHost { diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig index c5bd56ff67aa..4b880d030413 100644 --- a/core/java/android/app/wallpaper.aconfig +++ b/core/java/android/app/wallpaper.aconfig @@ -14,3 +14,11 @@ flag { description: "Fixes timing of wallpaper changed notification and adds extra information. Only effective after rebooting." bug: "369814294" } + +flag { + name: "live_wallpaper_content_handling" + namespace: "systemui" + description: "Support for user-generated content in live wallpapers. Only effective after rebooting." + bug: "347235611" + is_exported: true +} diff --git a/core/java/android/app/wallpaper/WallpaperDescription.aidl b/core/java/android/app/wallpaper/WallpaperDescription.aidl new file mode 100644 index 000000000000..8c959b8d0172 --- /dev/null +++ b/core/java/android/app/wallpaper/WallpaperDescription.aidl @@ -0,0 +1,20 @@ + +/* +** Copyright 2024, 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 android.app.wallpaper; + +parcelable WallpaperDescription; diff --git a/core/java/android/app/wallpaper/WallpaperDescription.java b/core/java/android/app/wallpaper/WallpaperDescription.java new file mode 100644 index 000000000000..dedcb48f3ad7 --- /dev/null +++ b/core/java/android/app/wallpaper/WallpaperDescription.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2024 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 android.app.wallpaper; + +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; + +import android.annotation.FlaggedApi; +import android.app.WallpaperInfo; +import android.content.ComponentName; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.text.Html; +import android.text.Spanned; +import android.text.SpannedString; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Describes a wallpaper, including associated metadata and optional content to be used by its + * {@link android.service.wallpaper.WallpaperService.Engine}, the {@link ComponentName} to be used + * by {@link android.app.WallpaperManager}, and an optional id to differentiate between different + * distinct wallpapers rendered by the same wallpaper service. + * + * <p>This class is used to communicate among a wallpaper rendering service, a wallpaper chooser UI, + * and {@link android.app.WallpaperManager}. This class describes a specific instance of a live + * wallpaper, unlike {@link WallpaperInfo} which is common to all instances of a wallpaper + * component. Each {@link WallpaperDescription} can have distinct metadata. + * </p> + */ +@FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) +public final class WallpaperDescription implements Parcelable { + private static final String TAG = "WallpaperDescription"; + private static final String XML_TAG_CONTENT = "content"; + private static final String XML_TAG_DESCRIPTION = "description"; + + @Nullable private final ComponentName mComponent; + @Nullable private final String mId; + @Nullable private final Uri mThumbnail; + @Nullable private final CharSequence mTitle; + @NonNull private final List<CharSequence> mDescription; + @Nullable private final Uri mContextUri; + @Nullable private final CharSequence mContextDescription; + @NonNull private final PersistableBundle mContent; + + private WallpaperDescription(@Nullable ComponentName component, + @Nullable String id, @Nullable Uri thumbnail, @Nullable CharSequence title, + @Nullable List<CharSequence> description, @Nullable Uri contextUri, + @Nullable CharSequence contextDescription, + @Nullable PersistableBundle content) { + this.mComponent = component; + this.mId = id; + this.mThumbnail = thumbnail; + this.mTitle = title; + this.mDescription = (description != null) ? description : new ArrayList<>(); + this.mContextUri = contextUri; + this.mContextDescription = contextDescription; + this.mContent = (content != null) ? content : new PersistableBundle(); + } + + /** @return the component for this wallpaper, or {@code null} for a static wallpaper */ + @Nullable public ComponentName getComponent() { + return mComponent; + } + + /** @return the id for this wallpaper, or {@code null} if not provided */ + @Nullable public String getId() { + return mId; + } + + /** @return the thumbnail for this wallpaper, or {@code null} if not provided */ + @Nullable public Uri getThumbnail() { + return mThumbnail; + } + + /** + * @return the title for this wallpaper, with each list element intended to be a separate + * line, or {@code null} if not provided + */ + @Nullable public CharSequence getTitle() { + return mTitle; + } + + /** @return the description for this wallpaper */ + @NonNull + public List<CharSequence> getDescription() { + return new ArrayList<>(); + } + + /** @return the {@link Uri} for the action associated with the wallpaper, or {@code null} if not + * provided */ + @Nullable public Uri getContextUri() { + return mContextUri; + } + + /** @return the description for the action associated with the wallpaper, or {@code null} if not + * provided */ + @Nullable public CharSequence getContextDescription() { + return mContextDescription; + } + + /** @return any additional content required to render this wallpaper */ + @NonNull + public PersistableBundle getContent() { + return mContent; + } + + ////// Comparison overrides + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WallpaperDescription that)) return false; + return Objects.equals(mComponent, that.mComponent) && Objects.equals(mId, + that.mId); + } + + @Override + public int hashCode() { + return Objects.hash(mComponent, mId); + } + + ////// XML storage + + /** @hide */ + public void saveToXml(TypedXmlSerializer out) throws IOException, XmlPullParserException { + if (mComponent != null) { + out.attribute(null, "component", mComponent.flattenToShortString()); + } + if (mId != null) out.attribute(null, "id", mId); + if (mThumbnail != null) out.attribute(null, "thumbnail", mThumbnail.toString()); + if (mTitle != null) out.attribute(null, "title", toHtml(mTitle)); + if (mContextUri != null) out.attribute(null, "contexturi", mContextUri.toString()); + if (mContextDescription != null) { + out.attribute(null, "contextdescription", toHtml(mContextDescription)); + } + out.startTag(null, XML_TAG_DESCRIPTION); + for (CharSequence s : mDescription) out.attribute(null, "descriptionline", toHtml(s)); + out.endTag(null, XML_TAG_DESCRIPTION); + try { + out.startTag(null, XML_TAG_CONTENT); + mContent.saveToXml(out); + } catch (XmlPullParserException e) { + // Be extra conservative and don't fail when writing content since it could come + // from third parties + Log.e(TAG, "unable to convert wallpaper content to XML"); + } finally { + out.endTag(null, XML_TAG_CONTENT); + } + } + + /** @hide */ + public static WallpaperDescription restoreFromXml(TypedXmlPullParser in) throws IOException, + XmlPullParserException { + final int outerDepth = in.getDepth(); + String component = in.getAttributeValue(null, "component"); + ComponentName componentName = (component != null) ? ComponentName.unflattenFromString( + component) : null; + String id = in.getAttributeValue(null, "id"); + String thumbnailString = in.getAttributeValue(null, "thumbnail"); + Uri thumbnail = (thumbnailString != null) ? Uri.parse(thumbnailString) : null; + CharSequence title = fromHtml(in.getAttributeValue(null, "title")); + String contextUriString = in.getAttributeValue(null, "contexturi"); + Uri contextUri = (contextUriString != null) ? Uri.parse(contextUriString) : null; + CharSequence contextDescription = fromHtml( + in.getAttributeValue(null, "contextdescription")); + + List<CharSequence> description = new ArrayList<>(); + PersistableBundle content = null; + int type; + while ((type = in.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + String name = in.getName(); + if (XML_TAG_DESCRIPTION.equals(name)) { + for (int i = 0; i < in.getAttributeCount(); i++) { + description.add(fromHtml(in.getAttributeValue(i))); + } + } else if (XML_TAG_CONTENT.equals(name)) { + content = PersistableBundle.restoreFromXml(in); + } + } + + return new WallpaperDescription(componentName, id, thumbnail, title, description, + contextUri, contextDescription, content); + } + + private static String toHtml(@NonNull CharSequence c) { + Spanned s = (c instanceof Spanned) ? (Spanned) c : new SpannedString(c); + return Html.toHtml(s, Html.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL); + } + + private static CharSequence fromHtml(@Nullable String text) { + if (text == null) { + return null; + } else { + return removeTrailingWhitespace(Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)); + } + } + + // Html.fromHtml and toHtml add a trailing line. This removes it. See + // https://stackoverflow.com/q/9589381 + private static CharSequence removeTrailingWhitespace(CharSequence s) { + if (s == null) return null; + + int end = s.length(); + while (end > 0 && Character.isWhitespace(s.charAt(end - 1))) { + end--; + } + + return s.subSequence(0, end); + } + + ////// Parcelable implementation + + WallpaperDescription(@NonNull Parcel in) { + mComponent = ComponentName.readFromParcel(in); + mId = in.readString8(); + mThumbnail = Uri.CREATOR.createFromParcel(in); + mTitle = in.readCharSequence(); + mDescription = Arrays.stream(in.readCharSequenceArray()).toList(); + mContextUri = Uri.CREATOR.createFromParcel(in); + mContextDescription = in.readCharSequence(); + mContent = PersistableBundle.CREATOR.createFromParcel(in); + } + + @Nullable + public static final Creator<WallpaperDescription> CREATOR = new Creator<>() { + @Override + public WallpaperDescription createFromParcel(Parcel source) { + return new WallpaperDescription(source); + } + + @Override + public WallpaperDescription[] newArray(int size) { + return new WallpaperDescription[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + ComponentName.writeToParcel(mComponent, dest); + dest.writeString8(mId); + Uri.writeToParcel(dest, mThumbnail); + dest.writeCharSequence(mTitle); + dest.writeCharSequenceArray(mDescription.toArray(new CharSequence[0])); + Uri.writeToParcel(dest, mContextUri); + dest.writeCharSequence(mContextDescription); + dest.writePersistableBundle(mContent); + } + + ////// Builder + + /** + * Convert the current description to a {@link Builder}. + * @return the Builder representing this description + */ + @NonNull + public Builder toBuilder() { + return new Builder().setComponent(mComponent).setId(mId).setThumbnail(mThumbnail).setTitle( + mTitle).setDescription(mDescription).setContextUri( + mContextUri).setContextDescription(mContextDescription).setContent(mContent); + } + + /** Builder for the immutable {@link WallpaperDescription} class */ + public static final class Builder { + @Nullable private ComponentName mComponent; + @Nullable private String mId; + @Nullable private Uri mThumbnail; + @Nullable private CharSequence mTitle; + @NonNull private List<CharSequence> mDescription = new ArrayList<>(); + @Nullable private Uri mContextUri; + @Nullable private CharSequence mContextDescription; + @NonNull private PersistableBundle mContent = new PersistableBundle(); + + /** Creates a new, empty {@link Builder}. */ + public Builder() {} + + /** + * Specify the component for this wallpaper. + * + * <p>This method is hidden because only trusted apps should be able to specify the + * component, which names a wallpaper service to be started by the system. + * </p> + * + * @param component component name, or {@code null} for static wallpaper + * @hide + */ + @NonNull + public Builder setComponent(@Nullable ComponentName component) { + mComponent = component; + return this; + } + + /** + * Set the id for this wallpaper. + * + * <p>IDs are used to distinguish among different instances of wallpapers rendered by the + * same component, and should be unique among all wallpapers for that component. + * </p> + * + * @param id the id, or {@code null} for none + */ + @NonNull + public Builder setId(@Nullable String id) { + mId = id; + return this; + } + + /** + * Set the thumbnail Uri for this wallpaper. + * + * @param thumbnail the thumbnail Uri, or {@code null} for none + */ + @NonNull + public Builder setThumbnail(@Nullable Uri thumbnail) { + mThumbnail = thumbnail; + return this; + } + + /** + * Set the title for this wallpaper. + * + * @param title the title, or {@code null} for none + */ + @NonNull + public Builder setTitle(@Nullable CharSequence title) { + mTitle = title; + return this; + } + + /** + * Set the description for this wallpaper. Each array element should be shown on a + * different line. + * + * @param description the description, or an empty list for none + */ + @NonNull + public Builder setDescription(@NonNull List<CharSequence> description) { + mDescription = description; + return this; + } + + /** + * Set the Uri for the action associated with this wallpaper, to be shown as a link with the + * wallpaper information. + * + * @param contextUri the action Uri, or {@code null} for no action + */ + @NonNull + public Builder setContextUri(@Nullable Uri contextUri) { + mContextUri = contextUri; + return this; + } + + /** + * Set the link text for the action associated with this wallpaper. + * + * @param contextDescription the link text, or {@code null} for default text + */ + @NonNull + public Builder setContextDescription(@Nullable CharSequence contextDescription) { + mContextDescription = contextDescription; + return this; + } + + /** + * Set the additional content required to render this wallpaper. + * + * <p>When setting additional content (asset id, etc.), best practice is to set an ID as + * well. This allows WallpaperManager and other code to distinguish between different + * wallpapers handled by this component. + * </p> + * + * @param content additional content, or an empty bundle for none + */ + @NonNull + public Builder setContent(@NonNull PersistableBundle content) { + mContent = content; + return this; + } + + /** Creates and returns the {@link WallpaperDescription} represented by this builder. */ + @NonNull + public WallpaperDescription build() { + return new WallpaperDescription(mComponent, mId, mThumbnail, mTitle, mDescription, + mContextUri, mContextDescription, mContent); + } + } +} diff --git a/core/java/android/app/wallpaper/WallpaperInstance.aidl b/core/java/android/app/wallpaper/WallpaperInstance.aidl new file mode 100644 index 000000000000..15a15bef0d24 --- /dev/null +++ b/core/java/android/app/wallpaper/WallpaperInstance.aidl @@ -0,0 +1,20 @@ + +/* +** Copyright 2024, 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 android.app.wallpaper; + +parcelable WallpaperInstance; diff --git a/core/java/android/app/wallpaper/WallpaperInstance.java b/core/java/android/app/wallpaper/WallpaperInstance.java new file mode 100644 index 000000000000..48a649b87bfb --- /dev/null +++ b/core/java/android/app/wallpaper/WallpaperInstance.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 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 android.app.wallpaper; + +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; + +import android.annotation.FlaggedApi; +import android.app.WallpaperInfo; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +/** + * Describes a wallpaper that has been set as a current wallpaper. + * + * <p>This class is used by {@link android.app.WallpaperManager} to store information about a + * wallpaper that is currently in use. Because it has been set as an active wallpaper it offers + * some guarantees that {@link WallpaperDescription} does not: + * <ul> + * <li>It contains the {@link WallpaperInfo} corresponding to the + * {@link android.content.ComponentName}</li> specified in the description + * <li>{@link #getId()} is guaranteed to be non-null</li> + * </ul> + * </p> + */ +@FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) +public final class WallpaperInstance implements Parcelable { + private static final String DEFAULT_ID = "default_id"; + @Nullable private final WallpaperInfo mInfo; + @NonNull private final WallpaperDescription mDescription; + @Nullable private final String mIdOverride; + + /** + * Create a WallpaperInstance for the wallpaper given by {@link WallpaperDescription}. + * + * @param info the live wallpaper info for this wallpaper, or null if static + * @param description description of the wallpaper for this instance + */ + public WallpaperInstance(@Nullable WallpaperInfo info, + @NonNull WallpaperDescription description) { + this(info, description, null); + } + + /** + * Create a WallpaperInstance for the wallpaper given by {@link WallpaperDescription}. + * + * This is provided as an escape hatch to provide an explicit id for cases where the + * description id and {@link WallpaperInfo} are both {@code null}. + * + * @param info the live wallpaper info for this wallpaper, or null if static + * @param description description of the wallpaper for this instance + * @param idOverride optional id to override the value given in the description + * + * @hide + */ + public WallpaperInstance(@Nullable WallpaperInfo info, + @NonNull WallpaperDescription description, @Nullable String idOverride) { + mInfo = info; + mDescription = description; + mIdOverride = idOverride; + } + + /** @return the live wallpaper info, or {@code null} if static */ + @Nullable public WallpaperInfo getInfo() { + return mInfo; + } + + /** + * See {@link WallpaperDescription.Builder#getId()} for rules about id uniqueness. + * + * @return the ID of the wallpaper instance if given by the wallpaper description, otherwise a + * default value + */ + @NonNull public String getId() { + if (mIdOverride != null) { + return mIdOverride; + } else if (mDescription.getId() != null) { + return mDescription.getId(); + } else if (mInfo != null) { + return mInfo.getComponent().flattenToString(); + } else { + return DEFAULT_ID; + } + } + + /** @return the description for this wallpaper */ + @NonNull public WallpaperDescription getDescription() { + return mDescription; + } + + ////// Comparison overrides + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WallpaperInstance that)) return false; + if (mInfo == null) { + return that.mInfo == null && Objects.equals(getId(), that.getId()); + } else { + return that.mInfo != null + && Objects.equals(mInfo.getComponent(), that.mInfo.getComponent()) + && Objects.equals(getId(), that.getId()); + } + } + + @Override + public int hashCode() { + return (mInfo != null) ? Objects.hash(mInfo.getComponent(), getId()) : Objects.hash( + getId()); + } + + ////// Parcelable implementation + + WallpaperInstance(@NonNull Parcel in) { + mInfo = in.readTypedObject(WallpaperInfo.CREATOR); + mDescription = WallpaperDescription.CREATOR.createFromParcel(in); + mIdOverride = in.readString8(); + } + + @NonNull + public static final Creator<WallpaperInstance> CREATOR = new Creator<>() { + @Override + public WallpaperInstance createFromParcel(Parcel in) { + return new WallpaperInstance(in); + } + + @Override + public WallpaperInstance[] newArray(int size) { + return new WallpaperInstance[size]; + } + }; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mInfo, flags); + mDescription.writeToParcel(dest, flags); + dest.writeString8(mIdOverride); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index da7da7ddc3c5..9675d6bb1aba 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -46,6 +46,7 @@ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.BIND_WALLPAPER"/> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/> @@ -1770,6 +1771,25 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> + + <!-- Used by WallpaperInstanceTest --> + <service + android:name="stub.StubWallpaperService" + android:directBootAware="true" + android:enabled="true" + android:exported="true" + android:label="Stub wallpaper" + android:permission="android.permission.BIND_WALLPAPER"> + + <intent-filter> + <action android:name="android.service.wallpaper.WallpaperService" /> + </intent-filter> + + <!-- Link to XML that defines the wallpaper info. --> + <meta-data + android:name="android.service.wallpaper" + android:resource="@xml/livewallpaper" /> + </service> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/core/tests/coretests/res/xml/livewallpaper.xml b/core/tests/coretests/res/xml/livewallpaper.xml new file mode 100644 index 000000000000..3b3f4a793c63 --- /dev/null +++ b/core/tests/coretests/res/xml/livewallpaper.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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 + --> +<wallpaper + xmlns:android="http://schemas.android.com/apk/res/android" + android:settingsSliceUri="content://com.android.frameworks.coretests/slice" + android:supportsAmbientMode="true"/> diff --git a/core/tests/coretests/src/android/app/wallpaper/OWNERS b/core/tests/coretests/src/android/app/wallpaper/OWNERS new file mode 100644 index 000000000000..93b068dabb4f --- /dev/null +++ b/core/tests/coretests/src/android/app/wallpaper/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/core/java/android/service/wallpaper/OWNERS diff --git a/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java b/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java new file mode 100644 index 000000000000..01c2abf2781b --- /dev/null +++ b/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2024 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 android.app.wallpaper; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.content.ComponentName; +import android.net.Uri; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.util.Xml; + +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@RunWith(JUnit4.class) +public class WallpaperDescriptionTest { + private static final String TAG = "WallpaperDescriptionTest"; + + private final ComponentName mTestComponent = new ComponentName("fakePackage", "fakeClass"); + + @Test + public void equals_ignoresIrrelevantFields() { + String id = "fakeId"; + WallpaperDescription desc1 = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId(id).setTitle("fake one").build(); + WallpaperDescription desc2 = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId(id).setTitle("fake different").build(); + + assertThat(desc1).isEqualTo(desc2); + } + + @Test + public void hash_ignoresIrrelevantFields() { + String id = "fakeId"; + WallpaperDescription desc1 = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId(id).setTitle("fake one").build(); + WallpaperDescription desc2 = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId(id).setTitle("fake different").build(); + + assertThat(desc1.hashCode()).isEqualTo(desc2.hashCode()); + } + + @Test + public void xml_roundTripSucceeds() throws IOException, XmlPullParserException { + final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail"); + final List<CharSequence> description = List.of("line1", "line2"); + final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri"); + final PersistableBundle content = new PersistableBundle(); + content.putString("ckey", "cvalue"); + WallpaperDescription source = new WallpaperDescription.Builder() + .setComponent(mTestComponent).setId("fakeId").setThumbnail(thumbnail) + .setTitle("Fake title").setDescription(description) + .setContextUri(contextUri).setContextDescription("Context description") + .setContent(content).build(); + + ByteArrayOutputStream ostream = new ByteArrayOutputStream(); + TypedXmlSerializer serializer = Xml.newBinarySerializer(); + serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); + serializer.startDocument(null, true); + serializer.startTag(null, "test"); + source.saveToXml(serializer); + serializer.endTag(null, "test"); + serializer.endDocument(); + ostream.close(); + + WallpaperDescription destination = null; + ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); + TypedXmlPullParser parser = Xml.newBinaryPullParser(); + parser.setInput(istream, StandardCharsets.UTF_8.name()); + int type; + do { + type = parser.next(); + if (type == XmlPullParser.START_TAG && "test".equals(parser.getName())) { + destination = WallpaperDescription.restoreFromXml(parser); + } + } while (type != XmlPullParser.END_DOCUMENT); + + assertThat(destination).isNotNull(); + assertThat(destination.getComponent()).isEqualTo(source.getComponent()); + assertThat(destination.getId()).isEqualTo(source.getId()); + assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail()); + assertWithMessage("title mismatch").that( + CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0); + assertThat(destination.getDescription()).hasSize(source.getDescription().size()); + for (int i = 0; i < destination.getDescription().size(); i++) { + CharSequence strDest = destination.getDescription().get(i); + CharSequence strSrc = source.getDescription().get(i); + assertWithMessage("description string mismatch") + .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0); + } + assertThat(destination.getContextUri()).isEqualTo(source.getContextUri()); + assertWithMessage("context description mismatch").that( + CharSequence.compare(destination.getContextDescription(), + source.getContextDescription())).isEqualTo(0); + assertThat(destination.getContent()).isNotNull(); + assertThat(destination.getContent().getString("ckey")).isEqualTo( + source.getContent().getString("ckey")); + } + + @Test + public void parcel_roundTripSucceeds() { + final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail"); + final List<CharSequence> description = List.of("line1", "line2"); + final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri"); + final PersistableBundle content = new PersistableBundle(); + content.putString("ckey", "cvalue"); + WallpaperDescription source = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId("fakeId").setThumbnail(thumbnail).setTitle( + "Fake title").setDescription(description).setContextUri( + contextUri).setContextDescription("Context description").setContent( + content).build(); + + Parcel parcel = Parcel.obtain(); + source.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + WallpaperDescription destination = WallpaperDescription.CREATOR.createFromParcel(parcel); + + assertThat(destination.getComponent()).isEqualTo(source.getComponent()); + assertThat(destination.getId()).isEqualTo(source.getId()); + assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail()); + assertWithMessage("title mismatch").that( + CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0); + assertThat(destination.getDescription()).hasSize(source.getDescription().size()); + for (int i = 0; i < destination.getDescription().size(); i++) { + CharSequence strDest = destination.getDescription().get(i); + CharSequence strSrc = source.getDescription().get(i); + assertWithMessage("description string mismatch") + .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0); + } + assertThat(destination.getContextUri()).isEqualTo(source.getContextUri()); + assertWithMessage("context description mismatch").that( + CharSequence.compare(destination.getContextDescription(), + source.getContextDescription())).isEqualTo(0); + assertThat(destination.getContent()).isNotNull(); + assertThat(destination.getContent().getString("ckey")).isEqualTo( + source.getContent().getString("ckey")); + } + + @Test + public void parcel_roundTripSucceeds_withNulls() { + WallpaperDescription source = new WallpaperDescription.Builder().build(); + + Parcel parcel = Parcel.obtain(); + source.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + WallpaperDescription destination = WallpaperDescription.CREATOR.createFromParcel(parcel); + + assertThat(destination.getComponent()).isEqualTo(source.getComponent()); + assertThat(destination.getId()).isEqualTo(source.getId()); + assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail()); + assertThat(destination.getTitle()).isNull(); + assertThat(destination.getDescription()).hasSize(source.getDescription().size()); + for (int i = 0; i < destination.getDescription().size(); i++) { + CharSequence strDest = destination.getDescription().get(i); + CharSequence strSrc = source.getDescription().get(i); + assertWithMessage("description string mismatch") + .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0); + } + assertThat(destination.getContextUri()).isEqualTo(source.getContextUri()); + assertThat(destination.getContextDescription()).isNull(); + assertThat(destination.getContent()).isNotNull(); + assertThat(destination.getContent().keySet()).isEmpty(); + } + + @Test + public void toBuilder_succeeds() { + final String sourceId = "sourceId"; + final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail"); + final List<CharSequence> description = List.of("line1", "line2"); + final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri"); + final PersistableBundle content = new PersistableBundle(); + content.putString("ckey", "cvalue"); + final String destinationId = "destinationId"; + WallpaperDescription source = new WallpaperDescription.Builder().setComponent( + mTestComponent).setId(sourceId).setThumbnail(thumbnail).setTitle( + "Fake title").setDescription(description).setContextUri( + contextUri).setContextDescription("Context description").setContent( + content).build(); + + WallpaperDescription destination = source.toBuilder().setId(destinationId).build(); + + assertThat(destination.getComponent()).isEqualTo(source.getComponent()); + assertThat(destination.getId()).isEqualTo(destinationId); + assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail()); + assertWithMessage("title mismatch").that( + CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0); + assertThat(destination.getDescription()).hasSize(source.getDescription().size()); + for (int i = 0; i < destination.getDescription().size(); i++) { + CharSequence strDest = destination.getDescription().get(i); + CharSequence strSrc = source.getDescription().get(i); + assertWithMessage("description string mismatch") + .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0); + } + assertThat(destination.getContextUri()).isEqualTo(source.getContextUri()); + assertWithMessage("context description mismatch").that( + CharSequence.compare(destination.getContextDescription(), + source.getContextDescription())).isEqualTo(0); + assertThat(destination.getContent()).isNotNull(); + assertThat(destination.getContent().getString("ckey")).isEqualTo( + source.getContent().getString("ckey")); + } +} diff --git a/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java b/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java new file mode 100644 index 000000000000..d5a893717ad1 --- /dev/null +++ b/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2024 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 android.app.wallpaper; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.WallpaperInfo; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Parcel; +import android.service.wallpaper.WallpaperService; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +@RunWith(JUnit4.class) +public class WallpaperInstanceTest { + @Test + public void equals_bothNullInfo_sameId_isTrue() { + WallpaperDescription description = new WallpaperDescription.Builder().setId("123").build(); + WallpaperInstance instance1 = new WallpaperInstance(null, description); + WallpaperInstance instance2 = new WallpaperInstance(null, description); + + assertThat(instance1).isEqualTo(instance2); + } + + @Test + public void equals_bothNullInfo_differentIds_isFalse() { + WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build(); + WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build(); + WallpaperInstance instance1 = new WallpaperInstance(null, description1); + WallpaperInstance instance2 = new WallpaperInstance(null, description2); + + assertThat(instance1).isNotEqualTo(instance2); + } + + @Test + public void equals_singleNullInfo_isFalse() throws Exception { + WallpaperDescription description = new WallpaperDescription.Builder().build(); + WallpaperInstance instance1 = new WallpaperInstance(null, description); + WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description); + + assertThat(instance1).isNotEqualTo(instance2); + } + + @Test + public void equals_sameInfoAndId_isTrue() throws Exception { + WallpaperDescription description = new WallpaperDescription.Builder().setId("123").build(); + WallpaperInstance instance1 = new WallpaperInstance(makeWallpaperInfo(), description); + WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description); + + assertThat(instance1).isEqualTo(instance2); + } + + @Test + public void equals_sameInfo_differentIds_isFalse() throws Exception { + WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build(); + WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build(); + WallpaperInstance instance1 = new WallpaperInstance(makeWallpaperInfo(), description1); + WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description2); + + assertThat(instance1).isNotEqualTo(instance2); + } + + @Test + public void hash_nullInfo_works() { + WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build(); + WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build(); + WallpaperInstance base = new WallpaperInstance(null, description1); + WallpaperInstance sameId = new WallpaperInstance(null, description1); + WallpaperInstance differentId = new WallpaperInstance(null, description2); + + assertThat(base.hashCode()).isEqualTo(sameId.hashCode()); + assertThat(base.hashCode()).isNotEqualTo(differentId.hashCode()); + } + + @Test + public void hash_withInfo_works() throws Exception { + WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build(); + WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build(); + WallpaperInstance base = new WallpaperInstance(makeWallpaperInfo(), description1); + WallpaperInstance sameId = new WallpaperInstance(makeWallpaperInfo(), description1); + WallpaperInstance differentId = new WallpaperInstance(makeWallpaperInfo(), description2); + + assertThat(base.hashCode()).isEqualTo(sameId.hashCode()); + assertThat(base.hashCode()).isNotEqualTo(differentId.hashCode()); + } + + @Test + public void id_fromOverride() throws Exception { + final String id = "override"; + WallpaperInstance instance = new WallpaperInstance(makeWallpaperInfo(), + new WallpaperDescription.Builder().setId("abc123").build(), id); + + assertThat(instance.getId()).isEqualTo(id); + } + + @Test + public void id_fromDescription() throws Exception { + final String id = "abc123"; + WallpaperInstance instance = new WallpaperInstance(makeWallpaperInfo(), + new WallpaperDescription.Builder().setId(id).build()); + + assertThat(instance.getId()).isEqualTo(id); + } + + @Test + public void id_fromComponent() throws Exception { + WallpaperInfo info = makeWallpaperInfo(); + WallpaperInstance instance = new WallpaperInstance(info, + new WallpaperDescription.Builder().build()); + + assertThat(instance.getId()).isEqualTo(info.getComponent().flattenToString()); + } + + @Test + public void id_default() { + WallpaperInstance instance = new WallpaperInstance(null, + new WallpaperDescription.Builder().build()); + + assertThat(instance.getId()).isNotNull(); + } + + @Test + public void parcel_roundTripSucceeds() throws Exception { + WallpaperInstance source = new WallpaperInstance(makeWallpaperInfo(), + new WallpaperDescription.Builder().build()); + + Parcel parcel = Parcel.obtain(); + source.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + + WallpaperInstance destination = WallpaperInstance.CREATOR.createFromParcel(parcel); + + assertThat(destination.getInfo()).isNotNull(); + assertThat(destination.getInfo().getComponent()).isEqualTo(source.getInfo().getComponent()); + assertThat(destination.getId()).isEqualTo(source.getId()); + assertThat(destination.getDescription()).isEqualTo(source.getDescription()); + } + + @Test + public void parcel_roundTripSucceeds_withNulls() { + WallpaperInstance source = new WallpaperInstance(null, + new WallpaperDescription.Builder().build()); + + Parcel parcel = Parcel.obtain(); + source.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + + WallpaperInstance destination = WallpaperInstance.CREATOR.createFromParcel(parcel); + + assertThat(destination.getInfo()).isEqualTo(source.getInfo()); + assertThat(destination.getId()).isEqualTo(source.getId()); + assertThat(destination.getDescription()).isEqualTo(source.getDescription()); + } + + private WallpaperInfo makeWallpaperInfo() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); + intent.setPackage("com.android.frameworks.coretests"); + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> result = pm.queryIntentServices(intent, PackageManager.GET_META_DATA); + assertThat(result).hasSize(1); + ResolveInfo info = result.getFirst(); + return new WallpaperInfo(context, info); + } +} diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index 15f86e9c08ff..c8d5a0332a4f 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -29,6 +29,7 @@ import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; import android.app.WallpaperManager.ScreenOrientation; import android.app.WallpaperManager.SetWallpaperFlags; +import android.app.wallpaper.WallpaperDescription; import android.content.ComponentName; import android.graphics.Rect; import android.os.RemoteCallbackList; @@ -77,6 +78,8 @@ class WallpaperData { /** * The component name of the currently set live wallpaper. + * + * @deprecated */ private ComponentName mWallpaperComponent; @@ -179,6 +182,9 @@ class WallpaperData { */ int mOrientationWhenSet = ORIENTATION_UNKNOWN; + /** Description of the current wallpaper */ + private WallpaperDescription mDescription; + WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) { this.userId = userId; this.mWhich = wallpaperType; @@ -238,6 +244,14 @@ class WallpaperData { this.mWallpaperComponent = componentName; } + WallpaperDescription getDescription() { + return mDescription; + } + + void setDescription(WallpaperDescription description) { + this.mDescription = description; + } + @Override public String toString() { StringBuilder out = new StringBuilder(defaultString(this)); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 74ca23038666..cf76bf05ab19 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -16,6 +16,7 @@ package com.android.server.wallpaper; +import static android.app.Flags.liveWallpaperContentHandling; import static android.app.Flags.removeNextWallpaperComponent; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; @@ -30,11 +31,13 @@ import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked; import static com.android.window.flags.Flags.multiCrop; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WallpaperColors; import android.app.WallpaperManager; import android.app.WallpaperManager.SetWallpaperFlags; import android.app.backup.WallpaperBackupHelper; +import android.app.wallpaper.WallpaperDescription; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -194,9 +197,16 @@ public class WallpaperDataParser { if (loadSystem) { if (!success) { + // Set safe values that won't cause crashes wallpaper.cropHint.set(0, 0, 0, 0); wpdData.mPadding.set(0, 0, 0, 0); wallpaper.name = ""; + if (liveWallpaperContentHandling()) { + wallpaper.setDescription(new WallpaperDescription.Builder().setComponent( + mImageWallpaper).build()); + } else { + wallpaper.setComponent(mImageWallpaper); + } } else { if (wallpaper.wallpaperId <= 0) { wallpaper.wallpaperId = makeWallpaperIdLocked(); @@ -245,25 +255,11 @@ public class WallpaperDataParser { parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); } - String comp = parser.getAttributeValue(null, "component"); + ComponentName comp = parseComponentName(parser); if (removeNextWallpaperComponent()) { - wallpaperToParse.setComponent(comp != null - ? ComponentName.unflattenFromString(comp) - : null); - if (wallpaperToParse.getComponent() == null - || "android".equals(wallpaperToParse.getComponent() - .getPackageName())) { - wallpaperToParse.setComponent(mImageWallpaper); - } + wallpaperToParse.setComponent(comp); } else { - wallpaperToParse.nextWallpaperComponent = comp != null - ? ComponentName.unflattenFromString(comp) - : null; - if (wallpaperToParse.nextWallpaperComponent == null - || "android".equals(wallpaperToParse.nextWallpaperComponent - .getPackageName())) { - wallpaperToParse.nextWallpaperComponent = mImageWallpaper; - } + wallpaperToParse.nextWallpaperComponent = comp; } if (multiCrop()) { @@ -290,6 +286,17 @@ public class WallpaperDataParser { return lockWallpaper; } + @NonNull + private ComponentName parseComponentName(TypedXmlPullParser parser) { + String comp = parser.getAttributeValue(null, "component"); + ComponentName c = (comp != null) ? ComponentName.unflattenFromString(comp) : null; + if (c == null || "android".equals(c.getPackageName())) { + c = mImageWallpaper; + } + + return c; + } + private void ensureSaneWallpaperData(WallpaperData wallpaper) { // Only overwrite cropHint if the rectangle is invalid. if (wallpaper.cropHint.width() < 0 @@ -332,9 +339,29 @@ public class WallpaperDataParser { } } + void parseWallpaperDescription(TypedXmlPullParser parser, WallpaperData wallpaper) + throws XmlPullParserException, IOException { + + int type = parser.next(); + if (type == XmlPullParser.START_TAG && "description".equals(parser.getName())) { + // Always read the description if it's there - there may be one from a previous save + // with content handling enabled even if it's enabled now + WallpaperDescription description = WallpaperDescription.restoreFromXml(parser); + if (liveWallpaperContentHandling()) { + // null component means that wallpaper was last saved without content handling, so + // populate description from saved component + if (description.getComponent() == null) { + description = description.toBuilder().setComponent( + parseComponentName(parser)).build(); + } + wallpaper.setDescription(description); + } + } + } + @VisibleForTesting void parseWallpaperAttributes(TypedXmlPullParser parser, WallpaperData wallpaper, - boolean keepDimensionHints) throws XmlPullParserException { + boolean keepDimensionHints) throws XmlPullParserException, IOException { final int id = parser.getAttributeInt(null, "id", -1); if (id != -1) { wallpaper.wallpaperId = id; @@ -355,8 +382,7 @@ public class WallpaperDataParser { getAttributeInt(parser, "totalCropTop", 0), getAttributeInt(parser, "totalCropRight", 0), getAttributeInt(parser, "totalCropBottom", 0)); - ComponentName componentName = removeNextWallpaperComponent() ? wallpaper.getComponent() - : wallpaper.nextWallpaperComponent; + ComponentName componentName = parseComponentName(parser); if (multiCrop() && mImageWallpaper.equals(componentName)) { wallpaper.mCropHints = new SparseArray<>(); for (Pair<Integer, String> pair: screenDimensionPairs()) { @@ -443,6 +469,15 @@ public class WallpaperDataParser { } wallpaper.name = parser.getAttributeValue(null, "name"); wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false); + + parseWallpaperDescription(parser, wallpaper); + if (liveWallpaperContentHandling() && wallpaper.getDescription().getComponent() == null) { + // The last save was done before the content handling flag was enabled and has no + // WallpaperDescription, so create a default one with the correct component. + // CSP: log boot after flag change to false -> true + wallpaper.setDescription( + new WallpaperDescription.Builder().setComponent(componentName).build()); + } } private static int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) { @@ -610,9 +645,27 @@ public class WallpaperDataParser { out.attributeBoolean(null, "backup", true); } + writeWallpaperDescription(out, wallpaper); + out.endTag(null, tag); } + void writeWallpaperDescription(TypedXmlSerializer out, WallpaperData wallpaper) + throws IOException { + if (liveWallpaperContentHandling()) { + WallpaperDescription description = wallpaper.getDescription(); + if (description != null) { + String descriptionTag = "description"; + out.startTag(null, descriptionTag); + try { + description.saveToXml(out); + } catch (XmlPullParserException e) { + Slog.e(TAG, "Error writing wallpaper description", e); + } + out.endTag(null, descriptionTag); + } + } + } // Restore the named resource bitmap to both source + crop files boolean restoreNamedResourceLocked(WallpaperData wallpaper) { if (wallpaper.name.length() > 4 && "res:".equals(wallpaper.name.substring(0, 4))) { diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index c099517475c2..1ea36748eb5d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -40,7 +40,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -55,6 +54,7 @@ import android.app.AppOpsManager; import android.app.Flags; import android.app.WallpaperColors; import android.app.WallpaperManager; +import android.app.wallpaper.WallpaperDescription; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -65,6 +65,7 @@ import android.content.pm.ServiceInfo; import android.graphics.Color; import android.hardware.display.DisplayManager; import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.DisableFlags; @@ -426,7 +427,8 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) - public void testSaveLoadSettings() { + public void testSaveLoadSettings_withoutWallpaperDescription() + throws IOException, XmlPullParserException { WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); expectedData.setComponent(sDefaultWallpaperComponent); expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED), @@ -436,27 +438,19 @@ public class WallpaperManagerServiceTests { expectedData.mUidToDimAmount.put(1, 0.4f); ByteArrayOutputStream ostream = new ByteArrayOutputStream(); - try { - TypedXmlSerializer serializer = Xml.newBinarySerializer(); - serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); - mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null); - ostream.close(); - } catch (IOException e) { - fail("exception occurred while writing system wallpaper attributes"); - } + TypedXmlSerializer serializer = Xml.newBinarySerializer(); + serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null); + ostream.close(); WallpaperData actualData = new WallpaperData(0, FLAG_SYSTEM); - try { - ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); - TypedXmlPullParser parser = Xml.newBinaryPullParser(); - parser.setInput(istream, StandardCharsets.UTF_8.name()); - mService.mWallpaperDataParser.loadSettingsFromSerializer(parser, - actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */ - false, /* keepDimensionHints= */ true, - new WallpaperDisplayHelper.DisplayData(0)); - } catch (IOException | XmlPullParserException e) { - fail("exception occurred while parsing wallpaper"); - } + ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); + TypedXmlPullParser parser = Xml.newBinaryPullParser(); + parser.setInput(istream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.loadSettingsFromSerializer(parser, + actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */ + false, /* keepDimensionHints= */ true, + new WallpaperDisplayHelper.DisplayData(0)); assertThat(actualData.getComponent()).isEqualTo(expectedData.getComponent()); assertThat(actualData.primaryColors).isEqualTo(expectedData.primaryColors); @@ -472,33 +466,58 @@ public class WallpaperManagerServiceTests { } @Test + @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) + public void testSaveLoadSettings_withWallpaperDescription() + throws IOException, XmlPullParserException { + WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); + expectedData.setComponent(sDefaultWallpaperComponent); + PersistableBundle content = new PersistableBundle(); + content.putString("ckey", "cvalue"); + WallpaperDescription description = new WallpaperDescription.Builder() + .setComponent(sDefaultWallpaperComponent).setId("testId").setTitle("fake one") + .setContent(content).build(); + expectedData.setDescription(description); + + ByteArrayOutputStream ostream = new ByteArrayOutputStream(); + TypedXmlSerializer serializer = Xml.newBinarySerializer(); + serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null); + ostream.close(); + + WallpaperData actualData = new WallpaperData(0, FLAG_SYSTEM); + ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); + TypedXmlPullParser parser = Xml.newBinaryPullParser(); + parser.setInput(istream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.loadSettingsFromSerializer(parser, + actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */ + false, /* keepDimensionHints= */ true, + new WallpaperDisplayHelper.DisplayData(0)); + + assertThat(actualData.getComponent()).isEqualTo(expectedData.getComponent()); + assertThat(actualData.getDescription()).isEqualTo(expectedData.getDescription()); + } + + @Test @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) - public void testSaveLoadSettings_legacyNextComponent() { + public void testSaveLoadSettings_legacyNextComponent() + throws IOException, XmlPullParserException { WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); systemWallpaperData.setComponent(sDefaultWallpaperComponent); ByteArrayOutputStream ostream = new ByteArrayOutputStream(); - try { - TypedXmlSerializer serializer = Xml.newBinarySerializer(); - serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); - mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, systemWallpaperData, - null); - ostream.close(); - } catch (IOException e) { - fail("exception occurred while writing system wallpaper attributes"); - } + TypedXmlSerializer serializer = Xml.newBinarySerializer(); + serializer.setOutput(ostream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, systemWallpaperData, + null); + ostream.close(); WallpaperData shouldMatchSystem = new WallpaperData(0, FLAG_SYSTEM); - try { - ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); - TypedXmlPullParser parser = Xml.newBinaryPullParser(); - parser.setInput(istream, StandardCharsets.UTF_8.name()); - mService.mWallpaperDataParser.loadSettingsFromSerializer(parser, - shouldMatchSystem, /* userId= */0, /* loadSystem= */ true, /* loadLock= */ - false, /* keepDimensionHints= */ true, - new WallpaperDisplayHelper.DisplayData(0)); - } catch (IOException | XmlPullParserException e) { - fail("exception occurred while parsing wallpaper"); - } + ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray()); + TypedXmlPullParser parser = Xml.newBinaryPullParser(); + parser.setInput(istream, StandardCharsets.UTF_8.name()); + mService.mWallpaperDataParser.loadSettingsFromSerializer(parser, + shouldMatchSystem, /* userId= */0, /* loadSystem= */ true, /* loadLock= */ + false, /* keepDimensionHints= */ true, + new WallpaperDisplayHelper.DisplayData(0)); assertThat(shouldMatchSystem.nextWallpaperComponent).isEqualTo( systemWallpaperData.getComponent()); |