package de.robv.android.xposed.installer.repo;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LevelListDrawable;
import android.os.AsyncTask;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.Log;
import android.util.Pair;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import de.robv.android.xposed.installer.R;
public class RepoParser {
public final static String TAG = "XposedRepoParser";
protected final static String NS = null;
protected final XmlPullParser parser;
protected RepoParserCallback mCallback;
private boolean mRepoEventTriggered = false;
protected RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
parser = factory.newPullParser();
parser.setInput(is, null);
parser.nextTag();
mCallback = callback;
}
public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException {
new RepoParser(is, callback).readRepo();
}
public static Spanned parseSimpleHtml(final Context c, String source, final TextView textView) {
source = source.replaceAll("<li>", "\t\u0095 ");
source = source.replaceAll("</li>", "<br>");
Spanned html = Html.fromHtml(source, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
LevelListDrawable d = new LevelListDrawable();
@SuppressWarnings("deprecation")
Drawable empty = c.getResources().getDrawable(R.drawable.ic_no_image);
d.addLevel(0, 0, empty);
assert empty != null;
d.setBounds(0, 0, empty.getIntrinsicWidth(), empty.getIntrinsicHeight());
new ImageGetterAsyncTask(c, source, d).execute(textView);
return d;
}
}, null);
// trim trailing newlines
int len = html.length();
int end = len;
for (int i = len - 1; i >= 0; i--) {
if (html.charAt(i) != '\n')
break;
end = i;
}
if (end == len)
return html;
else
return new SpannableStringBuilder(html, 0, end);
}
protected void readRepo() throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, NS, "repository");
Repository repository = new Repository();
repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial"));
repository.partialUrl = parser.getAttributeValue(NS, "partial-url");
repository.version = parser.getAttributeValue(NS, "version");
while (parser.nextTag() == XmlPullParser.START_TAG) {
String tagName = parser.getName();
switch (tagName) {
case "name":
repository.name = parser.nextText();
break;
case "module":
triggerRepoEvent(repository);
Module module = readModule(repository);
if (module != null)
mCallback.onNewModule(module);
break;
case "remove-module":
triggerRepoEvent(repository);
String packageName = readRemoveModule();
if (packageName != null)
mCallback.onRemoveModule(packageName);
break;
default:
skip(true);
break;
}
}
mCallback.onCompleted(repository);
}
private void triggerRepoEvent(Repository repository) {
if (mRepoEventTriggered)
return;
mCallback.onRepositoryMetadata(repository);
mRepoEventTriggered = true;
}
protected Module readModule(Repository repository) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, NS, "module");
final int startDepth = parser.getDepth();
Module module = new Module(repository);
module.packageName = parser.getAttributeValue(NS, "package");
if (module.packageName == null) {
logError("no package name defined");
leave(startDepth);
return null;
}
module.created = parseTimestamp("created");
module.updated = parseTimestamp("updated");
while (parser.nextTag() == XmlPullParser.START_TAG) {
String tagName = parser.getName();
switch (tagName) {
case "name":
module.name = parser.nextText();
break;
case "author":
module.author = parser.nextText();
break;
case "summary":
module.summary = parser.nextText();
break;
case "description":
String isHtml = parser.getAttributeValue(NS, "html");
if (isHtml != null && isHtml.equals("true"))
module.descriptionIsHtml = true;
module.description = parser.nextText();
break;
case "screenshot":
module.screenshots.add(parser.nextText());
break;
case "moreinfo":
String label = parser.getAttributeValue(NS, "label");
String role = parser.getAttributeValue(NS, "role");
String value = parser.nextText();
module.moreInfo.add(new Pair<>(label, value));
if (role != null && role.contains("support"))
module.support = value;
break;
case "version":
ModuleVersion version = readModuleVersion(module);
if (version != null)
module.versions.add(version);
break;
default:
skip(true);
break;
}
}
if (module.name == null) {
logError("packages need at least a name");
return null;
}
return module;
}
private long parseTimestamp(String attName) {
String value = parser.getAttributeValue(NS, attName);
if (value == null)
return -1;
try {
return Long.parseLong(value) * 1000L;
} catch (NumberFormatException ex) {
return -1;
}
}
protected ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, NS, "version");
final int startDepth = parser.getDepth();
ModuleVersion version = new ModuleVersion(module);
version.uploaded = parseTimestamp("uploaded");
while (parser.nextTag() == XmlPullParser.START_TAG) {
String tagName = parser.getName();
switch (tagName) {
case "name":
version.name = parser.nextText();
break;
case "code":
try {
version.code = Integer.parseInt(parser.nextText());
} catch (NumberFormatException nfe) {
logError(nfe.getMessage());
leave(startDepth);
return null;
}
break;
case "reltype":
version.relType = ReleaseType.fromString(parser.nextText());
break;
case "download":
version.downloadLink = parser.nextText();
break;
case "md5sum":
version.md5sum = parser.nextText();
break;
case "changelog":
String isHtml = parser.getAttributeValue(NS, "html");
if (isHtml != null && isHtml.equals("true"))
version.changelogIsHtml = true;
version.changelog = parser.nextText();
break;
case "branch":
// obsolete
skip(false);
break;
default:
skip(true);
break;
}
}
return version;
}
protected String readRemoveModule() throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, NS, "remove-module");
final int startDepth = parser.getDepth();
String packageName = parser.getAttributeValue(NS, "package");
if (packageName == null) {
logError("no package name defined");
leave(startDepth);
return null;
}
return packageName;
}
protected void skip(boolean showWarning) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, null, null);
if (showWarning)
Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription());
int level = 1;
while (level > 0) {
int eventType = parser.next();
if (eventType == XmlPullParser.END_TAG) {
level--;
} else if (eventType == XmlPullParser.START_TAG) {
level++;
}
}
}
protected void leave(int targetDepth) throws XmlPullParserException, IOException {
Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription());
while (parser.getDepth() > targetDepth) {
//noinspection StatementWithEmptyBody
while (parser.next() != XmlPullParser.END_TAG) {
// do nothing
}
}
}
protected void logError(String error) {
Log.e(TAG, parser.getPositionDescription() + ": " + error);
}
public interface RepoParserCallback {
void onRepositoryMetadata(Repository repository);
void onNewModule(Module module);
void onRemoveModule(String packageName);
void onCompleted(Repository repository);
}
static class ImageGetterAsyncTask extends AsyncTask<TextView, Void, Bitmap> {
private LevelListDrawable levelListDrawable;
private Context context;
private String source;
private TextView t;
public ImageGetterAsyncTask(Context context, String source, LevelListDrawable levelListDrawable) {
this.context = context;
this.source = source;
this.levelListDrawable = levelListDrawable;
}
@Override
protected Bitmap doInBackground(TextView... params) {
t = params[0];
try {
return Picasso.with(context).load(source).get();
} catch (Exception e) {
return null;
}
}
@Override
protected void onPostExecute(final Bitmap bitmap) {
try {
Drawable d = new BitmapDrawable(context.getResources(), bitmap);
Point size = new Point();
((Activity) context).getWindowManager().getDefaultDisplay().getSize(size);
int multiplier = size.x / bitmap.getWidth();
if (multiplier <= 0) multiplier = 1;
levelListDrawable.addLevel(1, 1, d);
levelListDrawable.setBounds(0, 0, bitmap.getWidth() * multiplier, bitmap.getHeight() * multiplier);
levelListDrawable.setLevel(1);
t.setText(t.getText());
} catch (Exception ignored) { /* Like a null bitmap, etc. */
}
}
}
}