package de.robv.android.xposed.installer.util;
import android.content.Context;
import android.os.Build;
import android.support.annotation.StringRes;
import android.support.annotation.WorkerThread;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import de.robv.android.xposed.installer.R;
import de.robv.android.xposed.installer.XposedApp;
import de.robv.android.xposed.installer.util.DownloadsUtil.SyncDownloadInfo;
import de.robv.android.xposed.installer.util.InstallZipUtil.XposedProp;
import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult;
public final class FrameworkZips {
public static final String ARCH = getArch();
public static final String SDK = Integer.toString(Build.VERSION.SDK_INT);
private static final File ONLINE_FILE = new File(XposedApp.getInstance().getCacheDir(), "framework.json");
private static final String ONLINE_URL = "http://dl-xda.xposed.info/framework.json";
public enum Type {
INSTALLER(R.string.install_update, R.string.framework_install, R.string.framework_install_recovery),
UNINSTALLER(R.string.uninstall, R.string.uninstall, R.string.framework_uninstall_recovery);
public final int title;
public final int text_flash;
public final int text_flash_recovery;
Type(@StringRes int title, @StringRes int text_flash, @StringRes int text_flash_recovery) {
this.title = title;
this.text_flash = text_flash;
this.text_flash_recovery = text_flash_recovery;
}
}
private static final int TYPE_COUNT = Type.values().length;
@SuppressWarnings("rawtypes")
private static final Map[] EMPTY_MAP_ARRAY = new Map[TYPE_COUNT];
static {
Arrays.fill(EMPTY_MAP_ARRAY, Collections.emptyMap());
}
private static Map<String, OnlineFrameworkZip>[] sOnline = emptyMapArray();
private static Map<String, List<LocalFrameworkZip>>[] sLocal = emptyMapArray();
@SuppressWarnings("unchecked")
public static <K,V> Map<K,V>[] emptyMapArray() {
return (Map<K,V>[]) EMPTY_MAP_ARRAY;
}
public static class FrameworkZip {
public String title;
public Type type = Type.INSTALLER;
public boolean isOutdated() {
return true;
}
}
public static class OnlineFrameworkZip extends FrameworkZip {
public String url;
public boolean current = true;
public boolean isOutdated() {
return !current;
}
}
public static class LocalFrameworkZip extends FrameworkZip {
public File path;
}
@WorkerThread
private static void refreshOnline() {
Map<String, OnlineFrameworkZip>[] zips = getOnline();
synchronized (FrameworkZips.class) {
sOnline = zips;
}
}
// TODO provide user feedback in case of errors
private static Map<String, OnlineFrameworkZip>[] getOnline() {
String text;
try {
text = fileToString(ONLINE_FILE);
} catch (FileNotFoundException e) {
return emptyMapArray();
} catch (IOException e) {
Log.e(XposedApp.TAG, "Could not read " + ONLINE_FILE, e);
return emptyMapArray();
}
try {
JSONObject json = new JSONObject(text);
//noinspection unchecked
Map<String, OnlineFrameworkZip>[] zipsArray = new Map[TYPE_COUNT];
for (int i = 0; i < TYPE_COUNT; i++) {
zipsArray[i] = new LinkedHashMap<>();
}
JSONArray jsonZips = json.getJSONArray("zips");
for (int i = 0; i < jsonZips.length(); i++) {
parseZipSpec(jsonZips.getJSONObject(i), zipsArray);
}
return zipsArray;
} catch (JSONException e) {
Log.e(XposedApp.TAG, "Could not parse " + ONLINE_URL, e);
return emptyMapArray();
}
}
private static String fileToString(File file) throws IOException {
Reader reader = null;
try {
reader = new FileReader(file);
StringBuilder sb = new StringBuilder((int) file.length());
char[] buffer = new char[8192];
int read;
while ((read = reader.read(buffer, 0, buffer.length)) > 0) {
sb.append(buffer, 0, read);
}
return sb.toString();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
private static void parseZipSpec(JSONObject jsonZip, Map<String, OnlineFrameworkZip>[] zipsArray) throws JSONException {
if (!contains(jsonZip, "archs", ARCH) || !contains(jsonZip, "sdks", SDK)) {
return;
}
String titleTemplate = jsonZip.getString("title");
String urlTemplate = jsonZip.getString("url");
boolean current = jsonZip.optBoolean("current", false);
String typeString = jsonZip.optString("type", null);
Type type;
if (typeString == null) {
type = Type.INSTALLER;
} else if (typeString.equals("uninstaller")) {
type = Type.UNINSTALLER;
} else {
Log.w(XposedApp.TAG, "Unsupported framework zip type: " + typeString);
return;
}
Map<String, OnlineFrameworkZip> zips = zipsArray[type.ordinal()];
Map<String, String> attributes = new HashMap<>(3);
JSONArray jsonVersions = jsonZip.optJSONArray("versions");
if (jsonVersions != null) {
Set<String> excludes = Collections.emptySet();
JSONArray jsonExcludes = jsonZip.optJSONArray("exclude");
if (jsonExcludes != null) {
excludes = new HashSet<>();
for (int i = 0; i < jsonExcludes.length(); i++) {
JSONObject jsonExclude = jsonExcludes.getJSONObject(i);
if (contains(jsonExclude, "archs", ARCH) && contains(jsonExclude, "sdks", SDK)) {
JSONArray jsonExcludeVersions = jsonExclude.getJSONArray("versions");
for (int j = 0; j < jsonExcludeVersions.length(); j++) {
excludes.add(jsonExcludeVersions.getString(j));
}
}
}
}
for (int i = 0; i < jsonVersions.length(); i++) {
JSONObject versionData = jsonVersions.getJSONObject(i);
String version = versionData.getString("version");
if (excludes.contains(version)) {
continue;
}
attributes.clear();
attributes.put("arch", ARCH);
attributes.put("sdk", SDK);
parseAttributes(versionData, attributes);
addZip(zips, titleTemplate, urlTemplate, attributes,
versionData.optBoolean("current", current), type);
}
} else {
attributes.put("arch", ARCH);
attributes.put("sdk", SDK);
addZip(zips, titleTemplate, urlTemplate, attributes, current, type);
}
}
private static boolean contains(JSONObject obj, String key, String value) throws JSONException {
JSONArray array = obj.optJSONArray(key);
if (array == null) {
return true;
}
for (int i = 0; i < array.length(); i++) {
if (array.getString(i).equals(value)) {
return true;
}
}
return false;
}
private static void parseAttributes(JSONObject obj, Map<String, String> attributes) throws JSONException {
if (obj != null) {
Iterator<String> it = obj.keys();
while (it.hasNext()) {
String key = it.next();
Object value = obj.get(key);
if (value instanceof String) {
attributes.put(key, (String) value);
}
}
}
}
private static void addZip(Map<String, OnlineFrameworkZip> zips, String titleTemplate, String urlTemplate,
Map<String, String> attributes, boolean current, Type type) {
String title = replacePlaceholders(titleTemplate, attributes);
if (!zips.containsKey(title)) {
OnlineFrameworkZip zip = new OnlineFrameworkZip();
zip.title = title;
zip.url = replacePlaceholders(urlTemplate, attributes);
zip.current = current;
zip.type = type;
zips.put(zip.title, zip);
}
}
private static String replacePlaceholders(String template, Map<String, String> values) {
if (!template.contains("$(")) {
return template;
}
StringBuilder sb = new StringBuilder(template);
for (Entry<String, String> entry : values.entrySet()) {
String search = "$(" + entry.getKey() + ")";
int length = search.length();
int index;
while ((index = sb.indexOf(search)) != -1) {
sb.replace(index, index + length, entry.getValue());
}
}
return sb.toString();
}
@WorkerThread
private static void refreshLocal() {
//noinspection unchecked
Map<String, List<LocalFrameworkZip>>[] zipsArray = new Map[TYPE_COUNT];
for (int i = 0; i < TYPE_COUNT; i++) {
zipsArray[i] = new TreeMap<>();
}
for (File dir : DownloadsUtil.getDownloadDirs(DownloadsUtil.DOWNLOAD_FRAMEWORK)) {
if (!dir.isDirectory()) {
continue;
}
for (String filename : dir.list()) {
if (!filename.endsWith(".zip")) {
continue;
}
LocalFrameworkZip zip = analyze(new File(dir, filename));
if (zip != null) {
Map<String, List<LocalFrameworkZip>> zips = zipsArray[zip.type.ordinal()];
List<LocalFrameworkZip> list = zips.get(zip.title);
if (list == null) {
list = new ArrayList<>(1);
zips.put(zip.title, list);
}
list.add(zip);
}
}
}
synchronized (FrameworkZips.class) {
sLocal = zipsArray;
}
}
// TODO Replace this with a proper way to report loading failures to the users.
public static boolean hasLoadedOnlineZips() {
return sOnline != EMPTY_MAP_ARRAY;
}
public static Set<String> getAllTitles(Type type) {
Set<String> result = new LinkedHashSet<>(sOnline[type.ordinal()].keySet());
result.addAll(sLocal[type.ordinal()].keySet());
return result;
}
public static OnlineFrameworkZip getOnline(String title, Type type) {
return sOnline[type.ordinal()].get(title);
}
public static LocalFrameworkZip getLocal(String title, Type type) {
List<LocalFrameworkZip> all = sLocal[type.ordinal()].get(title);
return all != null ? all.get(0) : null;
}
public static boolean hasLocal(String title, Type type) {
return sLocal[type.ordinal()].containsKey(title);
}
public static List<LocalFrameworkZip> getAllLocal(String title, Type type) {
List<LocalFrameworkZip> all = sLocal[type.ordinal()].get(title);
return all != null ? all : Collections.<LocalFrameworkZip>emptyList();
}
public static void delete(Context context, String title, Type type) {
OnlineFrameworkZip online = getOnline(title, type);
if (online != null) {
DownloadsUtil.removeAllForUrl(context, online.url);
}
List<LocalFrameworkZip> locals = getAllLocal(title, type);
for (LocalFrameworkZip local : locals) {
DownloadsUtil.removeAllForLocalFile(context, local.path);
}
}
@WorkerThread
private static LocalFrameworkZip analyze(File file) {
String filename = file.getName();
ZipFile zipFile = null;
try {
zipFile = new ZipFile(file);
ZipCheckResult zcr = InstallZipUtil.checkZip(zipFile);
if (!zcr.isValidZip()) {
return null;
}
LocalFrameworkZip zip = new LocalFrameworkZip();
ZipEntry entry;
if ((entry = zipFile.getEntry("system/xposed.prop")) != null) {
XposedProp prop = InstallZipUtil.parseXposedProp(zipFile.getInputStream(entry));
if (prop == null || !prop.isCompatible()) {
Log.w(XposedApp.TAG, "ZIP file is not compatible: " + file);
return null;
}
zip.title = "Version " + prop.getVersion();
} else if (filename.startsWith("xposed-uninstaller-")) {
// TODO provide more information inside uninstaller ZIPs
zip.type = Type.UNINSTALLER;
zip.title = "Uninstaller";
int start = "xposed-uninstaller-".length();
int end = filename.lastIndexOf('-');
if (start < end) {
zip.title += " (" + filename.substring(start, end) + ")";
}
} else {
return null;
}
zip.path = file;
return zip;
} catch (IOException e) {
Log.e(XposedApp.TAG, "Errors while checking " + file, e);
return null;
} finally {
if (zipFile != null) {
InstallZipUtil.closeSilently(zipFile);
}
}
}
@SuppressWarnings("deprecation")
private static String getArch() {
if (Build.CPU_ABI.equals("arm64-v8a")) {
return "arm64";
} else if (Build.CPU_ABI.equals("x86_64")) {
return "x86_64";
} else if (Build.CPU_ABI.equals("mips64")) {
return "mips64";
} else if (Build.CPU_ABI.startsWith("x86") || Build.CPU_ABI2.startsWith("x86")) {
return "x86";
} else if (Build.CPU_ABI.startsWith("mips")) {
return "mips";
} else if (Build.CPU_ABI.startsWith("armeabi-v5") || Build.CPU_ABI.startsWith("armeabi-v6")) {
return "armv5";
} else {
return "arm";
}
}
private FrameworkZips() {
}
public static class OnlineZipLoader extends OnlineLoader<OnlineZipLoader> {
private static OnlineZipLoader sInstance = new OnlineZipLoader();
public static OnlineZipLoader getInstance() {
return sInstance;
}
@Override
protected synchronized void onFirstLoad() {
new Thread("OnlineZipInit") {
@Override
public void run() {
refreshOnline();
notifyListeners();
}
}.start();
}
@Override
protected boolean onReload() {
SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(ONLINE_URL, ONLINE_FILE);
switch (info.status) {
case SyncDownloadInfo.STATUS_NOT_MODIFIED:
return false;
case SyncDownloadInfo.STATUS_FAILED:
onClear();
return true;
case SyncDownloadInfo.STATUS_SUCCESS:
default:
refreshOnline();
return true;
}
}
@Override
protected void onClear() {
super.onClear();
synchronized (this) {
ONLINE_FILE.delete();
}
synchronized (FrameworkZips.class) {
sOnline = emptyMapArray();
}
}
}
public static class LocalZipLoader extends Loader<LocalZipLoader> {
private static LocalZipLoader sInstance = new LocalZipLoader();
public static LocalZipLoader getInstance() {
return sInstance;
}
@Override
protected boolean onReload() {
refreshLocal();
return true;
}
@Override
protected void onClear() {
synchronized (FrameworkZips.class) {
sLocal = emptyMapArray();
}
}
}
}