Skip to content

Commit

Permalink
Test xapk implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
alesimula committed Feb 6, 2022
1 parent a3dea3b commit 753570f
Show file tree
Hide file tree
Showing 15 changed files with 817 additions and 109 deletions.
24 changes: 21 additions & 3 deletions lib/android/android_utils.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:wsa_pacman/android/reader_apk.dart';
import 'package:wsa_pacman/android/reader_xapk.dart';
import 'package:wsa_pacman/utils/locale_utils.dart';

class Resource {
Expand All @@ -7,14 +9,30 @@ class Resource {
}

enum InstallState {
PROMPT, INSTALLING, SUCCESS, ERROR
PROMPT, INSTALLING, SUCCESS, ERROR
}
enum InstallType {
UNKNOWN, INSTALL, REINSTALL, UPDATE, DOWNGRADE
UNKNOWN, INSTALL, REINSTALL, UPDATE, DOWNGRADE
}
enum ResType {
COLOR, FILE
COLOR, FILE
}
enum AppPackage {
NONE, APK, XAPK
}

extension AppPackageType on AppPackage {
static AppPackage fromArguments(List<String> args) => args.isEmpty ? AppPackage.NONE : fromFilename(args.first);
static AppPackage fromFilename(String? name) => name == null || name.isEmpty ? AppPackage.NONE :
name.endsWith(".xapk") ? AppPackage.XAPK : AppPackage.APK;
void Function(String) get read { switch (this) {
case AppPackage.APK: return ApkReader.start;
case AppPackage.XAPK: return XapkReader.start;
case AppPackage.NONE: return (_){};
}}
bool get directInstall => this == AppPackage.APK;
}

extension InstallTypeExt on InstallType {
String buttonText(AppLocalizations locale) {switch (this) {
case InstallType.UNKNOWN: return locale.installer_btn_install;
Expand Down
46 changes: 29 additions & 17 deletions lib/android/reader_apk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:collection';
import 'dart:io';
import 'dart:typed_data';

import 'package:archive/archive_io.dart';
import 'package:wsa_pacman/windows/nt_io.dart';
import 'package:wsa_pacman/windows/win_io.dart';
import 'package:wsa_pacman/windows/win_path.dart';
Expand Down Expand Up @@ -46,7 +47,7 @@ extension on int {
class ApkReader {
//I just put '&& true' there so I could conveniently switch it off
static bool DEBUG = !kReleaseMode && true;
static String APK_FILE = /*r'C:\Users\Alex\Downloads\com.atono.dropticket.apk'*/ '';
static String APK_FILE = '';
static late Future<Map<String, Resource>> _resourceDump;
static late Future<Map<int, String>> _stringDump;
static late Future<Archive> _apkArchive;
Expand All @@ -56,6 +57,7 @@ class ApkReader {
static const String REGEX_QUOTED_TYPE = r'["'']type[0-9]+/([0-9]*)["'']';

static Future<Archive> _initArchiveFile(File file) async {
//var stream = InputFileStream(file.absolute.path);
return ZipDecoder().decodeBytes(file.readAsBytesSync());
}
static void _initArchive() {
Expand Down Expand Up @@ -184,7 +186,7 @@ class ApkReader {
}

/// Retrieves installation type (whether installing for the first time, reinstalling the same version, upgrading or downgrading)
static Future _loadInstallType(String package, int versionCode) async {if (package.isNotEmpty) {
static Future loadInstallType(String package, int versionCode) async {if (package.isNotEmpty) {
GState.androidPort;
String ipAddress = await GState.ipAddress.whenReady();
int port = await GState.androidPort.whenReady();
Expand Down Expand Up @@ -261,7 +263,7 @@ class ApkReader {

String package = info?.find(r"(^|\n|\s)name=\s*'([^'\n\s$]*)", 2) ?? "";
if (package.isNotEmpty) {
data.execute(() {GState.package.update((_) => package); _loadInstallType(package, versionCode);});
data.execute(() {GState.package.update((_) => package); loadInstallType(package, versionCode);});
}
//else data.execute(() => GState.apkInstallType.update((_) => InstallType.INSTALL));

Expand Down Expand Up @@ -309,19 +311,29 @@ class ApkReader {
await process;
if (inner != null) await inner;
if (iconUpdThread != null) await iconUpdThread;
if (data.legacyIcon) data.execute(() async {if (GState.apkForegroundIcon.$ == null && GState.apkIcon.$ == null) {
final legacy = await ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_legacy.si");
GState.apkIcon.update((p0) => (ScalableImageWidget(si: legacy)));
}});
else data.execute(() async {if (GState.apkForegroundIcon.$ == null && GState.apkIcon.$ == null) {
final fBackground = ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_background.si");
final fForeground = ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_foreground.si");
final background = await fBackground;
final foreground = await fForeground;
GState.apkBackgroundIcon.update((p0) => (ScalableImageWidget(si: background)));
GState.apkForegroundIcon.update((p0) => (ScalableImageWidget(si: foreground)));
}});
//data.pipe.send("WOOOOOOOO2: ${coso.stdout.toString()}");
bool legacyIcon = data.legacyIcon;
data.execute(() {
setDefaultIcon(legacyIcon);
});
//(await _apkArchive).clear();
}

/// Uses the default application icon if no icon has been found
/// Has to be called in the UI thread
static void setDefaultIcon(bool legacyIcon) async {
if (GState.apkForegroundIcon.$ == null && GState.apkIcon.$ == null) {
if (legacyIcon) {
final legacy = await ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_legacy.si");
GState.apkIcon.update((p0) => (ScalableImageWidget(si: legacy)));
}
else {
final fBackground = ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_background.si");
final fForeground = ScalableImage.fromSIAsset(rootBundle, "assets/icons/missing_icon_foreground.si");
final background = await fBackground, foreground = await fForeground;
GState.apkBackgroundIcon.update((p0) => (ScalableImageWidget(si: background)));
GState.apkForegroundIcon.update((p0) => (ScalableImageWidget(si: foreground)));
}
}
}

FutureOr<R> computeOrDebug<Q, R>(ComputeCallback<Q, R> callback, Q message, {String? debugLabel}) => (DEBUG) ?
Expand All @@ -342,7 +354,7 @@ class ApkReader {
String package = GState.package.$;
InstallType? installType = GState.apkInstallType.$;
if (GState.apkInstallType.$ == InstallType.UNKNOWN) {
await _loadInstallType(GState.package.$, _versionCode);
await loadInstallType(GState.package.$, _versionCode);
if (GState.apkInstallType.$ != InstallType.UNKNOWN) sub?.cancel();
}
else if (installType != null) sub?.cancel();
Expand Down
183 changes: 183 additions & 0 deletions lib/android/reader_xapk.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// ignore_for_file: non_constant_identifier_names, curly_braces_in_flow_control_structures, constant_identifier_names

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:collection/collection.dart';

import 'package:archive/archive.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:wsa_pacman/android/android_utils.dart';
import 'package:wsa_pacman/android/permissions.dart';
import 'package:wsa_pacman/android/reader_apk.dart';
import 'package:wsa_pacman/global_state.dart';
import 'package:wsa_pacman/main.dart';
import 'package:wsa_pacman/utils/misc_utils.dart';
import 'package:wsa_pacman/proto/manifest_xapk.pb.dart';
import 'package:wsa_pacman/utils/regexp_utils.dart';
import 'package:wsa_pacman/widget/adaptive_icon.dart';
import 'package:wsa_pacman/windows/nt_io.dart';
import 'package:wsa_pacman/windows/win_path.dart';

enum Architecture {
amd64, i386, aarch64, arm, ppc64, ppc
}

extension Architectures on Architecture {
static late final fullRegex = '(${[for (final arch in Architecture.values) for (final label in arch.labels) label].join('|')})';
get regex => '(${[for (final label in labels) label].join('|')})';
List<String> get labels => (){switch (this) {
case Architecture.i386: return ["i386", "i686", "i586", "i486", "x86"];
case Architecture.amd64: return ["x86_64", "amd64"];
case Architecture.arm: return ["aarch32", "arm"];
case Architecture.aarch64: return ["arm64", "aarch64"];
case Architecture.ppc: return ["powerpc", "ppc"];
case Architecture.ppc64: return ["powerpc64", "ppc64"];
}}();
}



class XapkReader {
static int _versionCode = 0;
static String APK_FILE = '';
static late Future<Archive> _xapkArchive;
static late final Directory _xapkTempDir = Directory(WinPath.tempSubdir).createTempSync("XAPK-Extracted@$pid@");

static Future<Archive> _initArchiveFile(File file) async => ZipDecoder().decodeBytes(file.readAsBytesSync());
static void _initArchive() {
//Maintain a lock on the file
File file = File(APK_FILE)..open();
_xapkArchive = _initArchiveFile(file);
}

static ManifestXapk _decodeManifest(List<int> bytes) => ManifestXapk.create()
..mergeFromProto3Json(utf8.decoder.fuse(json.decoder).convert(bytes));

static void installXApk(String workingDir, List<String> apkFiles, List<ManifestXapk_ApkExpansion> expansions, String ipAddress, int port, AppLocalizations lang, [bool downgrade = false]) async {
if (apkFiles.isNotEmpty) log("INSTALLING \"${apkFiles.first}\" on on $ipAddress:$port...");
var installation = Process.run('${Env.TOOLS_DIR}\\adb.exe', ['-s', '$ipAddress:$port', 'install-multiple', if (downgrade) '-r', if (downgrade) '-d', ...apkFiles], workingDirectory: workingDir)
.timeout(const Duration(seconds: 30)).onError((error, stackTrace) => ProcessResult(-1, -1, null, null));
log("COMMAND: ${['-s', '$ipAddress:$port', 'install-multiple', if (downgrade) '-r', if (downgrade) '-d', ...apkFiles].join(" ")}");
GState.apkInstallState.update((_) => InstallState.INSTALLING);
var result = await installation;
log("EXIT CODE: ${result.exitCode}");
String error = result.stderr.toString();
log("OUTPUT: ${result.stdout}");
log("ERROR: $error");
if (result.exitCode == 0) GState.apkInstallState.update((_) => InstallState.SUCCESS);
else {
GState.apkInstallState.update((_) => InstallState.ERROR);
//TODO add cause
RegExpMatch? errorMatch = RegExp(r'(^|\n)\s*adb:\s+failed\s+to\s+install\s+.*:\s+Failure\s+\[([^:]*):\s*([^\s].*[^\s])\s*\]').firstMatch(error);
String errorCode = errorMatch?.group(2) ?? "";
GState.errorCode.update((_) => errorCode.isNotEmpty ? errorCode : "UNKNOWN_ERROR");
String errorDesc = errorMatch?.group(3) ?? "";
GState.errorDesc.update((_) => errorDesc.isNotEmpty ? errorDesc : lang.installer_error_nomsg);
}
}

static List<String> _getApkList(ManifestXapk manifest) {
final archRegex = RegExp('^config\\.${Architectures.fullRegex}.*');
final String defaultBaseName = '${manifest.packageName}.apk';
Iterable<String> apkList;
if (manifest.splitApks.isNotEmpty) {
bool isBaseApk(ManifestXapk_ApkFile fileInfo) => fileInfo.id == 'base' || fileInfo.file == defaultBaseName;
ManifestXapk_ApkFile? baseApk = manifest.splitApks.firstWhereOrNull(isBaseApk);
if (manifest.splitApks.first == baseApk || baseApk == null) apkList = manifest.splitApks.map((e) => e.file);
else apkList = [baseApk.file].followedBy(manifest.splitApks.whereNot(isBaseApk).map((e) => e.file));
}
else if (manifest.splitConfigs.isNotEmpty) {
Iterable<String> configFiles = manifest.splitConfigs.map((e) => '$e.apk');
apkList = manifest.splitConfigs.contains(manifest.packageName) ? configFiles : [defaultBaseName].followedBy(configFiles);
}
else apkList = [defaultBaseName];

final List<String> archApkList = apkList.where((file) => archRegex.hasMatch(file)).toList();
if (archApkList.isEmpty || archApkList.length == 1) return apkList.toList();
apkList = apkList.whereNot((file) => archRegex.hasMatch(file));
for (final arch in Architecture.values) {
final regex = RegExp('^config\\.${arch.regex}.*');
for (final file in archApkList) if (regex.hasMatch(file)) return apkList.followedBy([file]).toList();
}
return apkList.followedBy([apkList.first]).toList();
}

static void _readManifest(IsolateData pData) async { try {
APK_FILE = pData.fileName;
_initArchive();
final archive = (await _xapkArchive);
final manifestFile = archive.findFile('manifest.json');
log("LOADING MANIFEST");
// TODO loading
if (manifestFile == null) return;
log("READING MANIFEST");
final manifest = _decodeManifest(manifestFile.content as List<int>);
Set<AndroidPermission> permissions = manifest.permissions.map((perm) => AndroidPermissionList.get(perm)).whereNotNull().toSet();
pData.execute(() {
_versionCode = manifest.versionCode;
GState.apkTitle.$ = manifest.name;
GState.version.$ = manifest.versionName;
GState.package.$ = manifest.packageName;
GState.permissions.$ = permissions;
});
String iconFile = manifest.icon.isNotEmpty ? manifest.icon : "icon.png";
final icon = archive.findFile(iconFile);
final image = icon != null ? Image.memory(icon.content) : null;
pData.execute(() async {
if (image != null) {
GState.apkAdaptiveNoScale.$ = true;
GState.apkBackgroundIcon.$ = image;
GState.apkForegroundIcon.$ = const SizedBox();
}
else ApkReader.setDefaultIcon(await GState.legacyIcons.whenReady());
});

final apkList = _getApkList(manifest);
String installDir = _xapkTempDir.absolute.path;
pData.execute(() {
GState.installCallback.$ = (ipAddress, port, lang, [downgrade = false]) => installXApk(installDir, apkList, [], ipAddress, port, lang, downgrade);
});



/*final handle = NtIO.openDirectory(_xapkTempDir.absolute.path, true, true);
log("HANDLE: $handle");*/

archive.extractAllSync(_xapkTempDir);
if (manifest.packageName.isNotEmpty) pData.execute(() {
ApkReader.loadInstallType(manifest.packageName, manifest.versionCode);
});

log("DIRECTORY: ${_xapkTempDir.path}");
} catch (e) {
_xapkTempDir.deleteSync(recursive: true);
//(await _xapkArchive).clear();
}}


/// Starts a process to read apk data
static void start(String fileName) async {
APK_FILE = fileName;
ReceivePort port = ReceivePort();
port.listen((message) {
if (message is VoidCallback) {message();}
});
//Recheck installation type when connected
compute(_readManifest, IsolateData(fileName, false, port.sendPort));
StreamSubscription? sub;
sub = GState.connectionStatus.stream.listen((event) async {
String package = GState.package.$;
InstallType? installType = GState.apkInstallType.$;
if (GState.apkInstallType.$ == InstallType.UNKNOWN) {
await ApkReader.loadInstallType(GState.package.$, _versionCode);
if (GState.apkInstallType.$ != InstallType.UNKNOWN) sub?.cancel();
}
else if (installType != null) sub?.cancel();
});
}
}
8 changes: 6 additions & 2 deletions lib/apk_installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class _ApkInstallerState extends State<ApkInstaller> {
Widget icon;
String appTitle = GState.apkTitle.of(context);
Widget? aForeground = GState.apkForegroundIcon.of(context);
bool adaptiveNoScale = GState.apkAdaptiveNoScale.of(context);
Widget? lIcon = GState.apkIcon.of(context);
WSAStatusAlert connectionStatus = GState.connectionStatus.of(context);
bool isConnected = connectionStatus.isConnected;
Expand Down Expand Up @@ -101,7 +102,7 @@ class _ApkInstallerState extends State<ApkInstaller> {
String ipAddress = GState.ipAddress.of(context);
int port = GState.androidPort.of(context);

if (aForeground != null) icon = AdaptiveIcon(backColor: GState.apkBackgroundColor.of(context), background: GState.apkBackgroundIcon.of(context), foreground: aForeground, radius: GState.iconShape.of(context).radius);
if (aForeground != null) icon = AdaptiveIcon(noScale: adaptiveNoScale, backColor: GState.apkBackgroundColor.of(context), background: GState.apkBackgroundIcon.of(context), foreground: aForeground, radius: GState.iconShape.of(context).radius);
else if (lIcon != null) icon = FittedBox(child: lIcon);
else icon = const ProgressRing();

Expand Down Expand Up @@ -178,7 +179,10 @@ class _ApkInstallerState extends State<ApkInstaller> {
child: Text(startingWSA ? lang.installer_btn_starting : installType?.buttonText(lang) ?? lang.installer_btn_loading),
checked: true,
style: installType == InstallType.DOWNGRADE ? warningButtonTheme : null,
onChanged: !canInstall ? null : (_){ApkInstaller.installApk(ApkReader.APK_FILE, ipAddress, port, lang, installType == InstallType.DOWNGRADE);},
onChanged: !canInstall ? null : (_){
if (Constants.packageType.directInstall) ApkInstaller.installApk(ApkReader.APK_FILE, ipAddress, port, lang, installType == InstallType.DOWNGRADE);
else GState.installCallback.$?.call(ipAddress, port, lang, installType == InstallType.DOWNGRADE);
},
)),
/*const SizedBox(width: 15),noMoveWindow(ToggleButton(
child: const Text('TEST-ICON'),
Expand Down
2 changes: 2 additions & 0 deletions lib/global_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ class GState {
static final apkIcon = SharedValue<Widget?>(value: null);
static final apkBackgroundIcon = SharedValue<Widget?>(value: null);
static final apkForegroundIcon = SharedValue<Widget?>(value: null);
static final apkAdaptiveNoScale = SharedValue<bool>(value: false);
static final apkBackgroundColor = SharedValue<Color?>(value: null);
static final installCallback = SharedValue<Function(String ipAddress, int port, AppLocalizations lang, [bool downgrade])?>(value: null);
// Installation info
static final errorCode = SharedValue<String>(value: "");
static final errorDesc = SharedValue<String>(value: "");
Expand Down
Loading

0 comments on commit 753570f

Please sign in to comment.