blob: ba423adfc5f47b5ad9e1887d5a841ef02bf01d49 [file] [log] [blame]
/*
* Copyright 2017 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 androidx.slice;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Base64;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Consumer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
class SliceXml {
private static final String NAMESPACE = null;
private static final String TAG_SLICE = "slice";
private static final String TAG_ACTION = "action";
private static final String TAG_ITEM = "item";
private static final String ATTR_URI = "uri";
private static final String ATTR_FORMAT = "format";
private static final String ATTR_SUBTYPE = "subtype";
private static final String ATTR_HINTS = "hints";
private static final String ATTR_ICON_TYPE = "iconType";
private static final String ATTR_ICON_PACKAGE = "pkg";
private static final String ATTR_ICON_RES_TYPE = "resType";
private static final String ICON_TYPE_RES = "res";
private static final String ICON_TYPE_URI = "uri";
private static final String ICON_TYPE_DEFAULT = "def";
public static Slice parseSlice(Context context, InputStream input,
String encoding, SliceUtils.SliceActionListener listener)
throws IOException, SliceUtils.SliceParseException {
try {
XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
parser.setInput(input, encoding);
int outerDepth = parser.getDepth();
int type;
Slice s = null;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type != START_TAG) {
continue;
}
s = parseSlice(context, parser, listener);
}
return s;
} catch (XmlPullParserException e) {
throw new IOException("Unable to init XML Serialization", e);
}
}
@SuppressLint("WrongConstant")
private static Slice parseSlice(Context context, XmlPullParser parser,
SliceUtils.SliceActionListener listener)
throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
if (!TAG_SLICE.equals(parser.getName()) && !TAG_ACTION.equals(parser.getName())) {
throw new IOException("Unexpected tag " + parser.getName());
}
int outerDepth = parser.getDepth();
int type;
String uri = parser.getAttributeValue(NAMESPACE, ATTR_URI);
Slice.Builder b = new Slice.Builder(Uri.parse(uri));
String[] hints = hints(parser.getAttributeValue(NAMESPACE, ATTR_HINTS));
b.addHints(hints);
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == START_TAG && TAG_ITEM.equals(parser.getName())) {
parseItem(context, b, parser, listener);
}
}
return b.build();
}
@SuppressLint("WrongConstant")
private static void parseItem(Context context, Slice.Builder b,
XmlPullParser parser, final SliceUtils.SliceActionListener listener)
throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
int type;
int outerDepth = parser.getDepth();
String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT);
String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE);
String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS);
String iconType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_TYPE);
String pkg = parser.getAttributeValue(NAMESPACE, ATTR_ICON_PACKAGE);
String resType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_RES_TYPE);
String[] hints = hints(hintStr);
String v;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == TEXT) {
switch (format) {
case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
// Nothing for now.
break;
case android.app.slice.SliceItem.FORMAT_IMAGE:
switch (iconType) {
case ICON_TYPE_RES:
String resName = parser.getText();
try {
Resources r = context.getPackageManager()
.getResourcesForApplication(pkg);
int id = r.getIdentifier(resName, resType, pkg);
if (id != 0) {
b.addIcon(IconCompat.createWithResource(
context.createPackageContext(pkg, 0), id), subtype,
hints);
} else {
throw new SliceUtils.SliceParseException(
"Cannot find resource " + pkg + ":" + resType
+ "/" + resName);
}
} catch (PackageManager.NameNotFoundException e) {
throw new SliceUtils.SliceParseException(
"Invalid icon package " + pkg, e);
}
break;
case ICON_TYPE_URI:
v = parser.getText();
b.addIcon(IconCompat.createWithContentUri(v), subtype, hints);
break;
default:
v = parser.getText();
byte[] data = Base64.decode(v, Base64.NO_WRAP);
Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
b.addIcon(IconCompat.createWithBitmap(image), subtype, hints);
break;
}
break;
case android.app.slice.SliceItem.FORMAT_INT:
v = parser.getText();
b.addInt(Integer.parseInt(v), subtype, hints);
break;
case android.app.slice.SliceItem.FORMAT_TEXT:
v = parser.getText();
b.addText(Html.fromHtml(v), subtype, hints);
break;
case android.app.slice.SliceItem.FORMAT_LONG:
v = parser.getText();
b.addLong(Long.parseLong(v), subtype, hints);
break;
default:
throw new IllegalArgumentException("Unrecognized format " + format);
}
} else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) {
b.addSubSlice(parseSlice(context, parser, listener), subtype);
} else if (type == START_TAG && TAG_ACTION.equals(parser.getName())) {
b.addAction(new Consumer<Uri>() {
@Override
public void accept(Uri uri) {
listener.onSliceAction(uri);
}
}, parseSlice(context, parser, listener), subtype);
}
}
}
private static String[] hints(String hintStr) {
return TextUtils.isEmpty(hintStr) ? new String[0] : hintStr.split(",");
}
public static void serializeSlice(Slice s, Context context, OutputStream output,
String encoding, SliceUtils.SerializeOptions options) throws IOException {
try {
XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
serializer.setOutput(output, encoding);
serializer.startDocument(encoding, null);
serialize(s, context, options, serializer, false, null);
serializer.endDocument();
serializer.flush();
} catch (XmlPullParserException e) {
throw new IOException("Unable to init XML Serialization", e);
}
}
private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options,
XmlSerializer serializer, boolean isAction, String subType) throws IOException {
serializer.startTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString());
if (subType != null) {
serializer.attribute(NAMESPACE, ATTR_SUBTYPE, subType);
}
if (!s.getHints().isEmpty()) {
serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints()));
}
for (SliceItem item : s.getItems()) {
serialize(item, context, options, serializer);
}
serializer.endTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
}
private static void serialize(SliceItem item, Context context,
SliceUtils.SerializeOptions options, XmlSerializer serializer) throws IOException {
String format = item.getFormat();
options.checkThrow(format);
serializer.startTag(NAMESPACE, TAG_ITEM);
serializer.attribute(NAMESPACE, ATTR_FORMAT, format);
if (item.getSubType() != null) {
serializer.attribute(NAMESPACE, ATTR_SUBTYPE, item.getSubType());
}
if (!item.getHints().isEmpty()) {
serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(item.getHints()));
}
switch (format) {
case android.app.slice.SliceItem.FORMAT_ACTION:
if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
serialize(item.getSlice(), context, options, serializer, true,
item.getSubType());
} else if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_THROW) {
throw new IllegalArgumentException("Slice contains an action " + item);
}
break;
case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
// Nothing for now.
break;
case android.app.slice.SliceItem.FORMAT_IMAGE:
if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
IconCompat icon = item.getIcon();
switch (icon.getType()) {
case Icon.TYPE_RESOURCE:
serializeResIcon(serializer, icon, context);
break;
case Icon.TYPE_URI:
Uri uri = icon.getUri();
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
serializeFileIcon(serializer, icon, context);
} else {
serializeIcon(serializer, icon, context, options);
}
break;
default:
serializeIcon(serializer, icon, context, options);
break;
}
} else if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_THROW) {
throw new IllegalArgumentException("Slice contains an image " + item);
}
break;
case android.app.slice.SliceItem.FORMAT_INT:
serializer.text(String.valueOf(item.getInt()));
break;
case android.app.slice.SliceItem.FORMAT_SLICE:
serialize(item.getSlice(), context, options, serializer, false, item.getSubType());
break;
case android.app.slice.SliceItem.FORMAT_TEXT:
if (item.getText() instanceof Spanned) {
serializer.text(Html.toHtml((Spanned) item.getText()));
} else {
serializer.text(String.valueOf(item.getText()));
}
break;
case android.app.slice.SliceItem.FORMAT_LONG:
serializer.text(String.valueOf(item.getLong()));
break;
default:
throw new IllegalArgumentException("Unrecognized format " + format);
}
serializer.endTag(NAMESPACE, TAG_ITEM);
}
private static void serializeResIcon(XmlSerializer serializer, IconCompat icon, Context context)
throws IOException {
try {
Resources res = context.getPackageManager().getResourcesForApplication(
icon.getResPackage());
int id = icon.getResId();
serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_RES);
serializer.attribute(NAMESPACE, ATTR_ICON_PACKAGE, res.getResourcePackageName(id));
serializer.attribute(NAMESPACE, ATTR_ICON_RES_TYPE, res.getResourceTypeName(id));
serializer.text(res.getResourceEntryName(id));
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalArgumentException("Slice contains invalid icon", e);
}
}
private static void serializeFileIcon(XmlSerializer serializer, IconCompat icon,
Context context) throws IOException {
serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_URI);
serializer.text(icon.getUri().toString());
}
private static void serializeIcon(XmlSerializer serializer, IconCompat icon,
Context context, SliceUtils.SerializeOptions options) throws IOException {
Drawable d = icon.loadDrawable(context);
int width = d.getIntrinsicWidth();
int height = d.getIntrinsicHeight();
if (width > options.getMaxWidth()) {
height = (int) (options.getMaxWidth() * height / (double) width);
width = options.getMaxWidth();
}
if (height > options.getMaxHeight()) {
width = (int) (options.getMaxHeight() * width / (double) height);
height = options.getMaxHeight();
}
Bitmap b = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
d.setBounds(0, 0, c.getWidth(), c.getHeight());
d.draw(c);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
b.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
b.recycle();
serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_DEFAULT);
serializer.text(new String(Base64.encode(outputStream.toByteArray(), Base64.NO_WRAP)));
}
private static String hintStr(List<String> hints) {
return TextUtils.join(",", hints);
}
private SliceXml() {
}
}