/*
* Copyright 2018 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.sharetarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;
import androidx.annotation.AnyThread;
import androidx.annotation.WorkerThread;
import androidx.collection.ArrayMap;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.util.AtomicFile;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class ShortcutsInfoSerialization {
private static final String TAG = "ShortcutInfoCompatSaver";
private static final String TAG_ROOT = "share_targets";
private static final String TAG_TARGET = "target";
private static final String TAG_INTENT = "intent";
private static final String TAG_CATEGORY = "categories";
private static final String ATTR_ID = "id";
private static final String ATTR_COMPONENT = "component";
private static final String ATTR_SHORT_LABEL = "short_label";
private static final String ATTR_LONG_LABEL = "long_label";
private static final String ATTR_DISABLED_MSG = "disabled_message";
private static final String ATTR_RANK = "rank";
private static final String ATTR_ICON_RES_NAME = "icon_resource_name";
private static final String ATTR_ICON_BMP_PATH = "icon_bitmap_path";
private static final String ATTR_ACTION = "action";
private static final String ATTR_TARGET_PACKAGE = "targetPackage";
private static final String ATTR_TARGET_CLASS = "targetClass";
private static final String ATTR_NAME = "name";
private ShortcutsInfoSerialization() {
}
/**
* Class to keep {@link ShortcutInfoCompat}s with extra info (resource name or file path) about
* their serialized Icon for lazy loading.
*/
static class ShortcutContainer {
final String mResourceName;
final String mBitmapPath;
final ShortcutInfoCompat mShortcutInfo;
@AnyThread
ShortcutContainer(ShortcutInfoCompat shortcut, String resourceName, String bitmapPath) {
mShortcutInfo = shortcut;
mResourceName = resourceName;
mBitmapPath = bitmapPath;
}
}
static void saveAsXml(List<ShortcutContainer> shortcutsList, File output) {
final AtomicFile atomicFile = new AtomicFile(output);
FileOutputStream fileStream = null;
try {
fileStream = atomicFile.startWrite();
final BufferedOutputStream stream = new BufferedOutputStream(fileStream);
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(stream, "UTF_8");
serializer.startDocument(null, true);
serializer.startTag(null, TAG_ROOT);
for (ShortcutContainer shortcut : shortcutsList) {
serializeShortcutContainer(serializer, shortcut);
}
serializer.endTag(null, TAG_ROOT);
serializer.endDocument();
stream.flush();
fileStream.flush();
atomicFile.finishWrite(fileStream);
} catch (Exception e) {
Log.e(TAG, "Failed to write to file " + atomicFile.getBaseFile(), e);
atomicFile.failWrite(fileStream);
throw new RuntimeException("Failed to write to file " + atomicFile.getBaseFile(), e);
}
}
@WorkerThread
static Map<String, ShortcutContainer> loadFromXml(File input, Context context) {
Map<String, ShortcutContainer> shortcutsList = new ArrayMap<>();
try (FileInputStream stream = new FileInputStream(input)) {
if (!input.exists()) {
return shortcutsList;
}
final XmlPullParser parser = Xml.newPullParser();
parser.setInput(stream, "UTF_8");
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_TARGET)) {
ShortcutContainer shortcut = parseShortcutContainer(parser, context);
if (shortcut != null && shortcut.mShortcutInfo != null) {
shortcutsList.put(shortcut.mShortcutInfo.getId(), shortcut);
}
}
}
} catch (Exception e) {
// trying to recover by deleting input file.
input.delete();
Log.e(TAG, "Failed to load saved values from file " + input.getAbsolutePath()
+ ". Old state removed, new added", e);
}
return shortcutsList;
}
@WorkerThread
private static ShortcutContainer parseShortcutContainer(XmlPullParser parser,
Context context) throws Exception {
if (!parser.getName().equals(TAG_TARGET)) {
return null;
}
String id = getAttributeValue(parser, ATTR_ID);
CharSequence label = getAttributeValue(parser, ATTR_SHORT_LABEL);
if (TextUtils.isEmpty(id) || TextUtils.isEmpty(label)) {
return null;
}
int rank = Integer.parseInt(getAttributeValue(parser, ATTR_RANK));
CharSequence longLabel = getAttributeValue(parser, ATTR_LONG_LABEL);
CharSequence disabledMessage = getAttributeValue(parser, ATTR_DISABLED_MSG);
ComponentName activity = parseComponentName(parser);
String iconResourceName = getAttributeValue(parser, ATTR_ICON_RES_NAME);
String iconBitmapPath = getAttributeValue(parser, ATTR_ICON_BMP_PATH);
ArrayList<Intent> intents = new ArrayList<>();
Set<String> categories = new HashSet<>();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case TAG_INTENT:
Intent intent = parseIntent(parser);
if (intent != null) {
intents.add(intent);
}
break;
case TAG_CATEGORY:
String category = getAttributeValue(parser, ATTR_NAME);
if (!TextUtils.isEmpty(category)) {
categories.add(category);
}
break;
}
} else if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_TARGET)) {
break;
}
}
ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, id)
.setShortLabel(label)
.setRank(rank);
if (!TextUtils.isEmpty(longLabel)) {
builder.setLongLabel(longLabel);
}
if (!TextUtils.isEmpty(disabledMessage)) {
builder.setDisabledMessage(disabledMessage);
}
if (activity != null) {
builder.setActivity(activity);
}
if (!intents.isEmpty()) {
builder.setIntents(intents.toArray(new Intent[0]));
}
if (!categories.isEmpty()) {
builder.setCategories(categories);
}
return new ShortcutContainer(builder.build(), iconResourceName, iconBitmapPath);
}
@WorkerThread
private static ComponentName parseComponentName(XmlPullParser parser) {
String value = getAttributeValue(parser, ATTR_COMPONENT);
return TextUtils.isEmpty(value) ? null : ComponentName.unflattenFromString(value);
}
@WorkerThread
private static Intent parseIntent(XmlPullParser parser) {
String action = getAttributeValue(parser, ATTR_ACTION);
String targetPackage = getAttributeValue(parser, ATTR_TARGET_PACKAGE);
String targetClass = getAttributeValue(parser, ATTR_TARGET_CLASS);
if (action == null) {
return null;
}
Intent intent = new Intent(action);
if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
intent.setClassName(targetPackage, targetClass);
}
return intent;
}
@WorkerThread
private static String getAttributeValue(XmlPullParser parser, String attribute) {
String value = parser.getAttributeValue("http://schemas.android.com/apk/res/android",
attribute);
if (value == null) {
value = parser.getAttributeValue(null, attribute);
}
return value;
}
@WorkerThread
private static void serializeShortcutContainer(XmlSerializer serializer,
ShortcutContainer container)
throws IOException {
serializer.startTag(null, TAG_TARGET);
ShortcutInfoCompat shortcut = container.mShortcutInfo;
serializeAttribute(serializer, ATTR_ID, shortcut.getId());
serializeAttribute(serializer, ATTR_SHORT_LABEL, shortcut.getShortLabel().toString());
serializeAttribute(serializer, ATTR_RANK, Integer.toString(shortcut.getRank()));
if (!TextUtils.isEmpty(shortcut.getLongLabel())) {
serializeAttribute(serializer, ATTR_LONG_LABEL, shortcut.getLongLabel().toString());
}
if (!TextUtils.isEmpty(shortcut.getDisabledMessage())) {
serializeAttribute(serializer, ATTR_DISABLED_MSG,
shortcut.getDisabledMessage().toString());
}
if (shortcut.getActivity() != null) {
serializeAttribute(serializer, ATTR_COMPONENT,
shortcut.getActivity().flattenToString());
}
if (!TextUtils.isEmpty(container.mResourceName)) {
serializeAttribute(serializer, ATTR_ICON_RES_NAME, container.mResourceName);
}
if (!TextUtils.isEmpty(container.mBitmapPath)) {
serializeAttribute(serializer, ATTR_ICON_BMP_PATH, container.mBitmapPath);
}
for (Intent intent : shortcut.getIntents()) {
serializeIntent(serializer, intent);
}
for (String category : shortcut.getCategories()) {
serializeCategory(serializer, category);
}
serializer.endTag(null, TAG_TARGET);
}
@WorkerThread
private static void serializeIntent(XmlSerializer serializer, Intent intent)
throws IOException {
serializer.startTag(null, TAG_INTENT);
serializeAttribute(serializer, ATTR_ACTION, intent.getAction());
if (intent.getComponent() != null) {
serializeAttribute(serializer, ATTR_TARGET_PACKAGE,
intent.getComponent().getPackageName());
serializeAttribute(serializer, ATTR_TARGET_CLASS, intent.getComponent().getClassName());
}
serializer.endTag(null, TAG_INTENT);
}
@WorkerThread
private static void serializeCategory(XmlSerializer serializer, String category)
throws IOException {
if (TextUtils.isEmpty(category)) {
return;
}
serializer.startTag(null, TAG_CATEGORY);
serializeAttribute(serializer, ATTR_NAME, category);
serializer.endTag(null, TAG_CATEGORY);
}
@WorkerThread
private static void serializeAttribute(XmlSerializer serializer, String attribute, String value)
throws IOException {
if (TextUtils.isEmpty(value)) {
return;
}
serializer.attribute(null, attribute, value);
}
}