Compare commits

...

15 Commits

Author SHA1 Message Date
5885ea57ad Merge pull request #519 from ImranR98/dev
Add Jenkins jobs as a Source (#514), switch to Apps page after App added (#508), Bugfixes (#510)
2023-04-30 17:05:28 -04:00
f8b326529f Add Jenkins to README 2023-04-30 17:03:54 -04:00
9f5f1174ba Increment version 2023-04-30 17:02:33 -04:00
779de58f74 Jenkins uses release dates only + APK delete bugfix 2023-04-30 17:00:00 -04:00
76e316422c Added Jenkins Source (#514) 2023-04-30 16:28:09 -04:00
36273fe02d Switch to apps tab after app added (#508) 2023-04-30 15:49:25 -04:00
03b592521c Fixed link sorting for HTML Source 2023-04-30 15:39:21 -04:00
a5ef47a060 Merge pull request #516 from gidano/main
Update hu.json
2023-04-30 14:40:26 -04:00
289c801fec Merge pull request #511 from ThePhoDit/es-translations
Spanish translations
2023-04-30 14:40:17 -04:00
73d04b1564 Update hu.json 2023-04-30 16:09:15 +02:00
9469d56144 Spanish translations 2023-04-30 11:37:07 +02:00
d063bca474 Merge pull request #506 from ImranR98/dev
Switched to synchronous install plugin (#99, #459)
2023-04-30 02:59:49 -04:00
7c592756fe Smarter APK caching (#459) 2023-04-30 02:47:53 -04:00
08586870fb Tweak use of attemptToCorrectInstallStatus 2023-04-30 02:28:14 -04:00
8b123acdcd Switched to synchronous install plugin 2023-04-30 02:23:53 -04:00
16 changed files with 576 additions and 138 deletions

View File

@ -17,6 +17,7 @@ Currently supported App sources:
- [SourceForge](https://sourceforge.net/)
- [APKMirror](https://apkmirror.com/) (Track-Only)
- Third Party F-Droid Repos
- Jenkins Jobs
- [Steam](https://store.steampowered.com/mobile)
- [Telegram App](https://telegram.org)
- [VLC](https://www.videolan.org/vlc/download-android.html)
@ -34,7 +35,6 @@ Currently supported App sources:
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
## Limitations
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.

View File

@ -25,6 +25,11 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -46,9 +51,18 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="dev.imranr.obtainium"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>

View File

@ -2,4 +2,5 @@
<paths>
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
<external-path path="." name="external_storage_root" />
<external-path name="external_files" path="."/>
</paths>

279
assets/translations/es.json Normal file
View File

@ -0,0 +1,279 @@
{
"invalidURLForSource": "URL de la aplicación {} no válida",
"noReleaseFound": "No se ha podido encontrar una versión válida",
"noVersionFound": "No se ha podido determinar la versión de la publicación",
"urlMatchesNoSource": "La URL no coincide con ninguna fuente conocida",
"cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación",
"appIdMismatch": "La ID del paquete descargado no coincide con la ID de la aplicación instalada",
"functionNotImplemented": "Esta clase no ha implementado esta función",
"placeholder": "Espacio reservado",
"someErrors": "Han ocurrido algunos errores",
"unexpectedError": "Error Inesperado",
"ok": "Correcto",
"and": "y",
"startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano",
"bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es {}",
"startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano",
"bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano",
"firstRun": "Esta es la primera ejecución de Obtainium",
"settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}",
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
"githubPATFormat": "nombre_de_usuario:token",
"githubPATLinkText": "Sobre los TAP de GitHub",
"includePrereleases": "Incluir versiones preliminares",
"fallbackToOlderReleases": "Retorceder a versiones previas",
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
"invalidRegEx": "Expresión regular inválida",
"noDescription": "Sin descripción",
"cancel": "Cancelar",
"continue": "Continuar",
"requiredInBrackets": "(Requerido)",
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
"colour": "Color",
"githubStarredRepos": "Repositorios favoritos de GitHub",
"uname": "Nombre de usuario",
"wrongArgNum": "Número de argumentos provistos inválido",
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
"source": "Origen",
"app": "Aplicación",
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
"cancelled": "Cancelado",
"appAlreadyAdded": "Aplicación ya añadida",
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
"addApp": "Añadir Aplicación",
"appSourceURL": "URL de Origen de la Aplicación",
"error": "Error",
"add": "Añadir",
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
"search": "Buscar",
"additionalOptsFor": "Opciones Adicionales para {}",
"supportedSourcesBelow": "Fuentes Soportadas:",
"trackOnlyInBrackets": "(Solo Seguimiento)",
"searchableInBrackets": "(Soporta Búsquedas)",
"appsString": "Aplicaciones",
"noApps": "Sin Aplicaciones",
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
"byX": "Por {}",
"percentProgress": "Progreso: {}%",
"pleaseWait": "Por favor, espere",
"updateAvailable": "Actualización Disponible",
"estimateInBracketsShort": "(Aprox.)",
"notInstalled": "No Instalado",
"estimateInBrackets": "(Aproximado)",
"selectAll": "Seleccionar Todo",
"deselectN": "Deseleccionar {}",
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
"updateX": "Actualizar {}",
"installX": "Instalar {}",
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
"changeX": "Cambiar {}",
"installUpdateApps": "Instalar/Actualizar Aplicaciones",
"installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
"markXSelectedAppsAsUpdated": "¿Marcar {} Aplicaciones Seleccionadas como Actualizadas?",
"no": "No",
"yes": "Sí",
"markSelectedAppsUpdated": "Marcar Aplicaciones Seleccionadas como Actualizadas",
"pinToTop": "Fijar arriba",
"unpinFromTop": "Desfijar de arriba",
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
"resetInstallStatus": "Restaurar Estado de Instalación",
"more": "Más",
"removeOutdatedFilter": "Elimiar Filtro de Aplicaciones Desactualizado",
"showOutdatedOnly": "Mostrar solo Aplicaciones Desactualizadas",
"filter": "Filtrar",
"filterActive": "Filtrar *",
"filterApps": "Filtrar Actualizaciones",
"appName": "Nombre de la Aplicación",
"author": "Autor",
"upToDateApps": "Aplicaciones Actualizadas",
"nonInstalledApps": "Aplicaciones No Instaladas",
"importExport": "Importar/Exportar",
"settings": "Ajustes",
"exportedTo": "Exportado a {}",
"obtainiumExport": "Exportar Obtainium",
"invalidInput": "Input incorrecto",
"importedX": "Importado {}",
"obtainiumImport": "Importar Obtainium",
"importFromURLList": "Importar desde lista de URLs",
"searchQuery": "Consulta de Búsqueda",
"appURLList": "Lista de URLs de Aplicaciones",
"line": "Línea",
"searchX": "Buscar {}",
"noResults": "Resultados no encontrados",
"importX": "Importar {}",
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
"importErrors": "Import Errors",
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
"okay": "Correcto",
"selectURL": "Seleccionar URL",
"selectURLs": "Seleccionar URLs",
"pick": "Escoger",
"theme": "Tema",
"dark": "Oscuro",
"light": "Claro",
"followSystem": "Seguir al Sistema",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Usar tema oscuro con negros puros",
"appSortBy": "Ordenar Aplicaciones Por",
"authorName": "Autor/Nombre",
"nameAuthor": "Nombre/Autor",
"asAdded": "Según se Añadieron",
"appSortOrder": "Orden de Clasificación de Aplicaciones",
"ascending": "Ascendente",
"descending": "Descendente",
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
"neverManualOnly": "Nunca - Solo Manual",
"appearance": "Apariencia",
"showWebInAppView": "Mostrar Vista de la Web de Origen",
"pinUpdates": "Fijar Actualizaciones en la Parte Superior de la Vista de Aplicaciones",
"updates": "Actualizaciones",
"sourceSpecific": "Fuente Específica",
"appSource": "Fuente de la Aplicación",
"noLogs": "Sin Logs",
"appLogs": "Logs de la Aplicación",
"close": "Cerrar",
"share": "Compartir",
"appNotFound": "Aplicación no encontrada",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Elige una APK",
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
"warning": "Aviso",
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
"updatesAvailable": "Actualizaciones Disponibles",
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitorizadas por Obtainium",
"noNewUpdates": "No hay nuevas actualizaciones.",
"xHasAnUpdate": "{} tiene una actualización.",
"appsUpdated": "Aplicaciones Actualizadas",
"appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano",
"xWasUpdatedToY": "{} ha sido actualizada a {}.",
"errorCheckingUpdates": "Error Buscando Actualizaciones",
"errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla",
"appsRemoved": "Aplicaciones Eliminadas",
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
"completeAppInstallation": "Instalación Completa de la Aplicación",
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
"checkingForUpdates": "Buscando Actualizaciones",
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
"trackOnly": "Solo Seguimiento",
"errorWithHttpStatusCode": "Error {}",
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
"unknown": "Desconocido",
"none": "Ninguno",
"never": "Nunca",
"latestVersionX": "Última Versión: {}",
"installedVersionX": "Versión Instalada: {}",
"lastUpdateCheckX": "Última Comprobación: {}",
"remove": "Eliminar",
"yesMarkUpdated": "Sí, Marcar como Actualizada",
"fdroid": "Repositorio oficial de F-Droid",
"appIdOrName": "ID o Nombre de la Aplicación",
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
"install": "Instalar",
"markInstalled": "Marcar como Instalda",
"update": "Actualizar",
"markUpdated": "Marcar como Actualizada",
"additionalOptions": "Opciones Adicionales",
"disableVersionDetection": "Descativar Detección de Versiones",
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
"downloadingX": "Descargando {}",
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
"noAPKFound": "APK no encontrada",
"noVersionDetection": "Sin detección de versiones",
"categorize": "Catogorizar",
"categories": "Categorías",
"category": "Categoría",
"noCategory": "Sin Categoría",
"noCategories": "Sin Categorías",
"deleteCategoriesQuestion": "¿Borrar Categorías?",
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
"addCategory": "Añadir Categoría",
"label": "Nombre",
"language": "Idioma",
"copiedToClipboard": "Copiado al Portapapeles",
"storagePermissionDenied": "Permiso de Almacenamiento rechazado",
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
"removeFromObtainium": "Eliminar de Obtainium",
"uninstallFromDevice": "Desinstalar del Dispositivo",
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
"releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
"changes": "Cambios",
"releaseDate": "Fecha de Publicación",
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
"versionDetection": "Detección de Versiones",
"standardVersionDetection": "Detección de versiones estándar",
"groupByCategory": "Agrupar por Categoría",
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
"overrideSource": "Sobrescribir Fuente",
"dontShowAgain": "No mostrar de nuevo",
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"
},
"tooManyRequestsTryAgainInMinutes": {
"one": "Muchas peticiones (limitado) - prueba de nuevo en {} minuto",
"other": "Muchas peticiones (limitado) - prueba de nuevo en {} minutos"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minuto",
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minutos"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización - se notificará al usuario si es necesario",
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones - se notificará al usuario si es necesario"
},
"apps": {
"one": "{} Aplicación",
"other": "{} Aplicaciones"
},
"url": {
"one": "{} URL",
"other": "{} URLs"
},
"minute": {
"one": "{} Minuto",
"other": "{} Minutos"
},
"hour": {
"one": "{} Hora",
"other": "{} Horas"
},
"day": {
"one": "{} Día",
"other": "{} Días"
},
"clearedNLogsBeforeXAfterY": {
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} y 1 aplicación más tiene actualizaciones.",
"other": "{} y {} aplicaciones más tiene actualizaciones."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} y 1 aplicación más han sido actualizadas.",
"other": "{} y {} aplicaciones más han sido actualizadas."
}
}

View File

@ -223,10 +223,10 @@
"standardVersionDetection": "Alapért. verzió érzékelés",
"groupByCategory": "Csoportosítás Kategória alapján",
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"overrideSource": "Forrás felülbírálása",
"dontShowAgain": "Ne mutassa ezt újra",
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -10,6 +10,66 @@ class HTML extends AppSource {
return url;
}
int compareAlphaNumeric(String a, String b) {
List<String> aParts = _splitAlphaNumeric(a);
List<String> bParts = _splitAlphaNumeric(b);
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
String aPart = aParts[i];
String bPart = bParts[i];
bool aIsNumber = _isNumeric(aPart);
bool bIsNumber = _isNumeric(bPart);
if (aIsNumber && bIsNumber) {
int aNumber = int.parse(aPart);
int bNumber = int.parse(bPart);
int cmp = aNumber.compareTo(bNumber);
if (cmp != 0) {
return cmp;
}
} else if (!aIsNumber && !bIsNumber) {
int cmp = aPart.compareTo(bPart);
if (cmp != 0) {
return cmp;
}
} else {
// Alphanumeric strings come before numeric strings
return aIsNumber ? 1 : -1;
}
}
return aParts.length.compareTo(bParts.length);
}
List<String> _splitAlphaNumeric(String s) {
List<String> parts = [];
StringBuffer sb = StringBuffer();
bool isNumeric = _isNumeric(s[0]);
sb.write(s[0]);
for (int i = 1; i < s.length; i++) {
bool currentIsNumeric = _isNumeric(s[i]);
if (currentIsNumeric == isNumeric) {
sb.write(s[i]);
} else {
parts.add(sb.toString());
sb.clear();
sb.write(s[i]);
isNumeric = currentIsNumeric;
}
}
parts.add(sb.toString());
return parts;
}
bool _isNumeric(String s) {
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
@ -23,7 +83,8 @@ class HTML extends AppSource {
.map((element) => element.attributes['href'] ?? '')
.where((element) => element.toLowerCase().endsWith('.apk'))
.toList();
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
links.sort(
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
links = links.where((element) => reg.hasMatch(element)).toList();

View File

@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class Jenkins extends AppSource {
Jenkins() {
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
}
@override
String trimJobUrl(String url) {
RegExp standardUrlRegEx = RegExp('.*/job/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/-/releases';
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
standardUrl = trimJobUrl(standardUrl);
Response res =
await get(Uri.parse('$standardUrl/lastSuccessfulBuild/api/json'));
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
var releaseDate = json['timestamp'] == null
? null
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
var version =
json['number'] == null ? null : (json['number'] as int).toString();
if (version == null) {
throw NoVersionError();
}
var apkUrls = (json['artifacts'] as List<dynamic>)
.map((e) {
var path = (e['relativePath'] as String?);
if (path != null && path.isNotEmpty) {
path = '$standardUrl/lastSuccessfulBuild/artifact/$path';
}
return path == null
? const MapEntry<String, String>('', '')
: MapEntry<String, String>(
(e['fileName'] ?? e['relativePath']) as String, path);
})
.where((url) =>
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
.toList();
if (apkUrls.isEmpty) {
throw NoAPKError();
}
return APKDetails(
version,
apkUrls,
releaseDate: releaseDate,
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:android_package_installer/android_package_installer.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:obtainium/providers/logs_provider.dart';
@ -44,6 +45,11 @@ class DowngradeError extends ObtainiumError {
DowngradeError() : super(tr('cantInstallOlderVersion'));
}
class InstallError extends ObtainiumError {
InstallError(int code)
: super(PackageInstallerStatus.byCode(code).name.substring(7));
}
class IDChangedError extends ObtainiumError {
IDChangedError() : super(tr('appIdMismatch'));
}

View File

@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.12.0';
const String currentVersion = '0.12.2';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@ -166,7 +166,9 @@ class _AddAppPageState extends State<AddAppPage> {
if (appsProvider.apps.containsKey(app.id)) {
throw ObtainiumError(tr('appAlreadyAdded'));
}
if (app.additionalSettings['trackOnly'] == true) {
if (app.additionalSettings['trackOnly'] == true ||
app.additionalSettings['versionDetection'] !=
'standardVersionDetection') {
app.installedVersion = app.latestVersion;
}
app.categories = pickedCategories;

View File

@ -6,6 +6,8 @@ import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/import_export.dart';
import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:provider/provider.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@ -24,6 +26,7 @@ class NavigationPageItem {
class _HomePageState extends State<HomePage> {
List<int> selectedIndexHistory = [];
int prevAppCount = -1;
List<NavigationPageItem> pages = [
NavigationPageItem(tr('appsString'), Icons.apps,
@ -36,6 +39,39 @@ class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.watch<AppsProvider>();
switchToPage(int index) async {
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
if (prevAppCount >= 0 &&
appsProvider.apps.length > prevAppCount &&
selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last == 1) {
switchToPage(0);
}
prevAppCount = appsProvider.apps.length;
return WillPopScope(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@ -65,27 +101,7 @@ class _HomePageState extends State<HomePage> {
.toList(),
onDestinationSelected: (int index) async {
HapticFeedback.selectionClick();
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
.currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
await switchToPage(index);
},
selectedIndex:
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,

View File

@ -6,11 +6,11 @@ import 'dart:convert';
import 'dart:io';
import 'package:android_intent_plus/flag.dart';
import 'package:android_package_installer/android_package_installer.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
import 'package:obtainium/components/generated_form.dart';
@ -113,21 +113,20 @@ class AppsProvider with ChangeNotifier {
() async {
// Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps();
// Delete existing APKs
(await getExternalStorageDirectory())
?.listSync()
.where((element) =>
element.path.endsWith('.apk') ||
element.path.endsWith('.apk.part'))
.forEach((apk) {
apk.delete();
// Delete any partial APKs
(await getExternalCacheDirectories())
?.first
.listSync()
.where((element) => element.path.endsWith('.apk.part'))
.forEach((partialApk) {
partialApk.delete();
});
}();
}
downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async {
var destDir = (await getExternalStorageDirectory())!.path;
var destDir = (await getExternalCacheDirectories())!.first.path;
StreamedResponse response =
await Client().send(Request('GET', Uri.parse(url)));
File downloadedFile = File('$destDir/$fileName');
@ -191,15 +190,6 @@ class AppsProvider with ChangeNotifier {
}
prevProg = prog;
});
// Delete older versions of the APK if any
for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != fileName) {
file.delete();
}
}
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
// The former case should be handled (give the App its real ID), the latter is a security issue
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
@ -217,6 +207,15 @@ class AppsProvider with ChangeNotifier {
await saveApps([app], onlyIfExists: !isTempId);
}
}
// Delete older versions of the APK if any
for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != downloadedFile.path.split('/').last) {
file.delete();
}
}
return DownloadedApk(app.id, downloadedFile);
} finally {
notificationsProvider?.cancel(notifId);
@ -268,7 +267,8 @@ class AppsProvider with ChangeNotifier {
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
// But even then, we don't know if it actually succeeded
Future<void> installApk(DownloadedApk file) async {
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
// TODO: Use 'silent' when/if ever possible
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
AppInfo? appInfo;
try {
@ -281,16 +281,16 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
await InstallPlugin.installApk(file.file.path, obtainiumId);
if (file.appId == obtainiumId) {
// Obtainium prompt should be lowest
await Future.delayed(const Duration(milliseconds: 500));
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
} else if (code == 0) {
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
file.file.delete();
}
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
// Don't correct install status as installation may not be done yet
await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false);
await saveApps([apps[file.appId]!.app]);
}
void uninstallApp(String appId) async {
@ -395,76 +395,44 @@ class AppsProvider with ChangeNotifier {
a.installedVersion = a.latestVersion;
return a;
}).toList());
// Download APKs for all Apps to be installed
// Prepare to download+install Apps
MultiAppMultiError errors = MultiAppMultiError();
List<DownloadedApk?> downloadedFiles =
await Future.wait(appsToInstall.map((id) async {
List<String> installedIds = [];
// Move Obtainium to the end of the line (let all other apps update first)
String? temp;
appsToInstall.removeWhere((element) {
bool res = element == obtainiumId || element == obtainiumTempId;
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
appsToInstall = [...appsToInstall, temp!];
}
for (var id in appsToInstall) {
try {
return await downloadApp(apps[id]!.app, context);
// ignore: use_build_context_synchronously
var downloadedFile = await downloadApp(apps[id]!.app, context);
bool willBeSilent =
await canInstallSilently(apps[downloadedFile.appId]!.app);
willBeSilent = false; // TODO: Remove this when silent updates work
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) {
throw ObtainiumError(tr('cancelled'));
}
if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously
await waitForUserToReturnToForeground(context);
}
await installApk(downloadedFile, silent: willBeSilent);
installedIds.add(id);
} catch (e) {
errors.add(id, e.toString());
}
return null;
}));
downloadedFiles =
downloadedFiles.where((element) => element != null).toList();
// Separate the Apps to install into silent and regular lists
List<DownloadedApk> silentUpdates = [];
List<DownloadedApk> regularInstalls = [];
for (var f in downloadedFiles) {
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
if (willBeSilent) {
silentUpdates.add(f);
} else {
regularInstalls.add(f);
}
}
// Move everything to the regular install list (since silent updates don't currently work)
// TODO: Remove this when silent updates work
regularInstalls.addAll(silentUpdates);
// If Obtainium is being installed, it should be the last one
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
DownloadedApk? temp;
items.removeWhere((element) {
bool res =
element.appId == obtainiumId || element.appId == obtainiumTempId;
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
items = [temp!, ...items];
}
return items;
}
silentUpdates = moveObtainiumToStart(silentUpdates);
regularInstalls = moveObtainiumToStart(regularInstalls);
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) {
throw ObtainiumError(tr('cancelled'));
}
// // Install silent updates (uncomment when it works - TODO)
// for (var u in silentUpdates) {
// await installApk(u, silent: true); // Would need to add silent option
// }
// Do regular installs
if (regularInstalls.isNotEmpty && context != null) {
// ignore: use_build_context_synchronously
await waitForUserToReturnToForeground(context);
for (var i in regularInstalls) {
try {
await installApk(i);
} catch (e) {
errors.add(i.appId, e.toString());
}
}
}
if (errors.content.isNotEmpty) {
@ -473,7 +441,7 @@ class AppsProvider with ChangeNotifier {
NotificationsProvider().cancel(UpdateNotification([]).id);
return downloadedFiles.map((e) => e!.appId).toList();
return installedIds;
}
Future<Directory> getAppsDir() async {
@ -762,7 +730,7 @@ class AppsProvider with ChangeNotifier {
apps[i].installedVersion = null;
}
}
await saveApps(apps, attemptToCorrectInstallStatus: false);
await saveApps(apps, attemptToCorrectInstallStatus: !remove);
}
if (remove) {
await removeApps(apps.map((e) => e.id).toList());

View File

@ -206,8 +206,7 @@ class SettingsProvider with ChangeNotifier {
.map((e) => e as App)
.toList();
if (changedApps.isNotEmpty) {
appsProvider.saveApps(changedApps,
attemptToCorrectInstallStatus: false);
appsProvider.saveApps(changedApps);
}
}
prefs?.setString('categories', jsonEncode(cats));

View File

@ -15,6 +15,7 @@ import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/app_sources/izzyondroid.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/app_sources/jenkins.dart';
import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/neutroncode.dart';
import 'package:obtainium/app_sources/signal.dart';
@ -319,6 +320,22 @@ abstract class AppSource {
name = runtimeType.toString();
}
overrideVersionDetectionFormDefault(String vd, bool disableStandard) {
additionalAppSpecificSourceAgnosticSettingFormItems =
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
return e.map((e2) {
if (e2.key == 'versionDetection') {
var item = e2 as GeneratedFormDropdown;
item.defaultValue = vd;
if (disableStandard) {
item.disabledOptKeys = ['standardVersionDetection'];
}
}
return e2;
}).toList();
}).toList();
}
String standardizeUrl(String url) {
url = preStandardizeUrl(url);
if (!hostChanged) {
@ -341,7 +358,7 @@ abstract class AppSource {
[];
// Some additional data may be needed for Apps regardless of Source
final List<List<GeneratedFormItem>>
List<List<GeneratedFormItem>>
additionalAppSpecificSourceAgnosticSettingFormItems = [
[
GeneratedFormSwitch(
@ -440,6 +457,7 @@ class SourceProvider {
FDroid(),
IzzyOnDroid(),
FDroidRepo(),
Jenkins(),
SourceForge(),
APKMirror(),
Mullvad(),

View File

@ -17,6 +17,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.9"
android_package_installer:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1
url: "https://github.com/ImranR98/android_package_installer"
source: git
version: "0.0.1"
animations:
dependency: "direct main"
description:
@ -293,14 +302,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
install_plugin_v2:
dependency: "direct main"
description:
name: install_plugin_v2
sha256: d6b014637e7a53839e9c5a254f9fd9bb8866392c6db1f16184ce17818cc2d979
url: "https://pub.dev"
source: hosted
version: "1.0.0"
installed_apps:
dependency: "direct main"
description:

View File

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.12.0+160 # When changing this, update the tag in main() accordingly
version: 0.12.2+162 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'
@ -51,7 +51,10 @@ dependencies:
device_info_plus: ^8.0.0
file_picker: ^5.2.10
animations: ^2.0.4
install_plugin_v2: ^1.0.0
android_package_installer:
git:
url: https://github.com/ImranR98/android_package_installer
ref: main
share_plus: ^6.0.1
installed_apps: ^1.3.1
package_archive_info: ^0.1.0