1/* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package androidx.slice; 18 19import static org.xmlpull.v1.XmlPullParser.START_TAG; 20import static org.xmlpull.v1.XmlPullParser.TEXT; 21 22import android.annotation.SuppressLint; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.pm.PackageManager; 26import android.content.res.Resources; 27import android.graphics.Bitmap; 28import android.graphics.BitmapFactory; 29import android.graphics.Canvas; 30import android.graphics.drawable.Drawable; 31import android.graphics.drawable.Icon; 32import android.net.Uri; 33import android.os.Build; 34import android.text.Html; 35import android.text.Spanned; 36import android.text.TextUtils; 37import android.util.Base64; 38 39import androidx.annotation.RestrictTo; 40import androidx.core.graphics.drawable.IconCompat; 41import androidx.core.util.Consumer; 42 43import org.xmlpull.v1.XmlPullParser; 44import org.xmlpull.v1.XmlPullParserException; 45import org.xmlpull.v1.XmlPullParserFactory; 46import org.xmlpull.v1.XmlSerializer; 47 48import java.io.ByteArrayOutputStream; 49import java.io.IOException; 50import java.io.InputStream; 51import java.io.OutputStream; 52import java.util.List; 53 54/** 55 * @hide 56 */ 57@RestrictTo(RestrictTo.Scope.LIBRARY) 58class SliceXml { 59 60 private static final String NAMESPACE = null; 61 62 private static final String TAG_SLICE = "slice"; 63 private static final String TAG_ACTION = "action"; 64 private static final String TAG_ITEM = "item"; 65 66 private static final String ATTR_URI = "uri"; 67 private static final String ATTR_FORMAT = "format"; 68 private static final String ATTR_SUBTYPE = "subtype"; 69 private static final String ATTR_HINTS = "hints"; 70 private static final String ATTR_ICON_TYPE = "iconType"; 71 private static final String ATTR_ICON_PACKAGE = "pkg"; 72 private static final String ATTR_ICON_RES_TYPE = "resType"; 73 74 private static final String ICON_TYPE_RES = "res"; 75 private static final String ICON_TYPE_URI = "uri"; 76 private static final String ICON_TYPE_DEFAULT = "def"; 77 78 public static Slice parseSlice(Context context, InputStream input, 79 String encoding, SliceUtils.SliceActionListener listener) 80 throws IOException, SliceUtils.SliceParseException { 81 try { 82 XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 83 parser.setInput(input, encoding); 84 85 int outerDepth = parser.getDepth(); 86 int type; 87 Slice s = null; 88 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 89 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 90 if (type != START_TAG) { 91 continue; 92 } 93 s = parseSlice(context, parser, listener); 94 } 95 return s; 96 } catch (XmlPullParserException e) { 97 throw new IOException("Unable to init XML Serialization", e); 98 } 99 } 100 101 @SuppressLint("WrongConstant") 102 private static Slice parseSlice(Context context, XmlPullParser parser, 103 SliceUtils.SliceActionListener listener) 104 throws IOException, XmlPullParserException, SliceUtils.SliceParseException { 105 if (!TAG_SLICE.equals(parser.getName()) && !TAG_ACTION.equals(parser.getName())) { 106 throw new IOException("Unexpected tag " + parser.getName()); 107 } 108 int outerDepth = parser.getDepth(); 109 int type; 110 String uri = parser.getAttributeValue(NAMESPACE, ATTR_URI); 111 Slice.Builder b = new Slice.Builder(Uri.parse(uri)); 112 String[] hints = hints(parser.getAttributeValue(NAMESPACE, ATTR_HINTS)); 113 b.addHints(hints); 114 115 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 116 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 117 if (type == START_TAG && TAG_ITEM.equals(parser.getName())) { 118 parseItem(context, b, parser, listener); 119 } 120 } 121 return b.build(); 122 } 123 124 @SuppressLint("DefaultCharset") 125 private static void parseItem(Context context, Slice.Builder b, 126 XmlPullParser parser, final SliceUtils.SliceActionListener listener) 127 throws IOException, XmlPullParserException, SliceUtils.SliceParseException { 128 int type; 129 int outerDepth = parser.getDepth(); 130 String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT); 131 String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE); 132 String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS); 133 String iconType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_TYPE); 134 String pkg = parser.getAttributeValue(NAMESPACE, ATTR_ICON_PACKAGE); 135 String resType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_RES_TYPE); 136 @Slice.SliceHint String[] hints = hints(hintStr); 137 String v; 138 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 139 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 140 if (type == TEXT) { 141 switch (format) { 142 case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT: 143 // Nothing for now. 144 break; 145 case android.app.slice.SliceItem.FORMAT_IMAGE: 146 switch (iconType) { 147 case ICON_TYPE_RES: 148 String resName = parser.getText(); 149 try { 150 Resources r = context.getPackageManager() 151 .getResourcesForApplication(pkg); 152 int id = r.getIdentifier(resName, resType, pkg); 153 if (id != 0) { 154 b.addIcon(IconCompat.createWithResource( 155 context.createPackageContext(pkg, 0), id), subtype, 156 hints); 157 } else { 158 throw new SliceUtils.SliceParseException( 159 "Cannot find resource " + pkg + ":" + resType 160 + "/" + resName); 161 } 162 } catch (PackageManager.NameNotFoundException e) { 163 throw new SliceUtils.SliceParseException( 164 "Invalid icon package " + pkg, e); 165 } 166 break; 167 case ICON_TYPE_URI: 168 v = parser.getText(); 169 b.addIcon(IconCompat.createWithContentUri(v), subtype, hints); 170 break; 171 default: 172 v = parser.getText(); 173 byte[] data = Base64.decode(v, Base64.NO_WRAP); 174 Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length); 175 b.addIcon(IconCompat.createWithBitmap(image), subtype, hints); 176 break; 177 } 178 break; 179 case android.app.slice.SliceItem.FORMAT_INT: 180 v = parser.getText(); 181 b.addInt(Integer.parseInt(v), subtype, hints); 182 break; 183 case android.app.slice.SliceItem.FORMAT_TEXT: 184 v = parser.getText(); 185 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { 186 // 19-21 don't allow special characters in XML, so we base64 encode it. 187 v = new String(Base64.decode(v, Base64.NO_WRAP)); 188 } 189 b.addText(Html.fromHtml(v), subtype, hints); 190 break; 191 case android.app.slice.SliceItem.FORMAT_LONG: 192 v = parser.getText(); 193 b.addLong(Long.parseLong(v), subtype, hints); 194 break; 195 default: 196 throw new IllegalArgumentException("Unrecognized format " + format); 197 } 198 } else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) { 199 b.addSubSlice(parseSlice(context, parser, listener), subtype); 200 } else if (type == START_TAG && TAG_ACTION.equals(parser.getName())) { 201 b.addAction(new Consumer<Uri>() { 202 @Override 203 public void accept(Uri uri) { 204 listener.onSliceAction(uri); 205 } 206 }, parseSlice(context, parser, listener), subtype); 207 } 208 } 209 } 210 211 @Slice.SliceHint 212 private static String[] hints(String hintStr) { 213 return TextUtils.isEmpty(hintStr) ? new String[0] : hintStr.split(","); 214 } 215 216 public static void serializeSlice(Slice s, Context context, OutputStream output, 217 String encoding, SliceUtils.SerializeOptions options) throws IOException { 218 try { 219 XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer(); 220 serializer.setOutput(output, encoding); 221 serializer.startDocument(encoding, null); 222 223 serialize(s, context, options, serializer, false, null); 224 225 serializer.endDocument(); 226 serializer.flush(); 227 } catch (XmlPullParserException e) { 228 throw new IOException("Unable to init XML Serialization", e); 229 } 230 } 231 232 private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options, 233 XmlSerializer serializer, boolean isAction, String subType) throws IOException { 234 serializer.startTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE); 235 serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString()); 236 if (subType != null) { 237 serializer.attribute(NAMESPACE, ATTR_SUBTYPE, subType); 238 } 239 if (!s.getHints().isEmpty()) { 240 serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints())); 241 } 242 for (SliceItem item : s.getItems()) { 243 serialize(item, context, options, serializer); 244 } 245 246 serializer.endTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE); 247 } 248 249 @SuppressWarnings("DefaultCharset") 250 private static void serialize(SliceItem item, Context context, 251 SliceUtils.SerializeOptions options, XmlSerializer serializer) throws IOException { 252 String format = item.getFormat(); 253 options.checkThrow(format); 254 255 serializer.startTag(NAMESPACE, TAG_ITEM); 256 serializer.attribute(NAMESPACE, ATTR_FORMAT, format); 257 if (item.getSubType() != null) { 258 serializer.attribute(NAMESPACE, ATTR_SUBTYPE, item.getSubType()); 259 } 260 if (!item.getHints().isEmpty()) { 261 serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(item.getHints())); 262 } 263 264 switch (format) { 265 case android.app.slice.SliceItem.FORMAT_ACTION: 266 if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_CONVERT) { 267 serialize(item.getSlice(), context, options, serializer, true, 268 item.getSubType()); 269 } else if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_THROW) { 270 throw new IllegalArgumentException("Slice contains an action " + item); 271 } 272 break; 273 case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT: 274 // Nothing for now. 275 break; 276 case android.app.slice.SliceItem.FORMAT_IMAGE: 277 if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_CONVERT) { 278 IconCompat icon = item.getIcon(); 279 280 switch (icon.getType()) { 281 case Icon.TYPE_RESOURCE: 282 serializeResIcon(serializer, icon, context); 283 break; 284 case Icon.TYPE_URI: 285 Uri uri = icon.getUri(); 286 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 287 serializeFileIcon(serializer, icon, context); 288 } else { 289 serializeIcon(serializer, icon, context, options); 290 } 291 break; 292 default: 293 serializeIcon(serializer, icon, context, options); 294 break; 295 } 296 } else if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_THROW) { 297 throw new IllegalArgumentException("Slice contains an image " + item); 298 } 299 break; 300 case android.app.slice.SliceItem.FORMAT_INT: 301 serializer.text(String.valueOf(item.getInt())); 302 break; 303 case android.app.slice.SliceItem.FORMAT_SLICE: 304 serialize(item.getSlice(), context, options, serializer, false, item.getSubType()); 305 break; 306 case android.app.slice.SliceItem.FORMAT_TEXT: 307 if (item.getText() instanceof Spanned) { 308 String text = Html.toHtml((Spanned) item.getText()); 309 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { 310 // 19-21 don't allow special characters in XML, so we base64 encode it. 311 text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP); 312 } 313 serializer.text(text); 314 } else { 315 String text = String.valueOf(item.getText()); 316 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { 317 // 19-21 don't allow special characters in XML, so we base64 encode it. 318 text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP); 319 } 320 serializer.text(text); 321 } 322 break; 323 case android.app.slice.SliceItem.FORMAT_LONG: 324 serializer.text(String.valueOf(item.getLong())); 325 break; 326 default: 327 throw new IllegalArgumentException("Unrecognized format " + format); 328 } 329 serializer.endTag(NAMESPACE, TAG_ITEM); 330 } 331 332 private static void serializeResIcon(XmlSerializer serializer, IconCompat icon, Context context) 333 throws IOException { 334 try { 335 Resources res = context.getPackageManager().getResourcesForApplication( 336 icon.getResPackage()); 337 int id = icon.getResId(); 338 serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_RES); 339 serializer.attribute(NAMESPACE, ATTR_ICON_PACKAGE, res.getResourcePackageName(id)); 340 serializer.attribute(NAMESPACE, ATTR_ICON_RES_TYPE, res.getResourceTypeName(id)); 341 serializer.text(res.getResourceEntryName(id)); 342 } catch (PackageManager.NameNotFoundException e) { 343 throw new IllegalArgumentException("Slice contains invalid icon", e); 344 } 345 } 346 347 private static void serializeFileIcon(XmlSerializer serializer, IconCompat icon, 348 Context context) throws IOException { 349 serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_URI); 350 serializer.text(icon.getUri().toString()); 351 } 352 353 @SuppressWarnings("DefaultCharset") 354 private static void serializeIcon(XmlSerializer serializer, IconCompat icon, 355 Context context, SliceUtils.SerializeOptions options) throws IOException { 356 Drawable d = icon.loadDrawable(context); 357 int width = d.getIntrinsicWidth(); 358 int height = d.getIntrinsicHeight(); 359 if (width > options.getMaxWidth()) { 360 height = (int) (options.getMaxWidth() * height / (double) width); 361 width = options.getMaxWidth(); 362 } 363 if (height > options.getMaxHeight()) { 364 width = (int) (options.getMaxHeight() * width / (double) height); 365 height = options.getMaxHeight(); 366 } 367 Bitmap b = Bitmap.createBitmap(width, height, 368 Bitmap.Config.ARGB_8888); 369 Canvas c = new Canvas(b); 370 d.setBounds(0, 0, c.getWidth(), c.getHeight()); 371 d.draw(c); 372 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 373 b.compress(Bitmap.CompressFormat.PNG, 100, outputStream); 374 b.recycle(); 375 376 serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_DEFAULT); 377 serializer.text(new String(Base64.encode(outputStream.toByteArray(), Base64.NO_WRAP))); 378 } 379 380 private static String hintStr(List<String> hints) { 381 return TextUtils.join(",", hints); 382 } 383 384 private SliceXml() { 385 } 386} 387