package de.robv.android.xposed.installer.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPInputStream;
import de.robv.android.xposed.installer.R;
import de.robv.android.xposed.installer.XposedApp;
import de.robv.android.xposed.installer.repo.Module;
import de.robv.android.xposed.installer.repo.ModuleVersion;
import de.robv.android.xposed.installer.repo.ReleaseType;
import de.robv.android.xposed.installer.repo.RepoDb;
import de.robv.android.xposed.installer.repo.RepoParser;
import de.robv.android.xposed.installer.repo.RepoParser.RepoParserCallback;
import de.robv.android.xposed.installer.repo.Repository;
import de.robv.android.xposed.installer.util.DownloadsUtil.SyncDownloadInfo;
public class RepoLoader extends OnlineLoader<RepoLoader> {
private static final String DEFAULT_REPOSITORIES = "http://dl.xposed.info/repo/full.xml.gz";
private static RepoLoader mInstance = null;
private static final XposedApp sApp = XposedApp.getInstance();
private final Map<String, ReleaseType> mLocalReleaseTypesCache = new HashMap<>();
private SharedPreferences mModulePref;
private Map<Long, Repository> mRepositories = null;
private ReleaseType mGlobalReleaseType;
private RepoLoader() {
mInstance = this;
mPref = sApp.getSharedPreferences("repo", Context.MODE_PRIVATE);
mPrefKeyLastUpdateCheck = "last_update_check";
mModulePref = sApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE);
mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences().getString("release_type_global", "stable"));
refreshRepositories();
}
public static synchronized RepoLoader getInstance() {
if (mInstance == null)
new RepoLoader();
return mInstance;
}
public boolean refreshRepositories() {
mRepositories = RepoDb.getRepositories();
// Unlikely case (usually only during initial load): DB state doesn't
// fit to configuration
boolean needReload = false;
String[] config = mPref.getString("repositories", DEFAULT_REPOSITORIES).split("\\|");
if (mRepositories.size() != config.length) {
needReload = true;
} else {
int i = 0;
for (Repository repo : mRepositories.values()) {
if (!repo.url.equals(config[i++])) {
needReload = true;
break;
}
}
}
if (!needReload)
return false;
clear(false);
for (String url : config) {
RepoDb.insertRepository(url);
}
mRepositories = RepoDb.getRepositories();
return true;
}
public void setReleaseTypeGlobal(String relTypeString) {
ReleaseType relType = ReleaseType.fromString(relTypeString);
if (mGlobalReleaseType == relType)
return;
mGlobalReleaseType = relType;
// Updating the latest version for all modules takes a moment
new Thread("DBUpdate") {
@Override
public void run() {
RepoDb.updateAllModulesLatestVersion();
notifyListeners();
}
}.start();
}
public void setReleaseTypeLocal(String packageName, String relTypeString) {
ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) ? ReleaseType.fromString(relTypeString) : null;
if (getReleaseTypeLocal(packageName) == relType)
return;
synchronized (mLocalReleaseTypesCache) {
mLocalReleaseTypesCache.put(packageName, relType);
}
RepoDb.updateModuleLatestVersion(packageName);
notifyListeners();
}
private ReleaseType getReleaseTypeLocal(String packageName) {
synchronized (mLocalReleaseTypesCache) {
if (mLocalReleaseTypesCache.containsKey(packageName))
return mLocalReleaseTypesCache.get(packageName);
String value = mModulePref.getString(packageName + "_release_type",
null);
ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null;
mLocalReleaseTypesCache.put(packageName, result);
return result;
}
}
public Repository getRepository(long repoId) {
return mRepositories.get(repoId);
}
public Module getModule(String packageName) {
return RepoDb.getModuleByPackageName(packageName);
}
public ModuleVersion getLatestVersion(Module module) {
if (module == null || module.versions.isEmpty())
return null;
for (ModuleVersion version : module.versions) {
if (version.downloadLink != null && isVersionShown(version))
return version;
}
return null;
}
public boolean isVersionShown(ModuleVersion version) {
return version.relType
.ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal();
}
public ReleaseType getMaxShownReleaseType(String packageName) {
ReleaseType localSetting = getReleaseTypeLocal(packageName);
if (localSetting != null)
return localSetting;
else
return mGlobalReleaseType;
}
@Override
protected void onClear() {
super.onClear();
RepoDb.deleteRepositories();
mRepositories = new LinkedHashMap<>(0);
DownloadsUtil.clearCache(null);
}
public void setRepositories(String... repos) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < repos.length; i++) {
if (i > 0)
sb.append("|");
sb.append(repos[i]);
}
mPref.edit().putString("repositories", sb.toString()).apply();
if (refreshRepositories())
triggerReload(true);
}
public boolean hasModuleUpdates() {
return RepoDb.hasModuleUpdates();
}
public String getFrameworkUpdateVersion() {
return RepoDb.getFrameworkUpdateVersion();
}
private File getRepoCacheFile(String repo) {
String filename = "repo_" + HashUtil.md5(repo) + ".xml";
if (repo.endsWith(".gz"))
filename += ".gz";
return new File(sApp.getCacheDir(), filename);
}
@Override
protected boolean onReload() {
final List<String> messages = new LinkedList<>();
boolean hasChanged = downloadAndParseFiles(messages);
if (!messages.isEmpty()) {
XposedApp.runOnUiThread(new Runnable() {
public void run() {
for (String message : messages) {
Toast.makeText(sApp, message, Toast.LENGTH_LONG).show();
}
}
});
}
return hasChanged;
}
private boolean downloadAndParseFiles(List<String> messages) {
// These variables don't need to be atomic, just mutable
final AtomicBoolean hasChanged = new AtomicBoolean(false);
final AtomicInteger insertCounter = new AtomicInteger();
final AtomicInteger deleteCounter = new AtomicInteger();
for (Entry<Long, Repository> repoEntry : mRepositories.entrySet()) {
final long repoId = repoEntry.getKey();
final Repository repo = repoEntry.getValue();
String url = (repo.partialUrl != null && repo.version != null) ? String.format(repo.partialUrl, repo.version) : repo.url;
File cacheFile = getRepoCacheFile(url);
SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url,
cacheFile);
Log.i(XposedApp.TAG, String.format(
"Downloaded %s with status %d (error: %s), size %d bytes",
url, info.status, info.errorMessage, cacheFile.length()));
if (info.status != SyncDownloadInfo.STATUS_SUCCESS) {
if (info.errorMessage != null)
messages.add(info.errorMessage);
continue;
}
InputStream in = null;
RepoDb.beginTransation();
try {
in = new FileInputStream(cacheFile);
if (url.endsWith(".gz"))
in = new GZIPInputStream(in);
RepoParser.parse(in, new RepoParserCallback() {
@Override
public void onRepositoryMetadata(Repository repository) {
if (!repository.isPartial) {
RepoDb.deleteAllModules(repoId);
hasChanged.set(true);
}
}
@Override
public void onNewModule(Module module) {
RepoDb.insertModule(repoId, module);
hasChanged.set(true);
insertCounter.incrementAndGet();
}
@Override
public void onRemoveModule(String packageName) {
RepoDb.deleteModule(repoId, packageName);
hasChanged.set(true);
deleteCounter.decrementAndGet();
}
@Override
public void onCompleted(Repository repository) {
if (!repository.isPartial) {
RepoDb.updateRepository(repoId, repository);
repo.name = repository.name;
repo.partialUrl = repository.partialUrl;
repo.version = repository.version;
} else {
RepoDb.updateRepositoryVersion(repoId, repository.version);
repo.version = repository.version;
}
Log.i(XposedApp.TAG, String.format(
"Updated repository %s to version %s (%d new / %d removed modules)",
repo.url, repo.version, insertCounter.get(),
deleteCounter.get()));
}
});
RepoDb.setTransactionSuccessful();
} catch (Throwable t) {
Log.e(XposedApp.TAG, "Cannot load repository from " + url, t);
messages.add(sApp.getString(R.string.repo_load_failed, url, t.getMessage()));
DownloadsUtil.clearCache(url);
} finally {
if (in != null)
try {
in.close();
} catch (IOException ignored) {
}
cacheFile.delete();
RepoDb.endTransation();
}
}
// TODO Set ModuleColumns.PREFERRED for modules which appear in multiple
// repositories
return hasChanged.get();
}
}