diff --git a/app/src/main/java/org/fdroid/fdroid/#RepoUpdater.java# b/app/src/main/java/org/fdroid/fdroid/#RepoUpdater.java#
new file mode 100644
index 000000000..293387593
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/#RepoUpdater.java#
@@ -0,0 +1,397 @@
+
+
+package org.fdroid.fdroid;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoPersister;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.data.Schema.RepoTable;
+import org.fdroid.fdroid.net.Downloader;
+import org.fdroid.fdroid.net.DownloaderFactory;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.CodeSigner;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Responsible for updating an individual repository. This will:
+ * * Download the index.jar
+ * * Verify that it is signed correctly and by the correct certificate
+ * * Parse the index.xml from the .jar file
+ * * Save the resulting repo, apps, and apks to the database.
+ *
+ * WARNING: this class is the central piece of the entire security model of
+ * FDroid! Avoid modifying it when possible, if you absolutely must, be very,
+ * very careful with the changes that you are making!
+ */
+public class RepoUpdater {
+
+ private static final String TAG = "RepoUpdater";
+
+ private final String indexUrl;
+
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final Repo repo;
+ private boolean hasChanged;
+
+ @Nullable
+ private ProgressListener downloadProgressListener;
+ private ProgressListener committingProgressListener;
+ private ProgressListener processXmlProgressListener;
+ private String cacheTag;
+ private X509Certificate signingCertFromJar;
+
+ @NonNull private final RepoPersister persister;
+
+ /**
+ * Updates an app repo as read out of the database into a {@link Repo} instance.
+ *
+ * @param repo A {@link Repo} read out of the local database
+ */
+ public RepoUpdater(@NonNull Context context, @NonNull Repo repo) {
+ this.context = context;
+ this.repo = repo;
+ this.persister = new RepoPersister(context, repo);
+
+ String url = repo.address + "/index.jar";
+ String versionName = Utils.getVersionName(context);
+ if (versionName != null) {
+ url += "?client_version=" + versionName;
+ }
+ this.indexUrl = url;
+ }
+
+ public void setDownloadProgressListener(ProgressListener progressListener) {
+ this.downloadProgressListener = progressListener;
+ }
+
+ public void setProcessXmlProgressListener(ProgressListener progressListener) {
+ this.processXmlProgressListener = progressListener;
+ }
+
+ public void setCommittingProgressListener(ProgressListener progressListener) {
+ this.committingProgressListener = progressListener;
+ }
+
+ public boolean hasChanged() {
+ return hasChanged;
+ }
+
+ private Downloader downloadIndex() throws UpdateException {
+ Downloader downloader = null;
+ try {
+ downloader = DownloaderFactory.create(context, indexUrl);
+ downloader.setCacheTag(repo.lastetag);
+ downloader.setListener(downloadProgressListener);
+ downloader.download();
+
+ if (downloader.isCached()) {
+ // The index is unchanged since we last read it. We just mark
+ // everything that came from this repo as being updated.
+ Utils.debugLog(TAG, "Repo index for " + indexUrl + " is up to date (by etag)");
+ }
+
+ } catch (IOException e) {
+ if (downloader != null && downloader.outputFile != null) {
+ if (!downloader.outputFile.delete()) {
+ Log.w(TAG, "Couldn't delete file: " + downloader.outputFile.getAbsolutePath());
+ }
+ }
+
+ throw new UpdateException(repo, "Error getting index file", e);
+ } catch (InterruptedException e) {
+ // ignored if canceled, the local database just won't be updated
+ e.printStackTrace();
+ }
+ return downloader;
+ }
+
+ /**
+ * All repos are represented by a signed jar file, {@code index.jar}, which contains
+ * a single file, {@code index.xml}. This takes the {@code index.jar}, verifies the
+ * signature, then returns the unzipped {@code index.xml}.
+ *
+ * @throws UpdateException All error states will come from here.
+ */
+ public void update() throws UpdateException {
+
+ final Downloader downloader = downloadIndex();
+ hasChanged = downloader.hasChanged();
+
+ if (hasChanged) {
+ // Don't worry about checking the status code for 200. If it was a
+ // successful download, then we will have a file ready to use:
+ cacheTag = downloader.getCacheTag();
+ processDownloadedFile(downloader.outputFile);
+ }
+ }
+
+ private ContentValues repoDetailsToSave;
+ private String signingCertFromIndexXml;
+
+ private RepoXMLHandler.IndexReceiver createIndexReceiver() {
+ return new RepoXMLHandler.IndexReceiver() {
+ @Override
+ public void receiveRepo(String name, String description, String signingCert, int maxAge, int version, long timestamp) {
+ signingCertFromIndexXml = signingCert;
+ repoDetailsToSave = prepareRepoDetailsForSaving(name, description, maxAge, version, timestamp);
+ }
+
+ @Override
+ public void receiveApp(App app, List packages) {
+ try {
+ persister.saveToDb(app, packages);
+ } catch (UpdateException e) {
+ throw new RuntimeException("Error while saving repo details to database.", e);
+ }
+ }
+ };
+ }
+
+ public void processDownloadedFile(File downloadedFile) throws UpdateException {
+ InputStream indexInputStream = null;
+ try {
+ if (downloadedFile == null || !downloadedFile.exists()) {
+ throw new UpdateException(repo, downloadedFile + " does not exist!");
+ }
+
+ // Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes
+ // breakage when verifying the signature of the downloaded .jar. For more
+ // details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
+ FDroidApp.disableSpongyCastleOnLollipop();
+
+ JarFile jarFile = new JarFile(downloadedFile, true);
+ JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
+ indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
+ processXmlProgressListener, new URL(repo.address), (int) indexEntry.getSize());
+
+ // Process the index...
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ final SAXParser parser = factory.newSAXParser();
+ final XMLReader reader = parser.getXMLReader();
+ final RepoXMLHandler repoXMLHandler = new RepoXMLHandler(repo, createIndexReceiver());
+ reader.setContentHandler(repoXMLHandler);
+ reader.parse(new InputSource(indexInputStream));
+
+ long timestamp = repoDetailsToSave.getAsLong(RepoTable.Cols.TIMESTAMP);
+ if (timestamp < repo.timestamp) {
+ throw new UpdateException(repo, "index.jar is older that current index! "
+ + timestamp + " < " + repo.timestamp);
+ }
+
+ signingCertFromJar = getSigningCertFromJar(indexEntry);
+
+ // JarEntry can only read certificates after the file represented by that JarEntry
+ // has been read completely, so verification cannot run until now...
+ assertSigningCertFromXmlCorrect();
+ commitToDb();
+ } catch (SAXException | ParserConfigurationException | IOException e) {
+ throw new UpdateException(repo, "Error parsing index", e);
+ } finally {
+ FDroidApp.enableSpongyCastleOnLollipop();
+ Utils.closeQuietly(indexInputStream);
+ if (downloadedFile != null) {
+ if (!downloadedFile.delete()) {
+ Log.w(TAG, "Couldn't delete file: " + downloadedFile.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ private void commitToDb() throws UpdateException {
+ Log.i(TAG, "Repo signature verified, saving app metadata to database.");
+ if (committingProgressListener != null) {
+ try {
+ //TODO this should be an event, not a progress listener
+ committingProgressListener.onProgress(new URL(indexUrl), 0, -1);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+ persister.commit(repoDetailsToSave);
+ }
+
+ private void assertSigningCertFromXmlCorrect() throws SigningException {
+
+ // no signing cert read from database, this is the first use
+ if (repo.signingCertificate == null) {
+ verifyAndStoreTOFUCerts(signingCertFromIndexXml, signingCertFromJar);
+ }
+ verifyCerts(signingCertFromIndexXml, signingCertFromJar);
+
+ }
+
+ /**
+ * Update tracking data for the repo represented by this instance (index version, etag,
+ * description, human-readable name, etc.
+ */
+ private ContentValues prepareRepoDetailsForSaving(String name, String description, int maxAge, int version, long timestamp) {
+ ContentValues values = new ContentValues();
+
+ values.put(RepoTable.Cols.LAST_UPDATED, Utils.formatTime(new Date(), ""));
+
+ if (repo.lastetag == null || !repo.lastetag.equals(cacheTag)) {
+ values.put(RepoTable.Cols.LAST_ETAG, cacheTag);
+ }
+
+ if (version != -1 && version != repo.version) {
+ Utils.debugLog(TAG, "Repo specified a new version: from " + repo.version + " to " + version);
+ values.put(RepoTable.Cols.VERSION, version);
+ }
+
+ if (maxAge != -1 && maxAge != repo.maxage) {
+ Utils.debugLog(TAG, "Repo specified a new maximum age - updated");
+ values.put(RepoTable.Cols.MAX_AGE, maxAge);
+ }
+
+ if (description != null && !description.equals(repo.description)) {
+ values.put(RepoTable.Cols.DESCRIPTION, description);
+ }
+
+ if (name != null && !name.equals(repo.name)) {
+ values.put(RepoTable.Cols.NAME, name);
+ }
+
+ if (timestamp != repo.timestamp) {
+ values.put(RepoTable.Cols.TIMESTAMP, timestamp);
+ }
+
+ return values;
+ }
+
+ public static class UpdateException extends Exception {
+
+ private static final long serialVersionUID = -4492452418826132803L;
+ public final Repo repo;
+
+ public UpdateException(Repo repo, String message) {
+ super(message);
+ this.repo = repo;
+ }
+
+ public UpdateException(Repo repo, String message, Exception cause) {
+ super(message, cause);
+ this.repo = repo;
+ }
+ }
+
+ public static class SigningException extends UpdateException {
+ public SigningException(Repo repo, String message) {
+ super(repo, "Repository was not signed correctly: " + message);
+ }
+ }
+
+ /**
+ * FDroid's index.jar is signed using a particular format and does not allow lots of
+ * signing setups that would be valid for a regular jar. This validates those
+ * restrictions.
+ */
+ private X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException {
+ final CodeSigner[] codeSigners = jarEntry.getCodeSigners();
+ if (codeSigners == null || codeSigners.length == 0) {
+ throw new SigningException(repo, "No signature found in index");
+ }
+ /* we could in theory support more than 1, but as of now we do not */
+ if (codeSigners.length > 1) {
+ throw new SigningException(repo, "index.jar must be signed by a single code signer!");
+ }
+ List extends Certificate> certs = codeSigners[0].getSignerCertPath().getCertificates();
+ if (certs.size() != 1) {
+ throw new SigningException(repo, "index.jar code signers must only have a single certificate!");
+ }
+ return (X509Certificate) certs.get(0);
+ }
+
+ /**
+ * A new repo can be added with or without the fingerprint of the signing
+ * certificate. If no fingerprint is supplied, then do a pure TOFU and just
+ * store the certificate as valid. If there is a fingerprint, then first
+ * check that the signing certificate in the jar matches that fingerprint.
+ */
+ private void verifyAndStoreTOFUCerts(String certFromIndexXml, X509Certificate rawCertFromJar)
+ throws SigningException {
+ if (repo.signingCertificate != null) {
+ return; // there is a repo.signingCertificate already, nothing to TOFU
+ }
+
+ /* The first time a repo is added, it can be added with the signing certificate's
+ * fingerprint. In that case, check that fingerprint against what is
+ * actually in the index.jar itself. If no fingerprint, just store the
+ * signing certificate */
+ if (repo.fingerprint != null) {
+ String fingerprintFromIndexXml = Utils.calcFingerprint(certFromIndexXml);
+ String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
+ if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromIndexXml)
+ || !repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
+ throw new SigningException(repo, "Supplied certificate fingerprint does not match!");
+ }
+ } // else - no info to check things are valid, so just Trust On First Use
+
+ Utils.debugLog(TAG, "Saving new signing certificate in the database for " + repo.address);
+ ContentValues values = new ContentValues(2);
+ values.put(RepoTable.Cols.LAST_UPDATED, Utils.formatDate(new Date(), ""));
+ values.put(RepoTable.Cols.SIGNING_CERT, Hasher.hex(rawCertFromJar));
+ RepoProvider.Helper.update(context, repo, values);
+ }
+
+ /**
+ * FDroid works with three copies of the signing certificate:
+ * in the downloaded jar
+ * in the index XML
+ * stored in the local database
+ * It would work better removing the copy from the index XML, but it needs to stay
+ * there for backwards compatibility since the old TOFU process requires it. Therefore,
+ * since all three have to be present, all three are compared.
+ *
+ * @param certFromIndexXml the cert written into the header of the index XML
+ * @param rawCertFromJar the {@link X509Certificate} embedded in the downloaded jar
+ */
+ private void verifyCerts(String certFromIndexXml, X509Certificate rawCertFromJar) throws SigningException {
+ // convert binary data to string version that is used in FDroid's database
+ String certFromJar = Hasher.hex(rawCertFromJar);
+
+ // repo and repo.signingCertificate must be pre-loaded from the database
+ if (TextUtils.isEmpty(repo.signingCertificate)
+ || TextUtils.isEmpty(certFromJar)
+ || TextUtils.isEmpty(certFromIndexXml)) {
+ throw new SigningException(repo, "A empty repo or signing certificate is invalid!");
+ }
+
+ // though its called repo.signingCertificate, its actually a X509 certificate
+ if (repo.signingCertificate.equals(certFromJar)
+ && repo.signingCertificate.equals(certFromIndexXml)
+ && certFromIndexXml.equals(certFromJar)) {
+ return; // we have a match!
+ }
+ throw new SigningException(repo, "Signing certificate does not match!");
+ }
+
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/#RepoXMLHandler.java# b/app/src/main/java/org/fdroid/fdroid/#RepoXMLHandler.java#
new file mode 100644
index 000000000..5173da35d
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/#RepoXMLHandler.java#
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
+ * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.Repo;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses the index.xml into Java data structures.
+ */
+public class RepoXMLHandler extends DefaultHandler {
+
+ // The repo we're processing.
+ private final Repo repo;
+
+ private List apksList = new ArrayList<>();
+
+ private App curapp;
+ private Apk curapk;
+
+ private String currentApkHashType;
+
+ // After processing the XML, these will be -1 if the index didn't specify
+ // them - otherwise it will be the value specified.
+ private int repoMaxAge = -1;
+ private int repoVersion;
+ private long repoTimestamp;
+ private String repoDescription;
+ private String repoName;
+
+ // the X.509 signing certificate stored in the header of index.xml
+ private String repoSigningCert;
+
+ private final StringBuilder curchars = new StringBuilder();
+
+ interface IndexReceiver {
+ void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp);
+
+ void receiveApp(App app, List packages);
+ }
+
+ private final IndexReceiver receiver;
+
+ public RepoXMLHandler(Repo repo, @NonNull IndexReceiver receiver) {
+ this.repo = repo;
+ this.receiver = receiver;
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) {
+ curchars.append(ch, start, length);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+
+ if ("application".equals(localName) && curapp != null) {
+ onApplicationParsed();
+ } else if ("package".equals(localName) && curapk != null && curapp != null) {
+ apksList.add(curapk);
+ curapk = null;
+ } else if ("repo".equals(localName)) {
+ onRepoParsed();
+ } else if (curchars.length() == 0) {
+ // All options below require non-empty content
+ return;
+ }
+ final String str = curchars.toString().trim();
+ if (curapk != null) {
+ switch (localName) {
+ case "version":
+ curapk.versionName = str;
+ break;
+ case "versioncode":
+ curapk.versionCode = Utils.parseInt(str, -1);
+ break;
+ case "size":
+ curapk.size = Utils.parseInt(str, 0);
+ break;
+ case "hash":
+ if (currentApkHashType == null || "md5".equals(currentApkHashType)) {
+ if (curapk.hash == null) {
+ curapk.hash = str;
+ curapk.hashType = "SHA-256";
+ }
+ } else if ("sha256".equals(currentApkHashType)) {
+ curapk.hash = str;
+ curapk.hashType = "SHA-256";
+ }
+ break;
+ case "sig":
+ curapk.sig = str;
+ break;
+ case "srcname":
+ curapk.srcname = str;
+ break;
+ case "apkname":
+ curapk.apkName = str;
+ break;
+ case "sdkver":
+ curapk.minSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE);
+ break;
+ case "targetSdkVersion":
+ curapk.targetSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE);
+ break;
+ case "maxsdkver":
+ curapk.maxSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MAX_VALUE);
+ if (curapk.maxSdkVersion == 0) {
+ // before fc0df0dcf4dd0d5f13de82d7cd9254b2b48cb62d, this could be 0
+ curapk.maxSdkVersion = Apk.SDK_VERSION_MAX_VALUE;
+ }
+ break;
+ case "added":
+ curapk.added = Utils.parseDate(str, null);
+ break;
+ case "permissions":
+ curapk.permissions = Utils.parseCommaSeparatedString(str);
+ break;
+ case "features":
+ curapk.features = Utils.parseCommaSeparatedString(str);
+ break;
+ case "nativecode":
+ curapk.nativecode = Utils.parseCommaSeparatedString(str);
+ break;
+ }
+ } else if (curapp != null) {
+ switch (localName) {
+ case "name":
+ curapp.name = str;
+ break;
+ case "icon":
+ curapp.icon = str;
+ break;
+ case "description":
+ // This is the old-style description. We'll read it
+ // if present, to support old repos, but in newer
+ // repos it will get overwritten straight away!
+ curapp.description = "" + str + "
";
+ break;
+ case "desc":
+ // New-style description.
+ curapp.description = str;
+ break;
+ case "summary":
+ curapp.summary = str;
+ break;
+ case "license":
+ curapp.license = str;
+ break;
+ case "author":
+ curapp.author = str;
+ break;
+ case "email":
+ curapp.email = str;
+ break;
+ case "source":
+ curapp.sourceURL = str;
+ break;
+ case "changelog":
+ curapp.changelogURL = str;
+ break;
+ case "donate":
+ curapp.donateURL = str;
+ break;
+ case "bitcoin":
+ curapp.bitcoinAddr = str;
+ break;
+ case "litecoin":
+ curapp.litecoinAddr = str;
+ break;
+ case "flattr":
+ curapp.flattrID = str;
+ break;
+ case "web":
+ curapp.webURL = str;
+ break;
+ case "tracker":
+ curapp.trackerURL = str;
+ break;
+ case "added":
+ curapp.added = Utils.parseDate(str, null);
+ break;
+ case "lastupdated":
+ curapp.lastUpdated = Utils.parseDate(str, null);
+ break;
+ case "marketversion":
+ curapp.upstreamVersionName = str;
+ break;
+ case "marketvercode":
+ curapp.upstreamVersionCode = Utils.parseInt(str, -1);
+ break;
+ case "categories":
+ curapp.categories = Utils.parseCommaSeparatedString(str);
+ break;
+ case "antifeatures":
+ curapp.antiFeatures = Utils.parseCommaSeparatedString(str);
+ break;
+ case "requirements":
+ curapp.requirements = Utils.parseCommaSeparatedString(str);
+ break;
+ }
+ } else if ("description".equals(localName)) {
+ repoDescription = cleanWhiteSpace(str);
+ }
+ }
+
+ private void onApplicationParsed() {
+ receiver.receiveApp(curapp, apksList);
+ curapp = null;
+ apksList = new ArrayList<>();
+ // If the app packageName is already present in this apps list, then it
+ // means the same index file has a duplicate app, which should
+ // not be allowed.
+ // However, I'm thinking that it should be undefined behaviour,
+ // because it is probably a bug in the fdroid server that made it
+ // happen, and I don't *think* it will crash the client, because
+ // the first app will insert, the second one will update the newly
+ // inserted one.
+
+ //////#TODO
+
+ /*
+
+ sample this as a xml handlers
+ should add screen shot id the project as it be
+ and also there is no way to contact developer
+ another is to create a place to collect userr data and vote for a package
+
+ */
+ }
+
+ private void onRepoParsed() {
+ receiver.receiveRepo(repoName, repoDescription, repoSigningCert, repoMaxAge, repoVersion, repoTimestamp);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ super.startElement(uri, localName, qName, attributes);
+
+ if ("repo".equals(localName)) {
+ repoSigningCert = attributes.getValue("", "pubkey");
+ repoMaxAge = Utils.parseInt(attributes.getValue("", "maxage"), -1);
+ repoVersion = Utils.parseInt(attributes.getValue("", "version"), -1);
+ repoName = cleanWhiteSpace(attributes.getValue("", "name"));
+ repoDescription = cleanWhiteSpace(attributes.getValue("", "description"));
+ repoTimestamp = parseLong(attributes.getValue("", "timestamp"), 0);
+ } else if ("application".equals(localName) && curapp == null) {
+ curapp = new App();
+ curapp.packageName = attributes.getValue("", "id");
+ } else if ("package".equals(localName) && curapp != null && curapk == null) {
+ curapk = new Apk();
+ curapk.packageName = curapp.packageName;
+ curapk.repo = repo.getId();
+ currentApkHashType = null;
+
+ } else if ("hash".equals(localName) && curapk != null) {
+ currentApkHashType = attributes.getValue("", "type");
+ }
+ curchars.setLength(0);
+ }
+
+ private static String cleanWhiteSpace(@Nullable String str) {
+ return str == null ? null : str.replaceAll("\\s", " ");
+ }
+
+ private static long parseLong(String str, long fallback) {
+ if (str == null || str.length() == 0) {
+ return fallback;
+ }
+ long result;
+ try {
+ result = Long.parseLong(str);
+ } catch (NumberFormatException e) {
+ result = fallback;
+ }
+ return result;
+ }
+}
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 7f6612da6..8b3c5bb3f 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -123,7 +123,6 @@
این مخزن از پیش برپا شده است. تأیید کنید که میخواهید دوباره فعّالش کنید.
اثر انگشت بد
این یک نشانی اینترنتی معتیبر نیست.
-
همرسانی فروشگاه بِل با بلوتوث
تنظیمات
مخزن جدید
@@ -147,7 +146,6 @@
همواره شامل کارههایی که نیازمند صفحهلمسی هستند
مخزن محلّی
-
فروشگاه بِل آمادهٔ تبادل است
برای دیدن جزییات و اجازه به دیگرات برای تبادل کارههایتان، لمس کنید.
در حال حذف مخزن جاری…
@@ -213,7 +211,10 @@
تمام کارهها بهروز هستند.\n\nتبریک! تمام کارههایتان بهروز هستند (یا مخزنهایتان منقضی شدهاند).
دسترسی ریشه داده نشد
+<<<<<<< 55ca6129e849c11398c95bc7685498ad100fc61d
+=======
+>>>>>>> fix names and color
یا افزارهٔ اندرویدیتان روت شده نیست، یا درخواست ریشه برای فروشگاه بِل را رد کردید.
حذف نصب
@@ -227,7 +228,10 @@
برای گشودن شبکههای موجود، ضربه بزنید
برای تعویض به یک شبکهٔ وایفای، ضربه بزنید
گشودن پویشگر QR
+<<<<<<< 55ca6129e849c11398c95bc7685498ad100fc61d
+=======
+>>>>>>> fix names and color
به فروشگاه بِل خوش آمدید!
میخواهید اکنون کارهها را از %1$s بگیرید؟
دیگر این را نشان نده
@@ -246,7 +250,10 @@
غیرقابل مشاهده از طریق وایفای
نام افزاره
نمیتوانید کسی که دنبالشید را پیدا کنید؟
+<<<<<<< 55ca6129e849c11398c95bc7685498ad100fc61d
+=======
+>>>>>>> fix names and color
ارسال فروشگاه بِل
نمیتوان افراد نزدیک را برای تبادل با آنها پیدا کرد.
در حال اتّصال
@@ -278,7 +285,10 @@
تمام مخزنهای دیگر خطایی ایاد نکردند.
این مخزن هنوز استفاده نشده است. برای دیدن کارههایی که فراهم میکند، باید فعّالش کنید.
+<<<<<<< 55ca6129e849c11398c95bc7685498ad100fc61d
+=======
+>>>>>>> fix names and color
حذف یک مخزن به این معناست که کارههایش دیگر در فروشگاه بِل موجود نخواهند بود.\n\nتوجّه: تمام کارههای نصب شدهٔ پیشین، روی افزارهتان باقی خواهند ماند.
%1$s غیرفعّال شد.\n\nبرای نصب کارهها از آن باید دوباره فعّالش کنید.
مخزن فروشگاه بِل %1$s ذخیره شد.
@@ -329,7 +339,6 @@
هیچ برنامهٔ مطابقی وجود ندارد.
هیچ برنامهٔ مطابقی برای بهروز رسانی وجود ندارد.
-
فروشگاه بِل از کار افتاد
خطایی پیشبینی نشده رخ داد که برنامه را وادار به توقّف کرد. میخواهید جزئیات را برای کمک به حل کردن مشکل رایانامه کنید؟
در اینجا میتوانید اطّلاعات اضافی و نظرها را بیفزایید:
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d23295a97..aa3e98f22 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
- F-Droid
+ Bel Market
The new version is signed with a different key to the old one. To install the new version, the old one must be uninstalled first. Please do this and try again. (Note that uninstalling will erase any internal data stored by the application)
It seems like this package is not compatible with your device. Do you want to try and install it anyway?
@@ -31,7 +31,7 @@
Update history
Days to consider apps new or recent: %s
Privileged Extension
- Use F-Droid Privileged Extension to install, update, and remove packages
+ Use Bel Market Privileged Extension to install, update, and remove packages
Update/Uninstall Privileged Extension
Open details screen of Privileged Extension to update/uninstall it
Name of your Local Repo
@@ -48,7 +48,7 @@
App Details
No such app found.
- About F-Droid
+ About Bel Market
Version
Website
Source code
@@ -86,7 +86,7 @@
Updates (%d)
1 update is available.
%d updates are available.
- F-Droid Updates Available
+ Bel Market Updates Available
+%1$d more…
No Bluetooth send method found, choose one!
Choose Bluetooth send method
@@ -105,7 +105,7 @@
Update Repos
Repositories
- Share F-Droid by Bluetooth
+ Share Bel Market by Bluetooth
Settings
About
Search
@@ -158,7 +158,7 @@
Always include apps that require touchscreen
Local Repo
- F-Droid is ready to swap
+ Bel Market is ready to swap
Touch to view details and allow others to swap your apps.
Deleting current repo…
Adding %s to repo…
@@ -222,12 +222,12 @@
Unknown
Delete Repository?
Deleting a repository means
- apps from it will no longer be available from F-Droid.\n\nNote: All
+ apps from it will no longer be available from Bel Market.\n\nNote: All
previously installed apps will remain on your device.
Disabled "%1$s".\n\nYou will
need to re-enable this repository to install apps from it.
- Saved F-Droid repository %1$s.
- Looking for F-Droid repository at\n%1$s
+ Saved Bel Market repository %1$s.
+ Looking for Bel Market repository at\n%1$s
%s or later
Up to %s
%1$s up to %2$s
@@ -261,7 +261,7 @@
Time
Writing
- No apps installed.\n\nThere are apps on your device, but they are not available from F-Droid. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.
+ No apps installed.\n\nThere are apps on your device, but they are not available from Bel Market. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.
No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.
All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).
No matching installed applications.
@@ -270,31 +270,31 @@
Requesting root access…
Root access denied
- Either your Android device is not rooted or you have denied root access for F-Droid.
+ Either your Android device is not rooted or you have denied root access for Bel Market.
Failed to install due to an unknown error
Failed to uninstall due to an unknown error
- F-Droid Privileged Extension is not available
- This option is only available when F-Droid Privileged Extension is installed.
+ Bel Market Privileged Extension is not available
+ This option is only available when Bel Market Privileged Extension is installed.
The signature of the extension is wrong! Please create a bug report!
The privileged permissions have not been granted to the extension! Please create a bug report!
Install
Open Extension
- Successfully installed F-Droid Privileged Extension
- Installation of F-Droid Privileged Extension failed
- F-Droid Privileged Extension has been successfully installed. This allows F-Droid to install, upgrade and uninstall apps on its own.
- The installation of F-Droid Privileged Extension has failed. The installation method is not supported by all Android distributions, please consult the F-Droid bug tracker for more information.
+ Successfully installed Bel Market Privileged Extension
+ Installation of Bel Market Privileged Extension failed
+ Bel Market Privileged Extension has been successfully installed. This allows Bel Market to install, upgrade and uninstall apps on its own.
+ The installation of Bel Market Privileged Extension has failed. The installation method is not supported by all Android distributions, please consult the Bel Market bug tracker for more information.
Installing…
Installing and rebooting…
Uninstalling…
- Do you want to install F-Droid Privileged Extension?
+ Do you want to install Bel Market Privileged Extension?
This takes up to 10 seconds.
This takes up to 10 seconds and the device will be <b>rebooted</b> afterwards.
- Do you want to uninstall F-Droid Privileged Extension?
+ Do you want to uninstall Bel Market Privileged Extension?
Uninstall
- Installation of F-Droid Privileged Extension is currently not supported on Android 5.1 or later.
+ Installation of Bel Market Privileged Extension is currently not supported on Android 5.1 or later.
Touch to swap
- If your friend has F-Droid and NFC turned on touch your devices together.
+ If your friend has Bel Market and NFC turned on touch your devices together.
Join the same Wi-Fi as your friend
To swap using Wi-Fi, ensure you are on the same network. If you don\'t have access to the same network, one of you can create a Wi-Fi Hotspot.
Help your friend join your hotspot
@@ -305,7 +305,7 @@
Tap to open available networks
Tap to switch to a Wi-Fi network
Open QR Scanner
- Welcome to F-Droid!
+ Welcome to Bel Market!
Do you want to get apps from %1$s now?
Don\'t show this again
One person needs to scan the code, or type the URL of the other in a browser.
@@ -324,13 +324,13 @@
Not visible via Wi-Fi
Device Name
Can\'t find who you\'re looking for?
- Send F-Droid
+ Send Bel Market
Could not find people nearby to swap with.
Connecting
Confirm swap
The QR code you scanned doesn\'t look like a swap code.
Bluetooth unavailable
- Cannot send F-Droid, because Bluetooth is unavailable on this device.
+ Cannot send Bel Market, because Bluetooth is unavailable on this device.
Loading…
An error occurred while connecting to device, we can\'t seem to swap with it.
Swapping not enabled
@@ -389,7 +389,7 @@
Dark
Night
- F-Droid has crashed
+ Bel Market has crashed
An unexpected error occurred
forcing the application to stop. Would you like to e-mail the
details to help fix the issue?