Merge changes If35e3ce4,I6a06a9a0 into main
* changes:
Add WallpaperDescription to WallpaperData and persist description
Add new framework classes for content handling: WallpaperInstance etc.
diff --git a/core/api/current.txt b/core/api/current.txt
index 85b452c8..8bd4367 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9665,6 +9665,47 @@
}
+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 c5bd56f..4b880d0 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -14,3 +14,11 @@
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 0000000..8c959b8
--- /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 0000000..dedcb48
--- /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 0000000..15a15be
--- /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 0000000..48a649b
--- /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 da7da7d..9675d6b 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 0000000..3b3f4a7
--- /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 0000000..93b068d
--- /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 0000000..01c2abf
--- /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 0000000..d5a8937
--- /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 15f86e9..c8d5a03 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.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 @@
/**
* The component name of the currently set live wallpaper.
+ *
+ * @deprecated
*/
private ComponentName mWallpaperComponent;
@@ -179,6 +182,9 @@
*/
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 @@
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 74ca230..cf76bf0 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.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 @@
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 @@
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 @@
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 @@
}
}
+ 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 @@
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 @@
}
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 @@
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 c099517..1ea3674 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.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.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.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 @@
@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 @@
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 @@
}
@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());