chore: Merge branch dev to main (#1329)

This commit is contained in:
oSumAtrIX 2023-10-05 01:35:08 +02:00 committed by GitHub
commit d9acd0d74b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 346 additions and 613 deletions

View file

@ -85,7 +85,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:14.2.2"
implementation "app.revanced:revanced-patcher:16.0.0"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View file

@ -42,6 +42,10 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ExportSettingsActivity"
android:exported="true">
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />

View file

@ -0,0 +1,86 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.MessageDigest
class ExportSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingPackageName = getCallingPackage()!!
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
// Create JSON Object
val json = JSONObject()
// Default Data
json.put("keystorePassword", "s3cur3p@ssw0rd")
// Load Shared Preferences
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val allEntries: Map<String, *> = sharedPreferences.getAll()
for ((key, value) in allEntries.entries) {
json.put(
key.replace("flutter.", ""),
if (value is Boolean) if (value) 1 else 0 else value
)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View file

@ -8,22 +8,21 @@ import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Error
import java.util.logging.LogRecord
import java.util.logging.Logger
@ -33,6 +32,8 @@ class MainActivity : FlutterActivity() {
private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null
private lateinit var patches: PatchSet
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@ -48,7 +49,6 @@ class MainActivity : FlutterActivity() {
mainChannel.setMethodCallHandler { call, result ->
when (call.method) {
"runPatcher" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
val originalFilePath = call.argument<String>("originalFilePath")
val inputFilePath = call.argument<String>("inputFilePath")
val patchedFilePath = call.argument<String>("patchedFilePath")
@ -59,7 +59,7 @@ class MainActivity : FlutterActivity() {
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null &&
if (
originalFilePath != null &&
inputFilePath != null &&
patchedFilePath != null &&
@ -73,7 +73,6 @@ class MainActivity : FlutterActivity() {
cancel = false
runPatcher(
result,
patchBundleFilePath,
originalFilePath,
inputFilePath,
patchedFilePath,
@ -93,29 +92,44 @@ class MainActivity : FlutterActivity() {
}
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
val cacheDirPath = call.argument<String>("cacheDirPath")
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
val cacheDirPath = call.argument<String>("cacheDirPath")!!
if (patchBundleFilePath != null) {
val patches = PatchBundleLoader.Dex(
try {
patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
optimizedDexDirectory = File(cacheDirPath)
).map { patch ->
val map = HashMap<String, Any>()
map["\"name\""] = "\"${patch.patchName.replace("\"","\\\"")}\""
map["\"description\""] = "\"${patch.description?.replace("\"","\\\"")}\""
map["\"excluded\""] = !patch.include
map["\"dependencies\""] = patch.dependencies?.map { "\"${it.java.patchName}\"" } ?: emptyList<Any>()
map["\"compatiblePackages\""] = patch.compatiblePackages?.map {
val map2 = HashMap<String, Any>()
map2["\"name\""] = "\"${it.name}\""
map2["\"versions\""] = it.versions.map { version -> "\"${version}\"" }
map2
} ?: emptyList<Any>()
map
)
} catch (ex: Exception) {
return@setMethodCallHandler result.notImplemented()
} catch (err: Error) {
return@setMethodCallHandler result.notImplemented()
}
result.success(patches)
} else result.notImplemented()
JSONArray().apply {
patches.forEach {
JSONObject().apply {
put("name", it.name)
put("description", it.description)
put("excluded", !it.use)
put("compatiblePackages", JSONArray().apply {
it.compatiblePackages?.forEach { compatiblePackage ->
val compatiblePackageJson = JSONObject().apply {
put("name", compatiblePackage.name)
put(
"versions",
JSONArray().apply {
compatiblePackage.versions?.forEach { version ->
put(version)
}
})
}
put(compatiblePackageJson)
}
})
}.let(::put)
}
}.toString().let(result::success)
}
else -> result.notImplemented()
@ -125,7 +139,6 @@ class MainActivity : FlutterActivity() {
private fun runPatcher(
result: MethodChannel.Result,
patchBundleFilePath: String,
originalFilePath: String,
inputFilePath: String,
patchedFilePath: String,
@ -168,8 +181,11 @@ class MainActivity : FlutterActivity() {
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) =
override fun publish(record: LogRecord) {
if (record.loggerName?.startsWith("app.revanced") != true) return
updateProgress(-1.0, "", record.message)
}
override fun flush() = Unit
override fun close() = flush()
@ -209,10 +225,7 @@ class MainActivity : FlutterActivity() {
updateProgress(0.1, "Loading patches...", "Loading patches")
val patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
optimizedDexDirectory = cacheDir
).filter { patch ->
val patches = patches.filter { patch ->
val isCompatible = patch.compatiblePackages?.any {
it.name == patcher.context.packageMetadata.packageName
} ?: false
@ -220,7 +233,7 @@ class MainActivity : FlutterActivity() {
val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.patchName }
compatibleOrUniversal && selectedPatches.any { it == patch.name }
}
if (cancel) {
@ -251,9 +264,9 @@ class MainActivity : FlutterActivity() {
val msg = patchResult.exception?.let {
val writer = StringWriter()
it.printStackTrace(PrintWriter(writer))
"${patchResult.patchName} failed: $writer"
"${patchResult.patch.name} failed: $writer"
} ?: run {
"${patchResult.patchName} succeeded"
"${patchResult.patch.name} succeeded"
}
updateProgress(progress, "", msg)
@ -317,7 +330,7 @@ class MainActivity : FlutterActivity() {
val stack = ex.stackTraceToString()
updateProgress(
-100.0,
"Aborted",
"Failed",
"An error occurred:\n$stack"
)
}

View file

@ -23,13 +23,13 @@
"widgetTitle": "Dashboard",
"updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications",
"patchedSubtitle": "Patched apps",
"noUpdates": "No updates available",
"WIP": "Work in progress...",
"noInstallations": "No patched applications installed",
"noInstallations": "No patched apps installed",
"installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager",
@ -56,9 +56,7 @@
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
},
"applicationItem": {
"patchButton": "Patch",
"infoButton": "Info",
"changelogLabel": "Changelog"
"infoButton": "Info"
},
"latestCommitCard": {
"loadingLabel": "Loading...",
@ -71,9 +69,8 @@
"widgetTitle": "Patcher",
"patchButton": "Patch",
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?",
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
},
"appSelectorCard": {
@ -148,9 +145,8 @@
"installTypeDescription": "Select the installation type to proceed with.",
"installButton": "Install",
"installRootType": "Root",
"installNonRootType": "Non-root",
"installRecommendedType": "Recommended",
"installRootType": "Mount",
"installNonRootType": "Normal",
"pressBackAgain": "Press back again to cancel",
"openButton": "Open",
@ -162,10 +158,6 @@
"exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export log",
"installErrorDialogTitle": "Error",
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
"noExit": "Installer is still running, cannot exit..."
},
"settingsView": {
@ -285,7 +277,6 @@
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package name",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type",
"rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root",

View file

@ -12,7 +12,7 @@ This page will guide you through building ReVanced Manager from source.
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
4. Add your GitHub username and the token to `~/android/gradle.properties`
```properties
gpr.user = YourUsername

View file

@ -1,5 +1,4 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:revanced_manager/utils/string.dart';
part 'patch.g.dart';
@ -9,26 +8,19 @@ class Patch {
required this.name,
required this.description,
required this.excluded,
required this.dependencies,
required this.compatiblePackages,
});
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
final String name;
final String description;
final String? description;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Map<String, dynamic> toJson() => _$PatchToJson(this);
String getSimpleName() {
return name
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG');
return name;
}
}

View file

@ -9,23 +9,19 @@ class PatchedApplication {
PatchedApplication({
required this.name,
required this.packageName,
required this.originalPackageName,
required this.version,
required this.apkFilePath,
required this.icon,
required this.patchDate,
this.isRooted = false,
this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [],
this.changelog = const [],
});
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json);
String name;
String packageName;
String originalPackageName;
String version;
final String apkFilePath;
@JsonKey(
@ -36,9 +32,7 @@ class PatchedApplication {
DateTime patchDate;
bool isRooted;
bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches;
List<String> changelog;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
@ -7,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
@ -21,17 +19,6 @@ class GithubAPI {
priority: CachePriority.high,
);
final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube',
'com.google.android.apps.youtube.music': 'music',
'com.twitter.android': 'twitter',
'com.reddit.frontpage': 'reddit',
'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
};
Future<void> initialize(String repoUrl) async {
try {
_dio = Dio(
@ -142,38 +129,6 @@ class GithubAPI {
}
}
Future<List<String>> getCommits(
String packageName,
String repoName,
DateTime since,
) async {
final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
final response = await _dio.get(
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'since': since.toIso8601String(),
},
);
final List<dynamic> commits = response.data;
return commits
.map(
(commit) => commit['commit']['message'].split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return [];
}
Future<File?> getLatestReleaseFile(
String extension,
String repoName,
@ -237,30 +192,4 @@ class GithubAPI {
}
return null;
}
Future<List<Patch>> getPatches(
String repoName,
String version,
String url,
) async {
List<Patch> patches = [];
try {
final File? f = await getPatchesReleaseFile(
'.json',
repoName,
version,
url,
);
if (f != null) {
final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList();
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return patches;
}
}

View file

@ -42,12 +42,14 @@ class ManagerAPI {
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';
return getIntegrationsRepo().toLowerCase() ==
'revanced/revanced-integrations';
}
Future<void> initialize() async {
@ -315,18 +317,18 @@ class ManagerAPI {
if (patchBundleFile != null) {
try {
final patchesObject = await PatcherAPI.patcherChannel.invokeMethod(
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
'cacheDirPath': cacheDir.path,
},
);
final List<Map<String, dynamic>> patchesMap = [];
patchesObject.forEach((patch) {
patchesMap.add(jsonDecode('$patch'));
});
patches = patchesMap.map((patch) => Patch.fromJson(patch)).toList();
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
patches = patchesJsonList
.map((patchJson) => Patch.fromJson(patchJson))
.toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
@ -503,25 +505,21 @@ class ManagerAPI {
return toRemove;
}
Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps,
) async {
final List<PatchedApplication> unsavedApps = [];
Future<List<PatchedApplication>> getMountedApps() async {
final List<PatchedApplication> mountedApps = [];
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
final List<String> 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(
mountedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
@ -532,33 +530,8 @@ class ManagerAPI {
}
}
}
}
final List<Application> 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;
return mountedApps;
}
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
@ -620,34 +593,20 @@ class ManagerAPI {
Future<void> reAssessSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps);
// Remove apps that are not installed anymore.
final List<PatchedApplication> 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]'), ''),
// Determine all apps that are installed by mounting.
final List<PatchedApplication> mountedApps = await getMountedApps();
mountedApps.removeWhere(
(app) => patchedApps
.any((patchedApp) => patchedApp.packageName == app.packageName),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
patchedApps.addAll(mountedApps);
await setPatchedApps(patchedApps);
}
@ -664,37 +623,6 @@ class ManagerAPI {
return !existsNonRoot;
}
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
return commits.isNotEmpty;
}
Future<List<String>> getAppChangelog(
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
if (newCommits.isEmpty) {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app;
if (patchedApp.isFromStorage) {
@ -762,6 +690,8 @@ class ManagerAPI {
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.deleteSync();
}
}
}

View file

@ -28,10 +28,10 @@ class PatcherAPI {
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{};
File? _outFile;
File? outFile;
Future<void> initialize() async {
await _loadPatches();
await loadPatches();
await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache;
@ -59,12 +59,10 @@ class PatcherAPI {
}
List<Patch> getUniversalPatches() {
return _patches
.where((patch) => patch.compatiblePackages.isEmpty)
.toList();
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
}
Future<void> _loadPatches() async {
Future<void> loadPatches() async {
try {
if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches();
@ -85,15 +83,14 @@ class PatcherAPI {
) async {
final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded =
_universalPatches.isNotEmpty &&
showUniversalPatches;
_universalPatches.isNotEmpty && showUniversalPatches;
if (allAppsIncluded) {
final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true,
onlyAppsWithLaunchIntent: true,
);
for(final app in appList) {
for (final app in appList) {
filteredApps.add(app as ApplicationWithIcon);
}
}
@ -149,55 +146,20 @@ class PatcherAPI {
.toList();
}
Future<bool> needsResourcePatching(
List<Patch> selectedPatches,
) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
),
);
}
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('settings'),
),
);
}
Future<void> runPatcher(
String packageName,
String apkFilePath,
List<Patch> selectedPatches,
) async {
final bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) {
try {
final Patch? settingsPatch = _patches.firstWhereOrNull(
(patch) =>
patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName),
);
if (settingsPatch != null) {
selectedPatches.add(settingsPatch);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
final File? patchBundleFile = await _managerAPI.downloadPatches();
final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (patchBundleFile != null) {
if (integrationsFile != null) {
_dataDir.createSync();
_tmpDir.createSync();
final Directory workDir = _tmpDir.createTempSync('tmp-');
final File inputFile = File('${workDir.path}/base.apk');
final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk');
outFile = File('${workDir.path}/out.apk');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
final String originalFilePath = apkFilePath;
@ -205,12 +167,11 @@ class PatcherAPI {
await patcherChannel.invokeMethod(
'runPatcher',
{
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': originalFilePath,
'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path,
'integrationsPath': integrationsFile!.path,
'outFilePath': outFile!.path,
'integrationsPath': integrationsFile.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path,
@ -236,7 +197,7 @@ class PatcherAPI {
}
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
if (_outFile != null) {
if (outFile != null) {
try {
if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
@ -244,11 +205,11 @@ class PatcherAPI {
return _rootAPI.installApp(
patchedApp.packageName,
patchedApp.apkFilePath,
_outFile!.path,
outFile!.path,
);
}
} else {
final install = await InstallPlugin.installApk(_outFile!.path);
final install = await InstallPlugin.installApk(outFile!.path);
return install['isSuccess'];
}
} on Exception catch (e) {
@ -263,11 +224,11 @@ class PatcherAPI {
void exportPatchedFile(String appName, String version) {
try {
if (_outFile != null) {
if (outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: _outFile!.path,
sourceFilePath: outFile!.path,
destinationFileName: newName,
),
);
@ -281,12 +242,12 @@ class PatcherAPI {
void sharePatchedFile(String appName, String version) {
try {
if (_outFile != null) {
if (outFile != null) {
final String newName = _getFileName(appName, version);
final int lastSeparator = _outFile!.path.lastIndexOf('/');
final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = _outFile!.copySync(newPath);
outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e) {

View file

@ -7,12 +7,15 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class RevancedAPI {
late Dio _dio = Dio();
final Lock getToolsLock = Lock();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
@ -66,7 +69,8 @@ class RevancedAPI {
Future<Map<String, dynamic>?> _getLatestRelease(
String extension,
String repoName,
) async {
) {
return getToolsLock.synchronized(() async {
try {
final response = await _dio.get('/tools');
final List<dynamic> tools = response.data['tools'];
@ -81,6 +85,7 @@ class RevancedAPI {
}
return null;
}
});
}
Future<String?> getLatestReleaseVersion(

View file

@ -73,7 +73,6 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
@ -202,7 +201,6 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: result.files.single.path!,
icon: application.icon,

View file

@ -37,7 +37,6 @@ class HomeViewModel extends BaseViewModel {
DateTime? _lastUpdate;
bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = [];
String? _latestManagerVersion = '';
File? downloadedApk;
@ -82,7 +81,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
}
@ -108,10 +107,6 @@ class HomeViewModel extends BaseViewModel {
void _getPatchedApps() {
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
.toList();
notifyListeners();
}
@ -469,11 +464,7 @@ class HomeViewModel extends BaseViewModel {
}
Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_toast.showBottom('homeView.refreshSuccess');
initialize(context);
}

View file

@ -130,10 +130,6 @@ class InstallerViewModel extends BaseViewModel {
Future<void> runPatcher() async {
try {
update(0.0, 'Initializing...', 'Initializing installer');
if (_patches.isNotEmpty) {
try {
update(0.1, '', 'Creating working directory');
await _patcherAPI.runPatcher(
_app.packageName,
_app.apkFilePath,
@ -142,16 +138,20 @@ class InstallerViewModel extends BaseViewModel {
} on Exception catch (e) {
update(
-100.0,
'Aborted...',
'An error occurred! Aborted\nError:\n$e',
'Failed...',
'Something went wrong:\n$e',
);
if (kDebugMode) {
print(e);
}
}
} else {
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
}
// Necessary to reset the state of patches by reloading them
// in a later patching process.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
FlutterBackground.disableBackgroundExecution();
@ -209,8 +209,8 @@ class InstallerViewModel extends BaseViewModel {
),
RadioListTile(
title: I18nText('installerView.installNonRootType'),
subtitle: I18nText('installerView.installRecommendedType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (selected) {
@ -219,7 +219,8 @@ class InstallerViewModel extends BaseViewModel {
),
RadioListTile(
title: I18nText('installerView.installRootType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (selected) {
@ -257,9 +258,9 @@ class InstallerViewModel extends BaseViewModel {
Future<void> stopPatcher() async {
try {
isCanceled = true;
update(0.5, 'Aborting...', 'Canceling patching process');
update(0.5, 'Canceling...', 'Canceling patching process');
await _patcherAPI.stopPatcher();
update(-100.0, 'Aborted...', 'Press back to exit');
update(-100.0, 'Canceled...', 'Press back to exit');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
@ -270,34 +271,6 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async {
try {
_app.isRooted = installAsRoot;
final bool hasMicroG =
_patches.any((p) => p.name.endsWith('MicroG support'));
final bool rootMicroG = installAsRoot && hasMicroG;
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
final bool ytWithoutRootMicroG =
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('installerView.installErrorDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
rootMicroG
? 'installerView.installErrorDialogText1'
: rootFromStorage
? 'installerView.installErrorDialogText3'
: 'installerView.installErrorDialogText2',
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
} else {
update(
1.0,
'Installing...',
@ -307,19 +280,24 @@ class InstallerViewModel extends BaseViewModel {
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
update(1.0, 'Installed!', 'Installed!');
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
if (hasMicroG) {
_app.name += ' ReVanced';
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
// In case a patch changed the app name or package name,
// update the app info.
final app =
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
if (app != null) {
_app.name = app.appName;
_app.packageName = app.packageName;
}
await _managerAPI.savePatchedApp(_app);
}
update(1.0, 'Installed!', 'Installed!');
} else {
// TODO(aabed): Show error message.
}
} on Exception catch (e) {
if (kDebugMode) {

View file

@ -39,9 +39,9 @@ class NavigationViewModel extends IndexTrackingViewModel {
// Force disable Material You on Android 11 and below
if (dynamicTheme.themeId.isOdd) {
const int ANDROID_12_SDK_VERSION = 31;
const int android12SdkVersion = 31;
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt < ANDROID_12_SDK_VERSION) {
if (info.version.sdkInt < android12SdkVersion) {
await prefs.setInt('themeMode', 0);
await prefs.setBool('useDynamicTheme', false);
await dynamicTheme.setTheme(0);

View file

@ -44,49 +44,6 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null;
}
Future<bool> isValidPatchConfig() async {
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
Future<void> showPatchConfirmationDialog(BuildContext context) async {
final bool isValid = await isValidPatchConfig();
if (context.mounted) {
if (isValid) {
showArmv7WarningDialog(context);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.splitApkWarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
),
],
),
);
}
}
}
Future<void> showRemovedPatchesDialog(BuildContext context) async {
if (removedPatches.isNotEmpty) {
return showDialog(
@ -115,7 +72,7 @@ class PatcherViewModel extends BaseViewModel {
),
);
} else {
showArmv7WarningDialog(context);
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
}
}
@ -185,9 +142,9 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear();
removedPatches.clear();
final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
this
.selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
@ -203,7 +160,7 @@ class PatcherViewModel extends BaseViewModel {
.selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
}
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}');

View file

@ -194,7 +194,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
description: patch.description ?? '',
packageVersion: model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
@ -246,7 +246,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:

View file

@ -28,7 +28,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll(
_patcherAPI.getFilteredPatches(
selectedApp!.originalPackageName,
selectedApp!.packageName,
),
);
patches.sort((a, b) {
@ -98,11 +98,11 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectDefaultPatches() {
selectedPatches.clear();
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
selectedPatches.addAll(
_patcherAPI
.getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
)
.where(
(element) =>
@ -187,7 +187,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
selectedPatches,
);
}
@ -195,7 +195,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
Future<void> loadSelectedPatches(BuildContext context) async {
if (_managerAPI.isPatchesChangeEnabled()) {
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
);
if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear();

View file

@ -71,12 +71,6 @@ class SettingsViewModel extends BaseViewModel {
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
@ -84,6 +78,12 @@ class SettingsViewModel extends BaseViewModel {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);

View file

@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
subtitle: Text(app.packageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.originalPackageNameLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: Text(app.originalPackageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),

View file

@ -13,7 +13,6 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/string.dart';
import 'package:stacked/stacked.dart';
class AppInfoViewModel extends BaseViewModel {
@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
}
String getAppliedPatchesString(List<String> appliedPatches) {
final List<String> names = appliedPatches
.map(
(p) => p
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG'),
)
.toList();
return '\u2022 ${names.join('\n\u2022 ')}';
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
}
void openApp(PatchedApplication app) {

View file

@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
icon: app.icon,
name: app.name,
patchDate: app.patchDate,
changelog: app.changelog,
isUpdatableApp: false,
onPressed: () =>
locator<HomeViewModel>().navigateToAppInfo(app),
),

View file

@ -5,8 +5,8 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';

View file

@ -1,6 +1,5 @@
import 'dart:typed_data';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@ -13,63 +12,30 @@ class ApplicationItem extends StatefulWidget {
required this.icon,
required this.name,
required this.patchDate,
required this.changelog,
required this.isUpdatableApp,
required this.onPressed,
}) : super(key: key);
final Uint8List icon;
final String name;
final DateTime patchDate;
final List<String> changelog;
final bool isUpdatableApp;
final Function() onPressed;
@override
State<ApplicationItem> createState() => _ApplicationItemState();
}
class _ApplicationItemState extends State<ApplicationItem>
with TickerProviderStateMixin {
late AnimationController _animationController;
class _ApplicationItemState extends State<ApplicationItem> {
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ExpandableController expController = ExpandableController();
return Container(
margin: const EdgeInsets.only(bottom: 16.0),
child: CustomCard(
onTap: () {
expController.toggle();
_animationController.isCompleted
? _animationController.reverse()
: _animationController.forward();
},
child: ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: Row(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
@ -110,23 +76,13 @@ class _ApplicationItemState extends State<ApplicationItem>
),
Row(
children: [
RotationTransition(
turns: Tween(begin: 0.0, end: 0.50)
.animate(_animationController),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
label: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
@ -135,30 +91,6 @@ class _ApplicationItemState extends State<ApplicationItem>
),
],
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 4.0,
right: 4.0,
bottom: 4.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'applicationItem.changelogLabel',
child: const Text(
'',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
],
),
),
),
),
);
}

View file

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 1.10.3+101000300
version: 1.11.0+101100000
environment:
sdk: '>=3.0.0 <4.0.0'
@ -75,6 +75,7 @@ dependencies:
flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0
install_plugin: ^2.1.0
synchronized: ^3.1.0
dev_dependencies:
json_serializable: ^6.6.1