import 'dart:convert'; import 'dart:io'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:injectable/injectable.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timeago/timeago.dart'; @lazySingleton class ManagerAPI { final RevancedAPI _revancedAPI = locator(); final GithubAPI _githubAPI = locator(); final RootAPI _rootAPI = RootAPI(); final String patcherRepo = 'revanced-patcher'; final String cliRepo = 'revanced-cli'; late SharedPreferences _prefs; bool isRooted = false; String storedPatchesFile = '/selected-patches.json'; String keystoreFile = '/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore'; String defaultKeystorePassword = 's3cur3p@ssw0rd'; String defaultApiUrl = 'https://api.revanced.app/'; String defaultRepoUrl = 'https://api.github.com'; String defaultPatcherRepo = 'revanced/revanced-patcher'; String defaultPatchesRepo = 'revanced/revanced-patches'; String defaultIntegrationsRepo = 'revanced/revanced-integrations'; String defaultCliRepo = 'revanced/revanced-cli'; String defaultManagerRepo = 'revanced/revanced-manager'; String? patchesVersion = ''; String? integrationsVersion = ''; bool isDefaultPatchesRepo() { return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches'; } bool isDefaultIntegrationsRepo() { return getIntegrationsRepo().toLowerCase() == 'revanced/revanced-integrations'; } Future initialize() async { _prefs = await SharedPreferences.getInstance(); isRooted = await _rootAPI.isRooted(); storedPatchesFile = (await getApplicationDocumentsDirectory()).path + storedPatchesFile; } String getApiUrl() { return _prefs.getString('apiUrl') ?? defaultApiUrl; } Future setApiUrl(String url) async { if (url.isEmpty || url == ' ') { url = defaultApiUrl; } await _revancedAPI.initialize(url); await _revancedAPI.clearAllCache(); await _prefs.setString('apiUrl', url); } String getRepoUrl() { return _prefs.getString('repoUrl') ?? defaultRepoUrl; } Future setRepoUrl(String url) async { if (url.isEmpty || url == ' ') { url = defaultRepoUrl; } await _prefs.setString('repoUrl', url); } String getPatchesDownloadURL(bool bundle) { return _prefs.getString('patchesDownloadURL-$bundle') ?? ''; } Future setPatchesDownloadURL(String value, bool bundle) async { await _prefs.setString('patchesDownloadURL-$bundle', value); } String getPatchesRepo() { return _prefs.getString('patchesRepo') ?? defaultPatchesRepo; } Future setPatchesRepo(String value) async { if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) { value = defaultPatchesRepo; } await _prefs.setString('patchesRepo', value); } bool getPatchesConsent() { return _prefs.getBool('patchesConsent') ?? false; } Future setPatchesConsent(bool consent) async { await _prefs.setBool('patchesConsent', consent); } bool isPatchesAutoUpdate() { return _prefs.getBool('patchesAutoUpdate') ?? false; } bool isPatchesChangeEnabled() { return _prefs.getBool('patchesChangeEnabled') ?? false; } void setPatchesChangeEnabled(bool value) { _prefs.setBool('patchesChangeEnabled', value); } bool showPatchesChangeWarning() { return _prefs.getBool('showPatchesChangeWarning') ?? true; } void setPatchesChangeWarning(bool value) { _prefs.setBool('showPatchesChangeWarning', !value); } bool isChangingToggleModified() { return _prefs.getBool('isChangingToggleModified') ?? false; } void setChangingToggleModified(bool value) { _prefs.setBool('isChangingToggleModified', value); } Future setPatchesAutoUpdate(bool value) async { await _prefs.setBool('patchesAutoUpdate', value); } List getSavedPatches(String packageName) { final List patchesJson = _prefs.getStringList('savedPatches-$packageName') ?? []; final List patches = patchesJson.map((String patchJson) { return Patch.fromJson(jsonDecode(patchJson)); }).toList(); return patches; } Future savePatches(List patches, String packageName) async { final List patchesJson = patches.map((Patch patch) { return jsonEncode(patch.toJson()); }).toList(); await _prefs.setStringList('savedPatches-$packageName', patchesJson); } String getIntegrationsDownloadURL() { return _prefs.getString('integrationsDownloadURL') ?? ''; } Future setIntegrationsDownloadURL(String value) async { await _prefs.setString('integrationsDownloadURL', value); } List getUsedPatches(String packageName) { final List patchesJson = _prefs.getStringList('usedPatches-$packageName') ?? []; final List patches = patchesJson.map((String patchJson) { return Patch.fromJson(jsonDecode(patchJson)); }).toList(); return patches; } Future setUsedPatches(List patches, String packageName) async { final List patchesJson = patches.map((Patch patch) { return jsonEncode(patch.toJson()); }).toList(); await _prefs.setStringList('usedPatches-$packageName', patchesJson); } String getIntegrationsRepo() { return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo; } Future setIntegrationsRepo(String value) async { if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) { value = defaultIntegrationsRepo; } await _prefs.setString('integrationsRepo', value); } bool getUseDynamicTheme() { return _prefs.getBool('useDynamicTheme') ?? false; } Future setUseDynamicTheme(bool value) async { await _prefs.setBool('useDynamicTheme', value); } bool getUseDarkTheme() { return _prefs.getBool('useDarkTheme') ?? false; } Future setUseDarkTheme(bool value) async { await _prefs.setBool('useDarkTheme', value); } bool areUniversalPatchesEnabled() { return _prefs.getBool('universalPatchesEnabled') ?? false; } Future enableUniversalPatchesStatus(bool value) async { await _prefs.setBool('universalPatchesEnabled', value); } bool areExperimentalPatchesEnabled() { return _prefs.getBool('experimentalPatchesEnabled') ?? false; } Future enableExperimentalPatchesStatus(bool value) async { await _prefs.setBool('experimentalPatchesEnabled', value); } Future setKeystorePassword(String password) async { await _prefs.setString('keystorePassword', password); } String getKeystorePassword() { return _prefs.getString('keystorePassword') ?? defaultKeystorePassword; } Future deleteTempFolder() async { final Directory dir = Directory('/data/local/tmp/revanced-manager'); if (await dir.exists()) { await dir.delete(recursive: true); } } Future deleteKeystore() async { final File keystore = File( keystoreFile, ); if (await keystore.exists()) { await keystore.delete(); } } List getPatchedApps() { final List apps = _prefs.getStringList('patchedApps') ?? []; return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); } Future setPatchedApps( List patchedApps, ) async { if (patchedApps.length > 1) { patchedApps.sort((a, b) => a.name.compareTo(b.name)); } await _prefs.setStringList( 'patchedApps', patchedApps.map((a) => json.encode(a.toJson())).toList(), ); } Future savePatchedApp(PatchedApplication app) async { final List patchedApps = getPatchedApps(); patchedApps.removeWhere((a) => a.packageName == app.packageName); final ApplicationWithIcon? installed = await DeviceApps.getApp( app.packageName, true, ) as ApplicationWithIcon?; if (installed != null) { app.name = installed.appName; app.version = installed.versionName!; app.icon = installed.icon; } patchedApps.add(app); await setPatchedApps(patchedApps); } Future deletePatchedApp(PatchedApplication app) async { final List patchedApps = getPatchedApps(); patchedApps.removeWhere((a) => a.packageName == app.packageName); await setPatchedApps(patchedApps); } Future clearAllData() async { try { _revancedAPI.clearAllCache(); _githubAPI.clearAllCache(); } on Exception catch (e) { if (kDebugMode) { print(e); } } } Future>> getContributors() async { return await _revancedAPI.getContributors(); } Future> getPatches() async { try { final String repoName = getPatchesRepo(); final String currentVersion = await getCurrentPatchesVersion(); final String url = getPatchesDownloadURL(false); return await _githubAPI.getPatches( repoName, currentVersion, url, ); } on Exception catch (e) { if (kDebugMode) { print(e); } return []; } } Future downloadPatches() async { try { final String repoName = getPatchesRepo(); final String currentVersion = await getCurrentPatchesVersion(); final String url = getPatchesDownloadURL(true); return await _githubAPI.getPatchesReleaseFile( '.jar', repoName, currentVersion, url, ); } on Exception catch (e) { if (kDebugMode) { print(e); } return null; } } Future downloadIntegrations() async { try { final String repoName = getIntegrationsRepo(); final String currentVersion = await getCurrentIntegrationsVersion(); final String url = getIntegrationsDownloadURL(); return await _githubAPI.getPatchesReleaseFile( '.apk', repoName, currentVersion, url, ); } on Exception catch (e) { if (kDebugMode) { print(e); } return null; } } Future downloadManager() async { return await _revancedAPI.getLatestReleaseFile( '.apk', defaultManagerRepo, ); } Future getLatestPatchesReleaseTime() async { if (isDefaultPatchesRepo()) { return await _revancedAPI.getLatestReleaseTime( '.json', defaultPatchesRepo, ); } else { final release = await _githubAPI.getLatestPatchesRelease(getPatchesRepo()); if (release != null) { final DateTime timestamp = DateTime.parse(release['created_at'] as String); return format(timestamp, locale: 'en_short'); } else { return null; } } } Future getLatestManagerReleaseTime() async { return await _revancedAPI.getLatestReleaseTime( '.apk', defaultManagerRepo, ); } Future getLatestManagerVersion() async { return await _revancedAPI.getLatestReleaseVersion( '.apk', defaultManagerRepo, ); } Future getLatestIntegrationsVersion() async { if (isDefaultIntegrationsRepo()) { return await _revancedAPI.getLatestReleaseVersion( '.apk', defaultIntegrationsRepo, ); } else { final release = await _githubAPI.getLatestRelease(getIntegrationsRepo()); if (release != null) { return release['tag_name']; } else { return null; } } } Future getLatestPatchesVersion() async { if (isDefaultPatchesRepo()) { return await _revancedAPI.getLatestReleaseVersion( '.json', defaultPatchesRepo, ); } else { final release = await _githubAPI.getLatestPatchesRelease(getPatchesRepo()); if (release != null) { return release['tag_name']; } else { return null; } } } Future getCurrentManagerVersion() async { final PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; } Future getCurrentPatchesVersion() async { patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0'; if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) { final String newPatchesVersion = await getLatestPatchesVersion() ?? '0.0.0'; if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') { await setCurrentPatchesVersion(newPatchesVersion); } } return patchesVersion!; } Future setCurrentPatchesVersion(String version) async { await _prefs.setString('patchesVersion', version); await setPatchesDownloadURL('', false); await setPatchesDownloadURL('', true); await downloadPatches(); } Future getCurrentIntegrationsVersion() async { integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0'; if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) { final String newIntegrationsVersion = await getLatestIntegrationsVersion() ?? '0.0.0'; if (integrationsVersion != newIntegrationsVersion && newIntegrationsVersion != '0.0.0') { await setCurrentIntegrationsVersion(newIntegrationsVersion); } } return integrationsVersion!; } Future setCurrentIntegrationsVersion(String version) async { await _prefs.setString('integrationsVersion', version); await setIntegrationsDownloadURL(''); await downloadIntegrations(); } Future> getAppsToRemove( List patchedApps, ) async { final List toRemove = []; for (final PatchedApplication app in patchedApps) { final bool isRemove = await isAppUninstalled(app); if (isRemove) { toRemove.add(app); } } return toRemove; } Future> getUnsavedApps( List patchedApps, ) async { final List unsavedApps = []; final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); if (hasRootPermissions) { final List installedApps = await _rootAPI.getInstalledApps(); for (final String packageName in installedApps) { if (!patchedApps.any((app) => app.packageName == packageName)) { final ApplicationWithIcon? application = await DeviceApps.getApp( packageName, true, ) as ApplicationWithIcon?; if (application != null) { unsavedApps.add( PatchedApplication( name: application.appName, packageName: application.packageName, originalPackageName: application.packageName, version: application.versionName!, apkFilePath: application.apkFilePath, icon: application.icon, patchDate: DateTime.now(), isRooted: true, ), ); } } } } final List userApps = await DeviceApps.getInstalledApplications(); for (final Application app in userApps) { if (app.packageName.startsWith('app.revanced') && !app.packageName.startsWith('app.revanced.manager.') && !patchedApps.any((uapp) => uapp.packageName == app.packageName)) { final ApplicationWithIcon? application = await DeviceApps.getApp( app.packageName, true, ) as ApplicationWithIcon?; if (application != null) { unsavedApps.add( PatchedApplication( name: application.appName, packageName: application.packageName, originalPackageName: application.packageName, version: application.versionName!, apkFilePath: application.apkFilePath, icon: application.icon, patchDate: DateTime.now(), ), ); } } } return unsavedApps; } Future showPatchesChangeWarningDialog(BuildContext context) { final ValueNotifier noShow = ValueNotifier(!showPatchesChangeWarning()); return showDialog( barrierDismissible: false, context: context, builder: (context) => WillPopScope( onWillPop: () async => false, child: AlertDialog( backgroundColor: Theme.of(context).colorScheme.secondaryContainer, title: I18nText('warning'), content: ValueListenableBuilder( valueListenable: noShow, builder: (context, value, child) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ I18nText( 'patchItem.patchesChangeWarningDialogText', child: const Text( '', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 8), CheckboxListTile( value: value, contentPadding: EdgeInsets.zero, title: I18nText( 'noShowAgain', ), onChanged: (selected) { noShow.value = selected!; }, ), ], ); }, ), actions: [ CustomMaterialButton( label: I18nText('okButton'), onPressed: () { setPatchesChangeWarning(noShow.value); Navigator.of(context).pop(); }, ), ], ), ), ); } Future reAssessSavedApps() async { final List patchedApps = getPatchedApps(); final List unsavedApps = await getUnsavedApps(patchedApps); patchedApps.addAll(unsavedApps); final List toRemove = await getAppsToRemove(patchedApps); patchedApps.removeWhere((a) => toRemove.contains(a)); for (final PatchedApplication app in patchedApps) { app.hasUpdates = await hasAppUpdates(app.originalPackageName, app.patchDate); app.changelog = await getAppChangelog(app.originalPackageName, app.patchDate); if (!app.hasUpdates) { final String? currentInstalledVersion = (await DeviceApps.getApp(app.packageName))?.versionName; if (currentInstalledVersion != null) { final String currentSavedVersion = app.version; final int currentInstalledVersionInt = int.parse( currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''), ); final int currentSavedVersionInt = int.parse( currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''), ); if (currentInstalledVersionInt > currentSavedVersionInt) { app.hasUpdates = true; } } } } await setPatchedApps(patchedApps); } Future isAppUninstalled(PatchedApplication app) async { bool existsRoot = false; final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName); if (app.isRooted) { final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); if (hasRootPermissions) { existsRoot = await _rootAPI.isAppInstalled(app.packageName); } return !existsRoot || !existsNonRoot; } return !existsNonRoot; } Future hasAppUpdates( String packageName, DateTime patchDate, ) async { final List commits = await _githubAPI.getCommits( packageName, getPatchesRepo(), patchDate, ); return commits.isNotEmpty; } Future> getAppChangelog( String packageName, DateTime patchDate, ) async { List newCommits = await _githubAPI.getCommits( packageName, getPatchesRepo(), patchDate, ); if (newCommits.isEmpty) { newCommits = await _githubAPI.getCommits( packageName, getPatchesRepo(), patchDate, ); } return newCommits; } Future isSplitApk(PatchedApplication patchedApp) async { Application? app; if (patchedApp.isFromStorage) { app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath); } else { app = await DeviceApps.getApp(patchedApp.packageName); } return app != null && app.isSplit; } Future setSelectedPatches( String app, List patches, ) async { final File selectedPatchesFile = File(storedPatchesFile); final Map patchesMap = await readSelectedPatchesFile(); if (patches.isEmpty) { patchesMap.remove(app); } else { patchesMap[app] = patches; } selectedPatchesFile.writeAsString(jsonEncode(patchesMap)); } // get default patches for app Future> getDefaultPatches() async { final List patches = await getPatches(); final List defaultPatches = []; if (areExperimentalPatchesEnabled() == false) { defaultPatches.addAll( patches .where( (element) => element.excluded == false && isPatchSupported(element), ) .map((p) => p.name), ); } else { defaultPatches.addAll( patches .where((element) => isPatchSupported(element)) .map((p) => p.name), ); } return defaultPatches; } Future> getSelectedPatches(String app) async { final Map patchesMap = await readSelectedPatchesFile(); final List defaultPatches = await getDefaultPatches(); return List.from(patchesMap.putIfAbsent(app, () => defaultPatches)); } Future> readSelectedPatchesFile() async { final File selectedPatchesFile = File(storedPatchesFile); if (!selectedPatchesFile.existsSync()) { return {}; } final String string = selectedPatchesFile.readAsStringSync(); if (string.trim().isEmpty) { return {}; } return jsonDecode(string); } Future resetLastSelectedPatches() async { final File selectedPatchesFile = File(storedPatchesFile); selectedPatchesFile.deleteSync(); } }