diff --git a/src/main/kotlin/com/looker/droidify/MainApplication.kt b/src/main/kotlin/com/looker/droidify/MainApplication.kt index 14936841..7d75bc4a 100644 --- a/src/main/kotlin/com/looker/droidify/MainApplication.kt +++ b/src/main/kotlin/com/looker/droidify/MainApplication.kt @@ -5,21 +5,17 @@ import android.app.Application import android.app.job.JobInfo import android.app.job.JobScheduler import android.content.* -import android.content.pm.PackageInfo import com.looker.droidify.content.Cache import com.looker.droidify.content.Preferences import com.looker.droidify.content.ProductPreferences import com.looker.droidify.database.Database -import com.looker.droidify.entity.InstalledItem import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.network.Downloader import com.looker.droidify.network.PicassoDownloader import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService -import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.Utils.toInstalledItem import com.looker.droidify.utility.extension.android.Android -import com.looker.droidify.utility.extension.android.singleSignature -import com.looker.droidify.utility.extension.android.versionCodeCompat import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.Picasso import java.net.InetSocketAddress @@ -27,10 +23,6 @@ import java.net.Proxy @Suppress("unused") class MainApplication : Application() { - private fun PackageInfo.toInstalledItem(): InstalledItem { - val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty() - return InstalledItem(packageName, versionName.orEmpty(), versionCodeCompat, signatureString) - } override fun onCreate() { super.onCreate() diff --git a/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt b/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt index 7a325323..87f27953 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt @@ -26,6 +26,8 @@ import com.looker.droidify.service.Connection import com.looker.droidify.service.DownloadService import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.Utils.startPackageInstaller +import com.looker.droidify.utility.Utils.startUpdate import com.looker.droidify.utility.extension.android.* import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable @@ -134,7 +136,6 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { } addOnScrollListener(scrollListener) addItemDecoration(adapter.gridItemDecoration) -// addItemDecoration(DividerItemDecoration(context, adapter::configureDivider)) savedInstanceState?.getParcelable(STATE_ADAPTER) ?.let(adapter::restoreState) layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) @@ -385,27 +386,7 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { ProductAdapter.Action.INSTALL, ProductAdapter.Action.UPDATE -> { val installedItem = installed?.installedItem - val productRepository = Product.findSuggested(products, installedItem) { it.first } - val compatibleReleases = productRepository?.first?.selectedReleases.orEmpty() - .filter { installedItem == null || installedItem.signature == it.signature } - val release = if (compatibleReleases.size >= 2) { - compatibleReleases - .filter { it.platforms.contains(Android.primaryPlatform) } - .minByOrNull { it.platforms.size } - ?: compatibleReleases.minByOrNull { it.platforms.size } - ?: compatibleReleases.firstOrNull() - } else { - compatibleReleases.firstOrNull() - } - val binder = downloadConnection.binder - if (productRepository != null && release != null && binder != null) { - binder.enqueue( - packageName, - productRepository.first.name, - productRepository.second, - release - ) - } else Unit + startUpdate(packageName, installedItem, products, downloadConnection) } ProductAdapter.Action.LAUNCH -> { val launcherActivities = installed?.launcherActivities.orEmpty() diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt index d41fb830..2392e440 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -1,7 +1,6 @@ package com.looker.droidify.screen import android.content.Intent -import android.net.Uri import android.os.Bundle import android.os.Parcel import android.view.ViewGroup @@ -11,15 +10,12 @@ import android.widget.Toolbar import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import com.looker.droidify.R -import com.looker.droidify.content.Cache import com.looker.droidify.content.Preferences import com.looker.droidify.database.CursorOwner import com.looker.droidify.utility.KParcelable -import com.looker.droidify.utility.extension.android.Android +import com.looker.droidify.utility.Utils.startPackageInstaller import com.looker.droidify.utility.extension.resources.getDrawableFromAttr import com.looker.droidify.utility.extension.text.nullIfEmpty -import com.topjohnwu.superuser.Shell -import java.io.File abstract class ScreenActivity : FragmentActivity() { companion object { @@ -219,11 +215,7 @@ abstract class ScreenActivity : FragmentActivity() { is SpecialIntent.Install -> { val packageName = specialIntent.packageName if (!packageName.isNullOrEmpty()) { - val fragment = currentFragment - if (fragment !is ProductFragment || fragment.packageName != packageName) { - pushFragment(ProductFragment(packageName)) - } - specialIntent.cacheFileName?.let(::startPackageInstaller) + specialIntent.cacheFileName?.let { startPackageInstaller(it) } } Unit } @@ -244,37 +236,6 @@ abstract class ScreenActivity : FragmentActivity() { } } - internal fun startPackageInstaller(cacheFileName: String) { - val file = Cache.getReleaseFile(this, cacheFileName) - if (Preferences[Preferences.Key.RootPermission]) { - val commandBuilder = StringBuilder() - commandBuilder.append("settings put global verifier_verify_adb_installs 0 ; ") - commandBuilder.append( - getPackageInstallCommand(file) - ) - commandBuilder.append(" ; settings put global verifier_verify_adb_installs 1") - Shell.su(commandBuilder.toString()).exec() - } else { - val (uri, flags) = if (Android.sdk(24)) { - Pair( - Cache.getReleaseUri(this, cacheFileName), - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - } else { - Pair(Uri.fromFile(file), 0) - } - // TODO Handle deprecation - @Suppress("DEPRECATION") - startActivity( - Intent(Intent.ACTION_INSTALL_PACKAGE) - .setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags) - ) - } - } - - private fun getPackageInstallCommand(cacheFile: File): String = - "cat \"${cacheFile.absolutePath}\" | pm install -t -r -S ${cacheFile.length()}" - internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName)) internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) internal fun navigatePreferences() = pushFragment(SettingsFragment()) diff --git a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt index ea0dde96..faf4454f 100644 --- a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt @@ -182,7 +182,9 @@ class SettingsFragment : ScreenFragment() { preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled) preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled) } - preferences[Preferences.Key.RootPermission]?.setEnabled(Shell.getShell().isRoot) + preferences[Preferences.Key.RootPermission]?.setEnabled( + Shell.getCachedShell()?.isRoot ?: Shell.getShell().isRoot + ) if (key == Preferences.Key.Theme) { requireActivity().recreate() } diff --git a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt index 740899d3..06df15ce 100644 --- a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt +++ b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt @@ -14,6 +14,7 @@ import com.looker.droidify.Common import com.looker.droidify.MainActivity import com.looker.droidify.R import com.looker.droidify.content.Cache +import com.looker.droidify.content.Preferences import com.looker.droidify.entity.Release import com.looker.droidify.entity.Repository import com.looker.droidify.network.Downloader @@ -300,9 +301,17 @@ class DownloadService : ConnectionService() { stateSubject.onNext(State.Success(task.packageName, task.name, task.release) { consumed = true }) - if (!consumed) { - showNotificationInstall(task) - } + if (consumed || (Preferences[Preferences.Key.RootPermission])) { + PendingIntent.getBroadcast( + this, + 0, + Intent(this, Receiver::class.java) + .setAction("$ACTION_INSTALL.${task.packageName}") + .putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName), + PendingIntent.FLAG_UPDATE_CURRENT + ) + .send() + } else showNotificationInstall(task) } private fun validatePackage(task: Task, file: File): ValidationError? { @@ -436,7 +445,7 @@ class DownloadService : ConnectionService() { .observeOn(AndroidSchedulers.mainThread()) .subscribe { result, throwable -> currentTask = null - throwable?.printStackTrace() + throwable.printStackTrace() if (result == null || !result.success) { showNotificationError( task, diff --git a/src/main/kotlin/com/looker/droidify/utility/Utils.kt b/src/main/kotlin/com/looker/droidify/utility/Utils.kt index 769a8c8b..5a11af88 100644 --- a/src/main/kotlin/com/looker/droidify/utility/Utils.kt +++ b/src/main/kotlin/com/looker/droidify/utility/Utils.kt @@ -1,15 +1,31 @@ package com.looker.droidify.utility import android.animation.ValueAnimator +import android.app.Activity import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo import android.content.pm.Signature import android.graphics.drawable.Drawable +import android.net.Uri import android.provider.Settings +import android.util.Log import com.looker.droidify.R +import com.looker.droidify.content.Cache +import com.looker.droidify.content.Preferences +import com.looker.droidify.entity.InstalledItem +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Repository +import com.looker.droidify.service.Connection +import com.looker.droidify.service.DownloadService import com.looker.droidify.utility.extension.android.Android +import com.looker.droidify.utility.extension.android.singleSignature +import com.looker.droidify.utility.extension.android.versionCodeCompat import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getDrawableCompat import com.looker.droidify.utility.extension.text.hex +import com.topjohnwu.superuser.Shell +import java.io.File import java.security.MessageDigest import java.security.cert.Certificate import java.security.cert.CertificateEncodingException @@ -21,6 +37,11 @@ object Utils { .apply { setTintList(context.getColorFromAttr(tintAttrResId)) } } + fun PackageInfo.toInstalledItem(): InstalledItem { + val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty() + return InstalledItem(packageName, versionName.orEmpty(), versionCodeCompat, signatureString) + } + fun getDefaultApplicationIcons(context: Context): Pair { val progressIcon: Drawable = createDefaultApplicationIcon(context, android.R.attr.textColorSecondary) @@ -78,4 +99,92 @@ object Utils { ) != 0f } } + + internal fun Activity.startPackageInstaller(cacheFileName: String) { + val file = Cache.getReleaseFile(this, cacheFileName) + if (Preferences[Preferences.Key.RootPermission]) { + val commandBuilder = StringBuilder() + val verifyState = getVerifyState() + if (verifyState == "1") commandBuilder.append("settings put global verifier_verify_adb_installs 0 ; ") + commandBuilder.append(getPackageInstallCommand(file)) + commandBuilder.append(" ; settings put global verifier_verify_adb_installs $verifyState") + val result = Shell.su(commandBuilder.toString()).exec() + if (result.isSuccess) Shell.su("${getUtilBoxPath()} rm ${quote(file.absolutePath)}") + } else { + val (uri, flags) = if (Android.sdk(24)) { + Pair( + Cache.getReleaseUri(this, cacheFileName), + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } else { + Pair(Uri.fromFile(file), 0) + } + // TODO Handle deprecation + @Suppress("DEPRECATION") + startActivity( + Intent(Intent.ACTION_INSTALL_PACKAGE) + .setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags) + ) + } + } + + fun startUpdate( + packageName: String, + installedItem: InstalledItem?, + products: List>, + downloadConnection: Connection + ) { + val productRepository = Product.findSuggested(products, installedItem) { it.first } + val compatibleReleases = productRepository?.first?.selectedReleases.orEmpty() + .filter { installedItem == null || installedItem.signature == it.signature } + val release = if (compatibleReleases.size >= 2) { + compatibleReleases + .filter { it.platforms.contains(Android.primaryPlatform) } + .minByOrNull { it.platforms.size } + ?: compatibleReleases.minByOrNull { it.platforms.size } + ?: compatibleReleases.firstOrNull() + } else { + compatibleReleases.firstOrNull() + } + val binder = downloadConnection.binder + if (productRepository != null && release != null && binder != null) { + binder.enqueue( + packageName, + productRepository.first.name, + productRepository.second, + release + ) + } else Unit + } + + private fun getPackageInstallCommand(cacheFile: File): String = + "cat \"${cacheFile.absolutePath}\" | pm install -t -r -S ${cacheFile.length()}" + + private fun getVerifyState(): String = + Shell.sh("settings get global verifier_verify_adb_installs").exec().out[0] + + private fun quote(string: String) = + "\"${string.replace(Regex("""[\\$"`]""")) { c -> "\\${c.value}" }}\"" + + private fun getUtilBoxPath(): String { + listOf("toybox", "busybox").forEach { + var shellResult = Shell.su("which $it").exec() + if (shellResult.out.isNotEmpty()) { + val utilBoxPath = shellResult.out.joinToString("") + if (utilBoxPath.isNotEmpty()) { + val utilBoxQuoted = quote(utilBoxPath) + shellResult = Shell.su("$utilBoxQuoted --version").exec() + if (shellResult.out.isNotEmpty()) { + val utilBoxVersion = shellResult.out.joinToString("") + Log.i( + this.javaClass.canonicalName, + "Using Utilbox $it : $utilBoxQuoted $utilBoxVersion" + ) + } + return utilBoxQuoted + } + } + } + return "" + } } diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 0aa46c4d..655941fb 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1,172 +1,172 @@ - A ação falhou - Adicionar repositório - Endereço - Todos os aplicativos - Todos os aplicativos estão atualizados - Já existe - Sempre - Amoled - Características indesejadas - Aplicativo - Aplicativo não encontrado - Email do autor - Página do autor - Disponível - Rastreador de erros - Cancelar - Não é possível editar o repositório pois ele está sincronizando no momento. - Lista de mudanças - Mudanças - Checando o repositório - Compilado para depuração - Confirmação - Conectando - Contém mídia não livre - Não foi possível baixar %s - Não foi possível sincronizar %s - Não foi possível validar %s - Escuro - Data de adição - Excluir - Tem certeza que deseja excluir o repositório? - Descrição - Detalhes - Doar - Baixado %s - Baixando - Baixando %s - Editar repositório - Formato de arquivo inválido. - Fingerprint - Contém anúncio - Possui dependências não livres - Possui vulnerabilidades de segurança - Resposta de servidor inválida. - Proxy HTTP - Ignorar todas as atualizações - Ignorar essa atualização - Sua %1$s (Versão da API %2$d) não é suportado. %3$s - A versão máxima da API é %d. - A versão mínima da API é %d. - Funcionalidades que estão faltando. - Esta versão é mais antiga que a instalada no seu dispositivo. + A ação falhou + Adicionar repositório + Endereço + Todos os aplicativos + Todos os aplicativos estão atualizados + Já existe + Sempre + Amoled + Características indesejadas + Aplicativo + Aplicativo não encontrado + Email do autor + Página do autor + Disponível + Rastreador de erros + Cancelar + Não é possível editar o repositório pois ele está sincronizando no momento. + Lista de mudanças + Mudanças + Checando o repositório + Compilado para depuração + Confirmação + Conectando + Contém mídia não livre + Não foi possível baixar %s + Não foi possível sincronizar %s + Não foi possível validar %s + Escuro + Data de adição + Excluir + Tem certeza que deseja excluir o repositório? + Descrição + Detalhes + Doar + Baixado %s + Baixando + Baixando %s + Editar repositório + Formato de arquivo inválido. + Fingerprint + Contém anúncio + Possui dependências não livres + Possui vulnerabilidades de segurança + Resposta de servidor inválida. + Proxy HTTP + Ignorar todas as atualizações + Ignorar essa atualização + Sua %1$s (Versão da API %2$d) não é suportado. %3$s + A versão máxima da API é %d. + A versão mínima da API é %d. + Funcionalidades que estão faltando. + Esta versão é mais antiga que a instalada no seu dispositivo. Desinstale a primeiro. - Sua %1$s plataforma não é suportada. + Sua %1$s plataforma não é suportada. Plataformas suportadas: %2$s. - Esta versão é assinada com um certificado diferente do que está + Esta versão é assinada com um certificado diferente do que está instalado no seu dispositivo. Desinstale-a primeiro. - Versão incompatível - Versões incompatíveis - Mostrar versões de aplicativos incompatíveis com o dispositivo - Incompatível com %s - Instalar - Instalado - Não foi possível verificar a integridade. - Endereço inválido - Formato de fingerprint inválido - Metadado inválido. - Permissão inválido. - Assinatura inválida. - Formato de nome do usuário inválido - Última atualização - Abrir - Licença - Licença %s - Claro - Link copiado para a área de transferência - Links - Incorporando %s - Nome - Erro da rede. - Nunca - Novas atualizações disponíveis - - %d nova atualização. - %d novas atualizações. - - Não há aplicativos disponíveis - Nenhum aplicativo instalado - Nenhuma descrição disponível. - Nenhum aplicativo correspondente encontrado - Sem proxy - Notificar sobre atualizações - Mostrar uma notificação quando atualizações estiverem disponíveis - Número de aplicativos - OK - Somente compatível com %s - Somente no Wi-Fi - Abrir %s? - Outro - Não foi possível analisar o arquivo de índice. - Senha - Falta a senha - Permissões - +%d mais - Configurações - Processando %1$s - Página do projeto - Promove serviços de rede não livres - Promove software não livre - Disponibilizado por %s - Proxy - Servidor de Proxy - Porta de Proxy - Tipo de Proxy - Repositórios - Repositório - Este repositório ainda não foi usado. Você precisa o ativar para visualizar + Versão incompatível + Versões incompatíveis + Mostrar versões de aplicativos incompatíveis com o dispositivo + Incompatível com %s + Instalar + Instalado + Não foi possível verificar a integridade. + Endereço inválido + Formato de fingerprint inválido + Metadado inválido. + Permissão inválido. + Assinatura inválida. + Formato de nome do usuário inválido + Última atualização + Abrir + Licença + Licença %s + Claro + Link copiado para a área de transferência + Links + Incorporando %s + Nome + Erro da rede. + Nunca + Novas atualizações disponíveis + + %d nova atualização. + %d novas atualizações. + + Não há aplicativos disponíveis + Nenhum aplicativo instalado + Nenhuma descrição disponível. + Nenhum aplicativo correspondente encontrado + Sem proxy + Notificar sobre atualizações + Mostrar uma notificação quando atualizações estiverem disponíveis + Número de aplicativos + OK + Somente compatível com %s + Somente no Wi-Fi + Abrir %s? + Outro + Não foi possível analisar o arquivo de índice. + Senha + Falta a senha + Permissões + +%d mais + Configurações + Processando %1$s + Página do projeto + Promove serviços de rede não livres + Promove software não livre + Disponibilizado por %s + Proxy + Servidor de Proxy + Porta de Proxy + Tipo de Proxy + Repositórios + Repositório + Este repositório ainda não foi usado. Você precisa o ativar para visualizar os aplicativos que ele fornece. - Sem assinatura. Não foi possível verificar a lista de aplicativos. Tenha cuidado ao baixar + Sem assinatura. Não foi possível verificar a lista de aplicativos. Tenha cuidado ao baixar aplicativos de repositórios sem assinatura. - Requer %s - Salvar - Salvando detalhes - Capturas de tela - Pesquisar - Selecione um mirror - Mostrar mais - Mostrar versões antigas - Assinatura %s - Assinado usando um algoritmo inseguro - Pular - SOCKS proxy - Ordenar por - Código fonte - Código fonte não está mais disponível - Sugerido - Sincronizar repositórios - Sincronizar repositórios automaticamente - Sincronizando - Sincronizando %s - Sistema - Toque para instalar. - Tema - Rastreia ou relata sua atividade - Desinstalar - Desconhecido - Error desconhecido. - Desconhecido: %s - Sem assinatura - Atualizações instáveis - Sugerir instalar versões instáveis - Não verificado - Atualização - Atualizações - O código fonte original não é livre - Nome de usuário - Falta o nome de usuário - O índice não pôde ser validado. - Versão %s - Versões - Esperando para começar a baixar - Página web - Instalação silenciosa - Permitir permissão de root para habilitar a instalação silenciosa - Temas - Créditos - themes Tipos de instalação + Requer %s + Salvar + Salvando detalhes + Capturas de tela + Pesquisar + Selecione um mirror + Mostrar mais + Mostrar versões antigas + Assinatura %s + Assinado usando um algoritmo inseguro + Pular + SOCKS proxy + Ordenar por + Código fonte + Código fonte não está mais disponível + Sugerido + Sincronizar repositórios + Sincronizar repositórios automaticamente + Sincronizando + Sincronizando %s + Sistema + Toque para instalar. + Tema + Rastreia ou relata sua atividade + Desinstalar + Desconhecido + Error desconhecido. + Desconhecido: %s + Sem assinatura + Atualizações instáveis + Sugerir instalar versões instáveis + Não verificado + Atualização + Atualizações + O código fonte original não é livre + Nome de usuário + Falta o nome de usuário + O índice não pôde ser validado. + Versão %s + Versões + Esperando para começar a baixar + Página web + Instalação silenciosa + Permitir permissão de root para habilitar a instalação silenciosa + Temas + Créditos + themes Tipos de instalação diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 6fe4e710..27f0059d 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1,150 +1,150 @@ - 操作失败 - 添加存储库 - 地址 - 全部应用 - 所有应用都是最新的 - 已经存在 - 总是 - Amoled - 不需要的特征 - 应用 - 应用未找到 - 作者邮箱 - 作者网站 - 可用 - 错误追踪器 - 取消 - 无法编辑存储库,因为它现在正在同步。 - 变更日志 - 变更 - 检查存储库 - 编译调试 - 确认 - 连接中 - 包含非自由媒体 - 无法下载 %s - 无法同步 %s - 无法验证 %s - 暗色 - 添加日期 - 删除 - 您确定要删除此存储库吗? - 描述 - 详情 - 捐助 - 已下载 %s - 下载中 - 正在下载 %s - 编辑存储库 - 文件格式无效。 - 指纹 - 包含广告 - 包含非自由依赖 - 包含安全漏洞 - 服务器响应无效。 - HTTP 代理 - 忽略所有更新 - 忽略此更新 - 您的 %1$s(API 版本 %2$d)不受支持。 %3$s - 最大 API 版本为 %d。 - 最小 API 版本为 %d。 - 缺少的功能。 - 此版本比您设备上安装的版本旧。 + 操作失败 + 添加存储库 + 地址 + 全部应用 + 所有应用都是最新的 + 已经存在 + 总是 + Amoled + 不需要的特征 + 应用 + 应用未找到 + 作者邮箱 + 作者网站 + 可用 + 错误追踪器 + 取消 + 无法编辑存储库,因为它现在正在同步。 + 变更日志 + 变更 + 检查存储库 + 编译调试 + 确认 + 连接中 + 包含非自由媒体 + 无法下载 %s + 无法同步 %s + 无法验证 %s + 暗色 + 添加日期 + 删除 + 您确定要删除此存储库吗? + 描述 + 详情 + 捐助 + 已下载 %s + 下载中 + 正在下载 %s + 编辑存储库 + 文件格式无效。 + 指纹 + 包含广告 + 包含非自由依赖 + 包含安全漏洞 + 服务器响应无效。 + HTTP 代理 + 忽略所有更新 + 忽略此更新 + 您的 %1$s(API 版本 %2$d)不受支持。 %3$s + 最大 API 版本为 %d。 + 最小 API 版本为 %d。 + 缺少的功能。 + 此版本比您设备上安装的版本旧。 请先卸载那个。 - 您的 %1$s 平台不受支持。 + 您的 %1$s 平台不受支持。 支持的平台:%2$s。 - 此版本使用不同的证书签名 + 此版本使用不同的证书签名 之前安装在您设备上的。请先卸载那个。 - 版本不兼容 - 版本不兼容 - 显示与设备不兼容的应用版本 - 与 %s 不兼容 - 安装 - 已安装 - 无法检查完整性。 - 地址无效 - 指纹格式无效 - 元数据无效。 - 权限无效。 - 签名无效。 - 用户名格式无效 - 最近更新 - 启动 - 许可证 - 许可证 %s - 亮色 - 链接已复制到剪贴板 - 链接 - 合并 %s - 名称 - 网络错误. - 从不 - 新的更新可用 - - %d 个更新. - %d 个更新. - - 没有可用的应用 - 没有已安装的应用 - 没有可用的描述。 - 未找到匹配的应用 - 无代理 - 更新通知 - 有可用更新时显示通知 - 应用数量 - 好的 - 仅与 %s 兼容 - 仅连到 Wi-Fi 时 - 打开 %s? - 其他 - 无法解析索引文件。 - 密码 - 缺少密码 - 权限 - +%d 个 - 设置 - 正在处理 %1$s - 项目网站 - 推广非自由网络服务 - 推广非自由软件 - 由 %s 提供 - 代理 - 代理主机 - 代理端口 - 代理类型 - 存储库 - 存储库 - 此存储库尚未使用。您需要启用它才能查看 + 版本不兼容 + 版本不兼容 + 显示与设备不兼容的应用版本 + 与 %s 不兼容 + 安装 + 已安装 + 无法检查完整性。 + 地址无效 + 指纹格式无效 + 元数据无效。 + 权限无效。 + 签名无效。 + 用户名格式无效 + 最近更新 + 启动 + 许可证 + 许可证 %s + 亮色 + 链接已复制到剪贴板 + 链接 + 合并 %s + 名称 + 网络错误. + 从不 + 新的更新可用 + + %d 个更新. + %d 个更新. + + 没有可用的应用 + 没有已安装的应用 + 没有可用的描述。 + 未找到匹配的应用 + 无代理 + 更新通知 + 有可用更新时显示通知 + 应用数量 + 好的 + 仅与 %s 兼容 + 仅连到 Wi-Fi 时 + 打开 %s? + 其他 + 无法解析索引文件。 + 密码 + 缺少密码 + 权限 + +%d 个 + 设置 + 正在处理 %1$s + 项目网站 + 推广非自由网络服务 + 推广非自由软件 + 由 %s 提供 + 代理 + 代理主机 + 代理端口 + 代理类型 + 存储库 + 存储库 + 此存储库尚未使用。您需要启用它才能查看 它提供的应用程序。 - 未签名。无法验证此应用列表。小心下载 + 未签名。无法验证此应用列表。小心下载 来自未签名存储库的应用。 - 需要 %s - 保存 - 保存详情 - 截图 - 搜索 - 选择一个镜像 - 显示更多 - 显示旧版本 - 签名 %s - 使用不安全算法签名 - 跳过 - SOCKS 代理 - 排序顺序 - 源代码 - 源代码不再可用 - 建议 - 同步存储库 - 自动同步存储库 - 同步中 - 同步中 %s - 系统 - 点击安装。 - 主题 - 跟踪或报告您的活动 + 需要 %s + 保存 + 保存详情 + 截图 + 搜索 + 选择一个镜像 + 显示更多 + 显示旧版本 + 签名 %s + 使用不安全算法签名 + 跳过 + SOCKS 代理 + 排序顺序 + 源代码 + 源代码不再可用 + 建议 + 同步存储库 + 自动同步存储库 + 同步中 + 同步中 %s + 系统 + 点击安装。 + 主题 + 跟踪或报告您的活动 卸载 未知 未知错误。