import 'dart:convert'; import 'dart:io'; import 'package:app_installer/app_installer.dart'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/services.dart'; import 'package:injectable/injectable.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/manager_api.dart'; import 'package:revanced_manager/services/root_api.dart'; import 'package:share_extend/share_extend.dart'; @lazySingleton class PatcherAPI { static const patcherChannel = MethodChannel('app.revanced.manager.flutter/patcher'); final ManagerAPI _managerAPI = locator(); final RootAPI _rootAPI = RootAPI(); late Directory _tmpDir; late File _keyStoreFile; List _patches = []; File? _outFile; Future initialize() async { await _loadPatches(); Directory appCache = await getTemporaryDirectory(); _tmpDir = Directory('${appCache.path}/patcher'); _keyStoreFile = File('${appCache.path}/revanced-manager.keystore'); cleanPatcher(); } void cleanPatcher() { if (_tmpDir.existsSync()) { _tmpDir.deleteSync(recursive: true); } } Future _loadPatches() async { try { if (_patches.isEmpty) { File? patchJsonFile = await _managerAPI.downloadPatches('.json'); if (patchJsonFile != null) { List list = json.decode(patchJsonFile.readAsStringSync()); _patches = list.map((patch) => Patch.fromJson(patch)).toList(); } } } on Exception { _patches = List.empty(); } } Future> getFilteredInstalledApps() async { List filteredApps = []; await _loadPatches(); for (Patch patch in _patches) { for (Package package in patch.compatiblePackages) { try { if (!filteredApps.any((app) => app.packageName == package.name)) { ApplicationWithIcon? app = await DeviceApps.getApp(package.name, true) as ApplicationWithIcon?; if (app != null) { filteredApps.add(app); } } } catch (e) { continue; } } } return filteredApps; } Future> getFilteredPatches(String packageName) async { await _loadPatches(); return _patches .where((patch) => !patch.name.contains('settings') && patch.compatiblePackages.any((pack) => pack.name == packageName)) .toList(); } Future> getAppliedPatches(List appliedPatches) async { await _loadPatches(); return _patches .where((patch) => appliedPatches.contains(patch.name)) .toList(); } Future runPatcher( String packageName, String originalFilePath, List selectedPatches, ) async { bool mergeIntegrations = selectedPatches.any( (patch) => patch.dependencies.contains('integrations'), ); bool resourcePatching = selectedPatches.any( (patch) => patch.dependencies.any((dep) => dep.contains('resource-')), ); bool includeSettings = selectedPatches.any( (patch) => patch.dependencies.contains('settings'), ); if (includeSettings) { try { Patch settingsPatch = _patches.firstWhere( (patch) => patch.name.contains('settings') && patch.compatiblePackages.any((pack) => pack.name == packageName), ); selectedPatches.add(settingsPatch); } catch (e) { // ignore } } File? patchBundleFile = await _managerAPI.downloadPatches('.jar'); File? integrationsFile; if (mergeIntegrations) { integrationsFile = await _managerAPI.downloadIntegrations('.apk'); } if (patchBundleFile != null) { _tmpDir.createSync(); Directory workDir = _tmpDir.createTempSync('tmp-'); File inputFile = File('${workDir.path}/base.apk'); File patchedFile = File('${workDir.path}/patched.apk'); _outFile = File('${workDir.path}/out.apk'); Directory cacheDir = Directory('${workDir.path}/cache'); cacheDir.createSync(); await patcherChannel.invokeMethod( 'runPatcher', { 'patchBundleFilePath': patchBundleFile.path, 'originalFilePath': originalFilePath, 'inputFilePath': inputFile.path, 'patchedFilePath': patchedFile.path, 'outFilePath': _outFile!.path, 'integrationsPath': mergeIntegrations ? integrationsFile!.path : '', 'selectedPatches': selectedPatches.map((p) => p.name).toList(), 'cacheDirPath': cacheDir.path, 'mergeIntegrations': mergeIntegrations, 'resourcePatching': resourcePatching, 'keyStoreFilePath': _keyStoreFile.path, }, ); } } Future installPatchedFile(PatchedApplication patchedApp) async { if (_outFile != null) { try { if (patchedApp.isRooted) { bool hasRootPermissions = await _rootAPI.hasRootPermissions(); if (hasRootPermissions) { return _rootAPI.installApp( patchedApp.packageName, patchedApp.apkFilePath, _outFile!.path, ); } else { return false; } } else { await AppInstaller.installApk(_outFile!.path); return await DeviceApps.isAppInstalled(patchedApp.packageName); } } on Exception { return false; } } return false; } void sharePatchedFile(String appName, String version) { if (_outFile != null) { String prefix = appName.toLowerCase().replaceAll(' ', '-'); String newName = '$prefix-revanced_v$version.apk'; int lastSeparator = _outFile!.path.lastIndexOf('/'); File share = _outFile!.renameSync( _outFile!.path.substring(0, lastSeparator + 1) + newName, ); ShareExtend.share(share.path, 'file'); } } Future checkOldPatch(PatchedApplication patchedApp) async { if (patchedApp.isRooted) { return await _rootAPI.isAppInstalled(patchedApp.packageName); } return false; } Future deleteOldPatch(PatchedApplication patchedApp) async { if (patchedApp.isRooted) { await _rootAPI.deleteApp(patchedApp.packageName, patchedApp.apkFilePath); } } void shareLog(String logs) { ShareExtend.share(logs, 'text'); } }