1901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk/*
2901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * Copyright 2017 The Android Open Source Project
3901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk *
4901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * Licensed under the Apache License, Version 2.0 (the "License");
5901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * you may not use this file except in compliance with the License.
6901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * You may obtain a copy of the License at
7901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk *
8901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk *      http://www.apache.org/licenses/LICENSE-2.0
9901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk *
10901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * Unless required by applicable law or agreed to in writing, software
11901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * distributed under the License is distributed on an "AS IS" BASIS,
12901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * See the License for the specific language governing permissions and
14901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * limitations under the License.
15901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk */
16901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
1785ef1446b82c8783a50af92c4cb1389fe0d0e907Aurimas Liutikaspackage androidx.slice;
18901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
19901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport static org.xmlpull.v1.XmlPullParser.START_TAG;
20901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport static org.xmlpull.v1.XmlPullParser.TEXT;
21901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
22901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.annotation.SuppressLint;
2307a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.content.ContentResolver;
24901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.content.Context;
2507a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.content.pm.PackageManager;
2607a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.content.res.Resources;
27901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.graphics.Bitmap;
2807a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.graphics.BitmapFactory;
2907a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.graphics.Canvas;
30901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.graphics.drawable.Drawable;
3107a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.graphics.drawable.Icon;
32901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.net.Uri;
3338db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monkimport android.os.Build;
34901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.text.Html;
35901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.text.Spanned;
36901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport android.text.TextUtils;
3707a4a56611cc044fd48b052db05aea332201216eJason Monkimport android.util.Base64;
38901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
393ec422a2e2a46b51d4cc6926fcaa35caacbdf98dJason Monkimport androidx.annotation.RestrictTo;
403ec422a2e2a46b51d4cc6926fcaa35caacbdf98dJason Monkimport androidx.core.graphics.drawable.IconCompat;
4107a4a56611cc044fd48b052db05aea332201216eJason Monkimport androidx.core.util.Consumer;
423ec422a2e2a46b51d4cc6926fcaa35caacbdf98dJason Monk
43901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport org.xmlpull.v1.XmlPullParser;
44901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport org.xmlpull.v1.XmlPullParserException;
45901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport org.xmlpull.v1.XmlPullParserFactory;
46901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport org.xmlpull.v1.XmlSerializer;
47901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
4807a4a56611cc044fd48b052db05aea332201216eJason Monkimport java.io.ByteArrayOutputStream;
49901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport java.io.IOException;
50901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport java.io.InputStream;
51901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport java.io.OutputStream;
52901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkimport java.util.List;
53901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
54901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk/**
55901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk * @hide
56901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk */
57901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk@RestrictTo(RestrictTo.Scope.LIBRARY)
58901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monkclass SliceXml {
59901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
60901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String NAMESPACE = null;
61901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
62901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String TAG_SLICE = "slice";
6307a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String TAG_ACTION = "action";
64901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String TAG_ITEM = "item";
65901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
66901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String ATTR_URI = "uri";
67901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String ATTR_FORMAT = "format";
68901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String ATTR_SUBTYPE = "subtype";
69901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static final String ATTR_HINTS = "hints";
7007a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ATTR_ICON_TYPE = "iconType";
7107a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ATTR_ICON_PACKAGE = "pkg";
7207a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ATTR_ICON_RES_TYPE = "resType";
73901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
7407a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ICON_TYPE_RES = "res";
7507a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ICON_TYPE_URI = "uri";
7607a4a56611cc044fd48b052db05aea332201216eJason Monk    private static final String ICON_TYPE_DEFAULT = "def";
7707a4a56611cc044fd48b052db05aea332201216eJason Monk
7807a4a56611cc044fd48b052db05aea332201216eJason Monk    public static Slice parseSlice(Context context, InputStream input,
7907a4a56611cc044fd48b052db05aea332201216eJason Monk            String encoding, SliceUtils.SliceActionListener listener)
8007a4a56611cc044fd48b052db05aea332201216eJason Monk            throws IOException, SliceUtils.SliceParseException {
81901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        try {
82901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
83901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            parser.setInput(input, encoding);
84901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
85901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            int outerDepth = parser.getDepth();
86901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            int type;
87901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            Slice s = null;
88901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
89901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
90901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                if (type != START_TAG) {
91901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    continue;
92901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                }
9307a4a56611cc044fd48b052db05aea332201216eJason Monk                s = parseSlice(context, parser, listener);
94901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            }
95901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            return s;
96901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        } catch (XmlPullParserException e) {
97901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            throw new IOException("Unable to init XML Serialization", e);
98901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
99901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
100901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
101901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    @SuppressLint("WrongConstant")
10207a4a56611cc044fd48b052db05aea332201216eJason Monk    private static Slice parseSlice(Context context, XmlPullParser parser,
10307a4a56611cc044fd48b052db05aea332201216eJason Monk            SliceUtils.SliceActionListener listener)
10407a4a56611cc044fd48b052db05aea332201216eJason Monk            throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
10507a4a56611cc044fd48b052db05aea332201216eJason Monk        if (!TAG_SLICE.equals(parser.getName()) && !TAG_ACTION.equals(parser.getName())) {
106901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            throw new IOException("Unexpected tag " + parser.getName());
107901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
108901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        int outerDepth = parser.getDepth();
109901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        int type;
110901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String uri = parser.getAttributeValue(NAMESPACE, ATTR_URI);
111901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        Slice.Builder b = new Slice.Builder(Uri.parse(uri));
112901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String[] hints = hints(parser.getAttributeValue(NAMESPACE, ATTR_HINTS));
113901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        b.addHints(hints);
114901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
115901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
116901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
117901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            if (type == START_TAG && TAG_ITEM.equals(parser.getName())) {
11807a4a56611cc044fd48b052db05aea332201216eJason Monk                parseItem(context, b, parser, listener);
119901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            }
120901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
121901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        return b.build();
122901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
123901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
12445e6d8a60387c26a7f7f26c209f54ef3023fc638Aurimas Liutikas    @SuppressLint("DefaultCharset")
12507a4a56611cc044fd48b052db05aea332201216eJason Monk    private static void parseItem(Context context, Slice.Builder b,
12607a4a56611cc044fd48b052db05aea332201216eJason Monk            XmlPullParser parser, final SliceUtils.SliceActionListener listener)
12707a4a56611cc044fd48b052db05aea332201216eJason Monk            throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
128901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        int type;
129901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        int outerDepth = parser.getDepth();
130901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT);
131901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE);
132901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS);
13307a4a56611cc044fd48b052db05aea332201216eJason Monk        String iconType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_TYPE);
13407a4a56611cc044fd48b052db05aea332201216eJason Monk        String pkg = parser.getAttributeValue(NAMESPACE, ATTR_ICON_PACKAGE);
13507a4a56611cc044fd48b052db05aea332201216eJason Monk        String resType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_RES_TYPE);
13645e6d8a60387c26a7f7f26c209f54ef3023fc638Aurimas Liutikas        @Slice.SliceHint String[] hints = hints(hintStr);
137901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String v;
138901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
139901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
140901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            if (type == TEXT) {
141901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                switch (format) {
142901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
143901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        // Nothing for now.
144901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        break;
145901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    case android.app.slice.SliceItem.FORMAT_IMAGE:
14607a4a56611cc044fd48b052db05aea332201216eJason Monk                        switch (iconType) {
14707a4a56611cc044fd48b052db05aea332201216eJason Monk                            case ICON_TYPE_RES:
14807a4a56611cc044fd48b052db05aea332201216eJason Monk                                String resName = parser.getText();
14907a4a56611cc044fd48b052db05aea332201216eJason Monk                                try {
15007a4a56611cc044fd48b052db05aea332201216eJason Monk                                    Resources r = context.getPackageManager()
15107a4a56611cc044fd48b052db05aea332201216eJason Monk                                                .getResourcesForApplication(pkg);
15207a4a56611cc044fd48b052db05aea332201216eJason Monk                                    int id = r.getIdentifier(resName, resType, pkg);
15307a4a56611cc044fd48b052db05aea332201216eJason Monk                                    if (id != 0) {
15407a4a56611cc044fd48b052db05aea332201216eJason Monk                                        b.addIcon(IconCompat.createWithResource(
15507a4a56611cc044fd48b052db05aea332201216eJason Monk                                                context.createPackageContext(pkg, 0), id), subtype,
15607a4a56611cc044fd48b052db05aea332201216eJason Monk                                                hints);
15707a4a56611cc044fd48b052db05aea332201216eJason Monk                                    } else {
15807a4a56611cc044fd48b052db05aea332201216eJason Monk                                        throw new SliceUtils.SliceParseException(
15907a4a56611cc044fd48b052db05aea332201216eJason Monk                                                "Cannot find resource " + pkg + ":" + resType
16007a4a56611cc044fd48b052db05aea332201216eJason Monk                                                        + "/" + resName);
16107a4a56611cc044fd48b052db05aea332201216eJason Monk                                    }
16207a4a56611cc044fd48b052db05aea332201216eJason Monk                                } catch (PackageManager.NameNotFoundException e) {
16307a4a56611cc044fd48b052db05aea332201216eJason Monk                                    throw new SliceUtils.SliceParseException(
16407a4a56611cc044fd48b052db05aea332201216eJason Monk                                            "Invalid icon package " + pkg, e);
16507a4a56611cc044fd48b052db05aea332201216eJason Monk                                }
16607a4a56611cc044fd48b052db05aea332201216eJason Monk                                break;
16707a4a56611cc044fd48b052db05aea332201216eJason Monk                            case ICON_TYPE_URI:
16807a4a56611cc044fd48b052db05aea332201216eJason Monk                                v = parser.getText();
16907a4a56611cc044fd48b052db05aea332201216eJason Monk                                b.addIcon(IconCompat.createWithContentUri(v), subtype, hints);
17007a4a56611cc044fd48b052db05aea332201216eJason Monk                                break;
17107a4a56611cc044fd48b052db05aea332201216eJason Monk                            default:
17207a4a56611cc044fd48b052db05aea332201216eJason Monk                                v = parser.getText();
17307a4a56611cc044fd48b052db05aea332201216eJason Monk                                byte[] data = Base64.decode(v, Base64.NO_WRAP);
17407a4a56611cc044fd48b052db05aea332201216eJason Monk                                Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
1753ec422a2e2a46b51d4cc6926fcaa35caacbdf98dJason Monk                                b.addIcon(IconCompat.createWithBitmap(image), subtype, hints);
17607a4a56611cc044fd48b052db05aea332201216eJason Monk                                break;
177901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        }
178901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        break;
179901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    case android.app.slice.SliceItem.FORMAT_INT:
180901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        v = parser.getText();
181901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        b.addInt(Integer.parseInt(v), subtype, hints);
182901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        break;
183901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    case android.app.slice.SliceItem.FORMAT_TEXT:
184901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        v = parser.getText();
18538db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
18638db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                            // 19-21 don't allow special characters in XML, so we base64 encode it.
18738db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                            v = new String(Base64.decode(v, Base64.NO_WRAP));
18838db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        }
189901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        b.addText(Html.fromHtml(v), subtype, hints);
190901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        break;
19107a4a56611cc044fd48b052db05aea332201216eJason Monk                    case android.app.slice.SliceItem.FORMAT_LONG:
192901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        v = parser.getText();
19307a4a56611cc044fd48b052db05aea332201216eJason Monk                        b.addLong(Long.parseLong(v), subtype, hints);
194901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        break;
195901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    default:
196901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                        throw new IllegalArgumentException("Unrecognized format " + format);
197901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                }
198901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            } else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) {
19907a4a56611cc044fd48b052db05aea332201216eJason Monk                b.addSubSlice(parseSlice(context, parser, listener), subtype);
20007a4a56611cc044fd48b052db05aea332201216eJason Monk            } else if (type == START_TAG && TAG_ACTION.equals(parser.getName())) {
20107a4a56611cc044fd48b052db05aea332201216eJason Monk                b.addAction(new Consumer<Uri>() {
20207a4a56611cc044fd48b052db05aea332201216eJason Monk                    @Override
20307a4a56611cc044fd48b052db05aea332201216eJason Monk                    public void accept(Uri uri) {
20407a4a56611cc044fd48b052db05aea332201216eJason Monk                        listener.onSliceAction(uri);
20507a4a56611cc044fd48b052db05aea332201216eJason Monk                    }
20607a4a56611cc044fd48b052db05aea332201216eJason Monk                }, parseSlice(context, parser, listener), subtype);
207901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            }
208901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
209901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
210901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
21145e6d8a60387c26a7f7f26c209f54ef3023fc638Aurimas Liutikas    @Slice.SliceHint
212901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static String[] hints(String hintStr) {
213901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        return TextUtils.isEmpty(hintStr) ? new String[0] : hintStr.split(",");
214901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
215901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
216901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    public static void serializeSlice(Slice s, Context context, OutputStream output,
217901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            String encoding, SliceUtils.SerializeOptions options) throws IOException {
218901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        try {
219901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
220901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.setOutput(output, encoding);
221901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.startDocument(encoding, null);
222901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
22307a4a56611cc044fd48b052db05aea332201216eJason Monk            serialize(s, context, options, serializer, false, null);
224901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
225901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.endDocument();
226901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.flush();
227901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        } catch (XmlPullParserException e) {
228901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            throw new IOException("Unable to init XML Serialization", e);
229901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
230901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
231901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
232901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options,
23307a4a56611cc044fd48b052db05aea332201216eJason Monk            XmlSerializer serializer, boolean isAction, String subType) throws IOException {
23407a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.startTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
235901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString());
23607a4a56611cc044fd48b052db05aea332201216eJason Monk        if (subType != null) {
23707a4a56611cc044fd48b052db05aea332201216eJason Monk            serializer.attribute(NAMESPACE, ATTR_SUBTYPE, subType);
23807a4a56611cc044fd48b052db05aea332201216eJason Monk        }
239901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        if (!s.getHints().isEmpty()) {
240901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints()));
241901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
242901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        for (SliceItem item : s.getItems()) {
243901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serialize(item, context, options, serializer);
244901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
245901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
24607a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.endTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
247901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
248901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
24945e6d8a60387c26a7f7f26c209f54ef3023fc638Aurimas Liutikas    @SuppressWarnings("DefaultCharset")
250901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static void serialize(SliceItem item, Context context,
251901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            SliceUtils.SerializeOptions options, XmlSerializer serializer) throws IOException {
252901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        String format = item.getFormat();
253901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        options.checkThrow(format);
254901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
255901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        serializer.startTag(NAMESPACE, TAG_ITEM);
256901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        serializer.attribute(NAMESPACE, ATTR_FORMAT, format);
257901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        if (item.getSubType() != null) {
258901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.attribute(NAMESPACE, ATTR_SUBTYPE, item.getSubType());
259901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
260901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        if (!item.getHints().isEmpty()) {
261901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(item.getHints()));
262901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
263901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
264901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        switch (format) {
265901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_ACTION:
26607a4a56611cc044fd48b052db05aea332201216eJason Monk                if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
26707a4a56611cc044fd48b052db05aea332201216eJason Monk                    serialize(item.getSlice(), context, options, serializer, true,
26807a4a56611cc044fd48b052db05aea332201216eJason Monk                            item.getSubType());
26907a4a56611cc044fd48b052db05aea332201216eJason Monk                } else if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_THROW) {
27007a4a56611cc044fd48b052db05aea332201216eJason Monk                    throw new IllegalArgumentException("Slice contains an action " + item);
271901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                }
272901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
273901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
274901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                // Nothing for now.
275901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
276901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_IMAGE:
27707a4a56611cc044fd48b052db05aea332201216eJason Monk                if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
27807a4a56611cc044fd48b052db05aea332201216eJason Monk                    IconCompat icon = item.getIcon();
27907a4a56611cc044fd48b052db05aea332201216eJason Monk
28007a4a56611cc044fd48b052db05aea332201216eJason Monk                    switch (icon.getType()) {
28107a4a56611cc044fd48b052db05aea332201216eJason Monk                        case Icon.TYPE_RESOURCE:
28207a4a56611cc044fd48b052db05aea332201216eJason Monk                            serializeResIcon(serializer, icon, context);
28307a4a56611cc044fd48b052db05aea332201216eJason Monk                            break;
28407a4a56611cc044fd48b052db05aea332201216eJason Monk                        case Icon.TYPE_URI:
28507a4a56611cc044fd48b052db05aea332201216eJason Monk                            Uri uri = icon.getUri();
28607a4a56611cc044fd48b052db05aea332201216eJason Monk                            if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
28707a4a56611cc044fd48b052db05aea332201216eJason Monk                                serializeFileIcon(serializer, icon, context);
28807a4a56611cc044fd48b052db05aea332201216eJason Monk                            } else {
28907a4a56611cc044fd48b052db05aea332201216eJason Monk                                serializeIcon(serializer, icon, context, options);
29007a4a56611cc044fd48b052db05aea332201216eJason Monk                            }
29107a4a56611cc044fd48b052db05aea332201216eJason Monk                            break;
29207a4a56611cc044fd48b052db05aea332201216eJason Monk                        default:
29307a4a56611cc044fd48b052db05aea332201216eJason Monk                            serializeIcon(serializer, icon, context, options);
29407a4a56611cc044fd48b052db05aea332201216eJason Monk                            break;
295901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                    }
29607a4a56611cc044fd48b052db05aea332201216eJason Monk                } else if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_THROW) {
29707a4a56611cc044fd48b052db05aea332201216eJason Monk                    throw new IllegalArgumentException("Slice contains an image " + item);
298901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                }
299901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
300901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_INT:
301901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                serializer.text(String.valueOf(item.getInt()));
302901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
303901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_SLICE:
30407a4a56611cc044fd48b052db05aea332201216eJason Monk                serialize(item.getSlice(), context, options, serializer, false, item.getSubType());
305901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
306901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            case android.app.slice.SliceItem.FORMAT_TEXT:
307901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                if (item.getText() instanceof Spanned) {
30838db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    String text = Html.toHtml((Spanned) item.getText());
30938db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
31038db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        // 19-21 don't allow special characters in XML, so we base64 encode it.
31138db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
31238db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    }
31338db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    serializer.text(text);
314901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                } else {
31538db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    String text = String.valueOf(item.getText());
31638db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
31738db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        // 19-21 don't allow special characters in XML, so we base64 encode it.
31838db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                        text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
31938db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    }
32038db00ef5d4b5a9d0d171f1bb15a5df567d448f6Jason Monk                    serializer.text(text);
321901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                }
322901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
32307a4a56611cc044fd48b052db05aea332201216eJason Monk            case android.app.slice.SliceItem.FORMAT_LONG:
32407a4a56611cc044fd48b052db05aea332201216eJason Monk                serializer.text(String.valueOf(item.getLong()));
325901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                break;
326901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk            default:
327901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk                throw new IllegalArgumentException("Unrecognized format " + format);
328901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        }
329901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        serializer.endTag(NAMESPACE, TAG_ITEM);
330901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
331901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk
33207a4a56611cc044fd48b052db05aea332201216eJason Monk    private static void serializeResIcon(XmlSerializer serializer, IconCompat icon, Context context)
33307a4a56611cc044fd48b052db05aea332201216eJason Monk            throws IOException {
33407a4a56611cc044fd48b052db05aea332201216eJason Monk        try {
33507a4a56611cc044fd48b052db05aea332201216eJason Monk            Resources res = context.getPackageManager().getResourcesForApplication(
33607a4a56611cc044fd48b052db05aea332201216eJason Monk                    icon.getResPackage());
33707a4a56611cc044fd48b052db05aea332201216eJason Monk            int id = icon.getResId();
33807a4a56611cc044fd48b052db05aea332201216eJason Monk            serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_RES);
33907a4a56611cc044fd48b052db05aea332201216eJason Monk            serializer.attribute(NAMESPACE, ATTR_ICON_PACKAGE, res.getResourcePackageName(id));
34007a4a56611cc044fd48b052db05aea332201216eJason Monk            serializer.attribute(NAMESPACE, ATTR_ICON_RES_TYPE, res.getResourceTypeName(id));
34107a4a56611cc044fd48b052db05aea332201216eJason Monk            serializer.text(res.getResourceEntryName(id));
34207a4a56611cc044fd48b052db05aea332201216eJason Monk        } catch (PackageManager.NameNotFoundException e) {
34307a4a56611cc044fd48b052db05aea332201216eJason Monk            throw new IllegalArgumentException("Slice contains invalid icon", e);
34407a4a56611cc044fd48b052db05aea332201216eJason Monk        }
34507a4a56611cc044fd48b052db05aea332201216eJason Monk    }
34607a4a56611cc044fd48b052db05aea332201216eJason Monk
34707a4a56611cc044fd48b052db05aea332201216eJason Monk    private static void serializeFileIcon(XmlSerializer serializer, IconCompat icon,
34807a4a56611cc044fd48b052db05aea332201216eJason Monk            Context context) throws IOException {
34907a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_URI);
35007a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.text(icon.getUri().toString());
35107a4a56611cc044fd48b052db05aea332201216eJason Monk    }
35207a4a56611cc044fd48b052db05aea332201216eJason Monk
35345e6d8a60387c26a7f7f26c209f54ef3023fc638Aurimas Liutikas    @SuppressWarnings("DefaultCharset")
35407a4a56611cc044fd48b052db05aea332201216eJason Monk    private static void serializeIcon(XmlSerializer serializer, IconCompat icon,
35507a4a56611cc044fd48b052db05aea332201216eJason Monk            Context context, SliceUtils.SerializeOptions options) throws IOException {
35607a4a56611cc044fd48b052db05aea332201216eJason Monk        Drawable d = icon.loadDrawable(context);
35707a4a56611cc044fd48b052db05aea332201216eJason Monk        int width = d.getIntrinsicWidth();
35807a4a56611cc044fd48b052db05aea332201216eJason Monk        int height = d.getIntrinsicHeight();
35907a4a56611cc044fd48b052db05aea332201216eJason Monk        if (width > options.getMaxWidth()) {
36007a4a56611cc044fd48b052db05aea332201216eJason Monk            height = (int) (options.getMaxWidth() * height / (double) width);
36107a4a56611cc044fd48b052db05aea332201216eJason Monk            width = options.getMaxWidth();
36207a4a56611cc044fd48b052db05aea332201216eJason Monk        }
36307a4a56611cc044fd48b052db05aea332201216eJason Monk        if (height > options.getMaxHeight()) {
36407a4a56611cc044fd48b052db05aea332201216eJason Monk            width = (int) (options.getMaxHeight() * width / (double) height);
36507a4a56611cc044fd48b052db05aea332201216eJason Monk            height = options.getMaxHeight();
36607a4a56611cc044fd48b052db05aea332201216eJason Monk        }
36707a4a56611cc044fd48b052db05aea332201216eJason Monk        Bitmap b = Bitmap.createBitmap(width, height,
36807a4a56611cc044fd48b052db05aea332201216eJason Monk                Bitmap.Config.ARGB_8888);
36907a4a56611cc044fd48b052db05aea332201216eJason Monk        Canvas c = new Canvas(b);
37007a4a56611cc044fd48b052db05aea332201216eJason Monk        d.setBounds(0, 0, c.getWidth(), c.getHeight());
37107a4a56611cc044fd48b052db05aea332201216eJason Monk        d.draw(c);
37207a4a56611cc044fd48b052db05aea332201216eJason Monk        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
37307a4a56611cc044fd48b052db05aea332201216eJason Monk        b.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
37407a4a56611cc044fd48b052db05aea332201216eJason Monk        b.recycle();
37507a4a56611cc044fd48b052db05aea332201216eJason Monk
37607a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_DEFAULT);
37707a4a56611cc044fd48b052db05aea332201216eJason Monk        serializer.text(new String(Base64.encode(outputStream.toByteArray(), Base64.NO_WRAP)));
37807a4a56611cc044fd48b052db05aea332201216eJason Monk    }
37907a4a56611cc044fd48b052db05aea332201216eJason Monk
380901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    private static String hintStr(List<String> hints) {
381901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk        return TextUtils.join(",", hints);
382901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk    }
3830f4ca634bbc43ddff900c35f7d2a43b55d8c830dJake Wharton
3840f4ca634bbc43ddff900c35f7d2a43b55d8c830dJake Wharton    private SliceXml() {
3850f4ca634bbc43ddff900c35f7d2a43b55d8c830dJake Wharton    }
386901e2a634a8fce0c5e5acaa369e4e69326980b96Jason Monk}
387