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