fix strings fa
This commit is contained in:
commit
61b96c9093
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fdroid.fdroid"
|
||||
package="org.belos.belmarket"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-sdk
|
||||
|
397
app/src/main/java/org/fdroid/fdroid/#RepoUpdater.java#
Normal file
397
app/src/main/java/org/fdroid/fdroid/#RepoUpdater.java#
Normal file
@ -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.
|
||||
*
|
||||
* <b>WARNING</b>: 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<Apk> 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:
|
||||
* <li>in the downloaded jar</li>
|
||||
* <li>in the index XML</li>
|
||||
* <li>stored in the local database</li>
|
||||
* 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!");
|
||||
}
|
||||
|
||||
}
|
307
app/src/main/java/org/fdroid/fdroid/#RepoXMLHandler.java#
Normal file
307
app/src/main/java/org/fdroid/fdroid/#RepoXMLHandler.java#
Normal file
@ -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<Apk> 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<Apk> 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 = "<p>" + str + "</p>";
|
||||
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;
|
||||
}
|
||||
}
|
@ -13,7 +13,11 @@
|
||||
<string name="notify_on">نمایش یک آگهی هنگام موجود بودن بهروز رسانیها</string>
|
||||
<string name="update_history">بهروزرسانی تاریخچه</string>
|
||||
<string name="no_such_app">چنین کارهای پیدا نشد.</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="about_title">دربارهٔ فروشگاه بل</string>
|
||||
=======
|
||||
<string name="about_title">دربارهٔ فروشگاه بِل</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="about_mail">رایانامه</string>
|
||||
<string name="app_installed">نصب شده</string>
|
||||
<string name="app_not_installed">نصب نشده</string>
|
||||
@ -28,7 +32,11 @@
|
||||
<string name="add_key">افزودن کلید</string>
|
||||
<string name="overwrite">جانویسی</string>
|
||||
<string name="tab_updates">بهروز رسانیها</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="fdroid_updates_available">بهروزرسانیهای فروشگاه بل موجودند</string>
|
||||
=======
|
||||
<string name="fdroid_updates_available">بهروزرسانیهای فروشگاه بِل موجودند</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="bluetooth_activity_not_found">هیچ روش ارسال با بلوتوثی پیدا نشد، یکی را برگزینید!</string>
|
||||
<string name="choose_bt_send">گزینش روش ارسال بلوتوث</string>
|
||||
<string name="repo_add_url">نشانی مخزن</string>
|
||||
@ -71,7 +79,11 @@
|
||||
<string name="up_to_maxsdk">تا %s</string>
|
||||
<string name="minsdk_up_to_maxsdk">%1$s تا %2$s</string>
|
||||
<string name="requires_features">نیاز دارد: %1$s</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="app_name">فروشگاه بل</string>
|
||||
=======
|
||||
<string name="app_name">فروشگاه بِل</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
|
||||
<string name="SignatureMismatch">نگارش جدید با کلیدی متفاوت با قبلی امضا شده است. برای نصب نگارش جدید، باید نخست نگارش قدیمی را حذف کرد. لطفاً این کار را انجام داده و دوباره تلاش کنید. (به خاطر داشته باشید که حذف نصب، تمام دادههای درونی ذخیره شدهٔ کاره را پاک میکند)</string>
|
||||
<string name="version">نگارش</string>
|
||||
@ -81,7 +93,11 @@
|
||||
<string name="notify">آگهیهای بهروز رسانی</string>
|
||||
<string name="update_history_summ">روزهایی که کاره جدید یا اخیر محسوب شود: %s</string>
|
||||
<string name="system_installer">افزونهٔ ممتاز</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="system_installer_on">استفاده از افزونهٔ ممتاز فروشگاه بل برای نصب، بهروز رسانی و حذف بستهها</string>
|
||||
=======
|
||||
<string name="system_installer_on">استفاده از افزونهٔ ممتاز فروشگاه بِل برای نصب، بهروز رسانی و حذف بستهها</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="uninstall_system">بهروز رسانی/حذف افزونهٔ ممتاز</string>
|
||||
<string name="uninstall_system_summary">گشودن صفحهٔ جزییات افزونهٔ ممتاز برای بهروز رسانی/حذف آن</string>
|
||||
<string name="local_repo_name">نام مخزن محلّی شما</string>
|
||||
@ -123,7 +139,11 @@
|
||||
<string name="repo_exists_enable">این مخزن از پیش برپا شده است. تأیید کنید که میخواهید دوباره فعّالش کنید.</string>
|
||||
<string name="bad_fingerprint">اثر انگشت بد</string>
|
||||
<string name="invalid_url">این یک نشانی اینترنتی معتیبر نیست.</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="menu_send_apk_bt">همرسانی فروشگاه بل با بلوتوث</string>
|
||||
=======
|
||||
<string name="menu_send_apk_bt">همرسانی فروشگاه بِل با بلوتوث</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="menu_settings">تنظیمات</string>
|
||||
<string name="menu_add_repo">مخزن جدید</string>
|
||||
|
||||
@ -146,7 +166,11 @@
|
||||
<string name="ignoreTouch_on">همواره شامل کارههایی که نیازمند صفحهلمسی هستند</string>
|
||||
|
||||
<string name="local_repo">مخزن محلّی</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="local_repo_running">فروشگاه بل آمادهٔ تبادل است</string>
|
||||
=======
|
||||
<string name="local_repo_running">فروشگاه بِل آمادهٔ تبادل است</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="touch_to_configure_local_repo">برای دیدن جزییات و اجازه به دیگرات برای تبادل کارههایتان، لمس کنید.</string>
|
||||
<string name="deleting_repo">در حال حذف مخزن جاری…</string>
|
||||
<string name="adding_apks_format">در حال افزودن %s به مخزن…</string>
|
||||
@ -211,7 +235,11 @@
|
||||
<string name="empty_can_update_app_list">تمام کارهها بهروز هستند.\n\nتبریک! تمام کارههایتان بهروز هستند (یا مخزنهایتان منقضی شدهاند).</string>
|
||||
|
||||
<string name="root_access_denied_title">دسترسی ریشه داده نشد</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="root_access_denied_body">یا افزارهٔ اندرویدیتان روت شده نیست، یا درخواست ریشه برای فروشگاه بل را رد کردید.</string>
|
||||
=======
|
||||
<string name="root_access_denied_body">یا افزارهٔ اندرویدیتان روت شده نیست، یا درخواست ریشه برای فروشگاه بِل را رد کردید.</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="system_uninstall_button">حذف نصب</string>
|
||||
|
||||
<string name="swap_nfc_title">برای تبادل، لمس کنید</string>
|
||||
@ -224,7 +252,11 @@
|
||||
<string name="swap_view_available_networks">برای گشودن شبکههای موجود، ضربه بزنید</string>
|
||||
<string name="swap_switch_to_wifi">برای تعویض به یک شبکهٔ وایفای، ضربه بزنید</string>
|
||||
<string name="open_qr_code_scanner">گشودن پویشگر QR</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="swap_welcome">به فروشگاه بل خوش آمدید!</string>
|
||||
=======
|
||||
<string name="swap_welcome">به فروشگاه بِل خوش آمدید!</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="swap_confirm_connect">میخواهید اکنون کارهها را از %1$s بگیرید؟</string>
|
||||
<string name="swap_dont_show_again">دیگر این را نشان نده</string>
|
||||
<string name="swap_scan_or_type_url">لازم است یک نفر رمز دیگری را پوییده، یا نشانی اینترنتیش را در مرورگری بنویسد.</string>
|
||||
@ -242,7 +274,11 @@
|
||||
<string name="swap_not_visible_wifi">غیرقابل مشاهده از طریق وایفای</string>
|
||||
<string name="swap_wifi_device_name">نام افزاره</string>
|
||||
<string name="swap_cant_find_peers">نمیتوانید کسی که دنبالشید را پیدا کنید؟</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="swap_send_fdroid">ارسال فروشگاه بل</string>
|
||||
=======
|
||||
<string name="swap_send_fdroid">ارسال فروشگاه بِل</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="swap_no_peers_nearby">نمیتوان افراد نزدیک را برای تبادل با آنها پیدا کرد.</string>
|
||||
<string name="swap_connecting">در حال اتّصال</string>
|
||||
<string name="swap_confirm">تأیید تبادل</string>
|
||||
@ -273,6 +309,7 @@
|
||||
|
||||
<string name="all_other_repos_fine">تمام مخزنهای دیگر خطایی ایاد نکردند.</string>
|
||||
<string name="repo_not_yet_updated">این مخزن هنوز استفاده نشده است. برای دیدن کارههایی که فراهم میکند، باید فعّالش کنید.</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="repo_confirm_delete_body">حذف یک مخزن به این معناست که کارههایش دیگر در فروشگاه بل موجود نخواهند بود.\n\nتوجّه: تمام کارههای نصب شدهٔ پیشین، روی افزارهتان باقی خواهند ماند.</string>
|
||||
<string name="repo_disabled_notification">%1$s غیرفعّال شد.\n\nبرای نصب کارهها از آن باید دوباره فعّالش کنید.</string>
|
||||
<string name="repo_added">مخزن فروشگاه بل %1$s ذخیره شد.</string>
|
||||
@ -283,10 +320,23 @@
|
||||
<string name="uninstall_error_unknown">شکست در نصب به دلیل خطایی ناشناخته</string>
|
||||
<string name="system_install_denied_title">افزونهٔ ممتاز فروشگاه بل موجود نیست</string>
|
||||
<string name="system_install_denied_body">این گزینه فقط هنگام نصب بودن افزونهٔ ممتاز فروشگاه بل، موجود است.</string>
|
||||
=======
|
||||
<string name="repo_confirm_delete_body">حذف یک مخزن به این معناست که کارههایش دیگر در فروشگاه بِل موجود نخواهند بود.\n\nتوجّه: تمام کارههای نصب شدهٔ پیشین، روی افزارهتان باقی خواهند ماند.</string>
|
||||
<string name="repo_disabled_notification">%1$s غیرفعّال شد.\n\nبرای نصب کارهها از آن باید دوباره فعّالش کنید.</string>
|
||||
<string name="repo_added">مخزن فروشگاه بِل %1$s ذخیره شد.</string>
|
||||
<string name="repo_searching_address">در حال گشتن به دنبال مخزن فروشگاه بِل در\n%1$s</string>
|
||||
<string name="empty_installed_app_list">هیچ کارهای نصب نشده است.\n\nکارههایی روی افزارهتان وجود دارند، ولی در فروشگاه بِل موجود نیستند. دلیل این امر میتواند این دلیل باشد که مخازنتان کارههایتان را موجود ندارند یا باید بهروز شوند.</string>
|
||||
<string name="empty_available_app_list">هیچ کارهای در این دسته نیست.\n\nتلاش کنید دستهای دیگر برگزیده یا مخازنتان را برای گرفتن فهرست تازهای از کارهها بهروز کنید.</string>
|
||||
<string name="install_error_unknown">شکست در نصب به دلیل خطایی ناشناخته</string>
|
||||
<string name="uninstall_error_unknown">شکست در نصب به دلیل خطایی ناشناخته</string>
|
||||
<string name="system_install_denied_title">افزونهٔ ممتاز فروشگاه بِل موجود نیست</string>
|
||||
<string name="system_install_denied_body">این گزینه فقط هنگام نصب بودن افزونهٔ ممتاز فروشگاه بِل، موجود است.</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="system_install_denied_signature">امضای افزونه اشتباه است! لطفاً یک گزارش خطا ایجاد کنید!</string>
|
||||
<string name="system_install_denied_permissions">اجازههای ممتاز به افزونه داده نشده است! لطفاً یک گزارش خطا ایجاد کنید!</string>
|
||||
<string name="system_install_button_install">نصب</string>
|
||||
<string name="system_install_button_open">گشودن افزونه</string>
|
||||
<<<<<<< HEAD
|
||||
<string name="system_install_post_success">افزونهٔ ممتاز فروشگاه بل با موفّقیت نصب شد</string>
|
||||
<string name="system_install_post_fail">نصب افزونهٔ ممتاز فروشگاه بل شکست خورد</string>
|
||||
<string name="system_install_post_success_message">افزونهٔ ممتاز فروشگاه بل با موفّقیت نصب شد. این مار به فروشگاه بل اجازه میدهد کارهها را توسّط خودش نصب کرده، ارتفا دهد و حدف کند.</string>
|
||||
@ -303,6 +353,24 @@
|
||||
<string name="swap_qr_isnt_for_swap">رمز QR ای که پوییدید، شبیه یک رمز تبادل نیست.</string>
|
||||
<string name="bluetooth_unavailable">بلوتوث موجود نیست</string>
|
||||
<string name="swap_cant_send_no_bluetooth">نمیتوان فروشگاه بل را فرستاد، زیرا بلوتوث روی این افزاره موحود نیست.</string>
|
||||
=======
|
||||
<string name="system_install_post_success">افزونهٔ ممتاز فروشگاه بِل با موفّقیت نصب شد</string>
|
||||
<string name="system_install_post_fail">نصب افزونهٔ ممتاز فروشگاه بِل شکست خورد</string>
|
||||
<string name="system_install_post_success_message">افزونهٔ ممتاز فروشگاه بِل با موفّقیت نصب شد. این مار به فروشگاه بِل اجازه میدهد کارهها را توسّط خودش نصب کرده، ارتفا دهد و حدف کند.</string>
|
||||
<string name="system_install_post_fail_message">نصب افزونهٔ ممتاز فروشگاه بِل شکست خورد. روش نصب توسّط همهٔ توزیعهای اندروید پشتیبانی نمیشود. لطفاً برای اطّلاعات بیشتر با ردیاب مشکل فروشگاه بِل مشاوره کنید.</string>
|
||||
<string name="system_install_installing">در حال نصب…</string>
|
||||
<string name="system_install_installing_rebooting">در حال نصب و راهانداری دوباره…</string>
|
||||
<string name="system_install_uninstalling">در حال حذف نصب…</string>
|
||||
<string name="system_install_question">میخواهید افزونهٔ ممتاز فروشگاه بِل را نصب کنید؟</string>
|
||||
<string name="system_install_warning">این کار بیش از ۱۰ ثانیه زمان میبرد.</string>
|
||||
<string name="system_install_warning_lollipop">این کار بیش از ۱۰ ثانیه زمان برده و پس از آن، افزاره <b>راهاندازی مجدّد</b> خواهد شد.</string>
|
||||
<string name="system_uninstall">میخواهید افزونهٔ ممتاز فروشگاه بِل را حذف کنید؟</string>
|
||||
<string name="swap_nfc_description">اگر دوستتان فروشگاه بِل داشته و NFC اش روشن است، افزارههایتان را با هم تماس بدهید.</string>
|
||||
<string name="swap_join_same_wifi_desc">برای تبادل با استفاده از وایفای، مطمئن شوید که روی شبکهٔ یکسانی هستید. اگر به شبکهٔ یکسان دسترسی ندارید، یکی از شما میتواند یک نقطهٔ داغ وایفای ایجاد کند.</string>
|
||||
<string name="swap_qr_isnt_for_swap">رمز QR ای که پوییدید، شبیه یک رمز تبادل نیست.</string>
|
||||
<string name="bluetooth_unavailable">بلوتوث موجود نیست</string>
|
||||
<string name="swap_cant_send_no_bluetooth">نمیتوان فروشگاه بِل را فرستاد، زیرا بلوتوث روی این افزاره موحود نیست.</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="loading">در حال بار کردن…</string>
|
||||
<string name="swap_connection_misc_error">هنگام اتّصال به افزاره خطایی رخ داد. به نظر میرسد با این وضع نمیتوانیم تبادل کنیم.</string>
|
||||
<string name="swap_not_enabled">تبادل فعّال نیست</string>
|
||||
@ -323,7 +391,11 @@
|
||||
<string name="empty_search_available_app_list">هیچ برنامهٔ مطابقی وجود ندارد.</string>
|
||||
<string name="empty_search_can_update_app_list">هیچ برنامهٔ مطابقی برای بهروز رسانی وجود ندارد.</string>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<string name="crash_dialog_title">فروشگاه بل از کار افتاد</string>
|
||||
=======
|
||||
<string name="crash_dialog_title">فروشگاه بِل از کار افتاد</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
<string name="crash_dialog_text">خطایی پیشبینی نشده رخ داد که برنامه را وادار به توقّف کرد. میخواهید جزئیات را برای کمک به حل کردن مشکل رایانامه کنید؟</string>
|
||||
<string name="crash_dialog_comment_prompt">در اینجا میتوانید اطّلاعات اضافی و نظرها را بیفزایید:</string>
|
||||
<string name="useTor">استفاده از تور</string>
|
||||
@ -339,7 +411,11 @@
|
||||
|
||||
<string name="downloading_apk">در حال بارگیری %1$s</string>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<string name="system_install_not_supported">در حال حاضر نصب افزونهٔ ممتاز فروشگاه بل روی اندروید ۵.۱ یا بالاتر پشتیبانی نمیشود.</string>
|
||||
=======
|
||||
<string name="system_install_not_supported">در حال حاضر نصب افزونهٔ ممتاز فروشگاه بِل روی اندروید ۵.۱ یا بالاتر پشتیبانی نمیشود.</string>
|
||||
>>>>>>> 1e8d7655bc355d2a3395ee254d0f6fbf4cf9294d
|
||||
|
||||
<string name="keep_hour">۱ ساعت</string>
|
||||
<string name="keep_day">۱ روز</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user