-
-
Notifications
You must be signed in to change notification settings - Fork 699
/
root_api.dart
220 lines (197 loc) Β· 5.95 KB
/
root_api.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import 'package:flutter/foundation.dart';
import 'package:root/root.dart';
class RootAPI {
// TODO(aAbed): remove in the future, keep it for now during migration.
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _revancedDirPath = '/data/adb/revanced';
final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async {
try {
final bool? isRooted = await Root.isRootAvailable();
return isRooted != null && isRooted;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
Future<bool> hasRootPermissions() async {
try {
bool? isRooted = await Root.isRootAvailable();
if (isRooted != null && isRooted) {
isRooted = await Root.isRooted();
return isRooted != null && isRooted;
}
return false;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
Future<void> setPermissions(
String permissions,
ownerGroup,
seLinux,
String filePath,
) async {
try {
final StringBuffer commands = StringBuffer();
if (permissions.isNotEmpty) {
commands.writeln('chmod $permissions $filePath');
}
if (ownerGroup.isNotEmpty) {
commands.writeln('chown $ownerGroup $filePath');
}
if (seLinux.isNotEmpty) {
commands.writeln('chcon $seLinux $filePath');
}
await Root.exec(
cmd: commands.toString(),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<bool> isAppInstalled(String packageName) async {
if (packageName.isNotEmpty) {
return fileExists('$_serviceDDirPath/$packageName.sh');
}
return false;
}
Future<List<String>> getInstalledApps() async {
final List<String> apps = List.empty(growable: true);
try {
final String? res = await Root.exec(cmd: 'ls $_revancedDirPath');
if (res != null) {
final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return apps;
}
Future<void> uninstall(String packageName) async {
await Root.exec(
cmd: '''
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
rm -rf $_revancedDirPath/$packageName $_serviceDDirPath/$packageName.sh
''',
);
}
// TODO(aAbed): remove in the future, keep it for now during migration.
Future<void> removeOrphanedFiles() async {
await Root.exec(
cmd: '''
find $_revancedDirPath -type f -name original.apk -delete
for file in "$_serviceDDirPath"/*; do
filename=\$(basename "\$file")
if [ -f "$_postFsDataDirPath/\$filename" ]; then
rm "$_postFsDataDirPath/\$filename"
fi
done
''',
);
}
Future<bool> install(
String packageName,
String originalFilePath,
String patchedFilePath,
) async {
try {
await setPermissions(
'0755',
'shell:shell',
'',
'$_revancedDirPath/$packageName',
);
await installPatchedApk(packageName, patchedFilePath);
await installServiceDScript(packageName);
await runMountScript(packageName);
return true;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
Future<void> installServiceDScript(String packageName) async {
await Root.exec(
cmd: 'mkdir -p $_serviceDDirPath',
);
final String mountScript = '''
#!/system/bin/sh
# Mount using Magisk mirror, if available.
MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin
MIRROR="\$MAGISKTMP/.magisk/mirror"
if [ ! -f \$MIRROR ]; then
MIRROR=""
fi
until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
until [ -d "/sdcard/Android" ]; do sleep 1; done
# Unmount any existing installation to prevent multiple unnecessary mounts.
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
base_path=$_revancedDirPath/$packageName/base.apk
stock_path=\$(pm path $packageName | grep base | sed "s/package://g" )
chcon u:object_r:apk_data_file:s0 \$base_path
mount -o bind \$MIRROR\$base_path \$stock_path
# Kill the app to force it to restart the mounted APK in case it is already running
am force-stop $packageName
'''
.trimMultilineString();
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$mountScript\' > "$scriptFilePath"',
);
await setPermissions('0744', '', '', scriptFilePath);
}
Future<void> installPatchedApk(
String packageName, String patchedFilePath,) async {
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec(
cmd: '''
mkdir -p $_revancedDirPath/$packageName
cp "$patchedFilePath" $newPatchedFilePath
''',
);
await setPermissions(
'0644',
'system:system',
'u:object_r:apk_data_file:s0',
newPatchedFilePath,
);
}
Future<void> runMountScript(
String packageName,
) async {
await Root.exec(cmd: '.$_serviceDDirPath/$packageName.sh');
}
Future<bool> fileExists(String path) async {
try {
final String? res = await Root.exec(
cmd: 'ls $path',
);
return res != null && res.isNotEmpty;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
}
// Remove leading spaces manually until
// https://github.com/dart-lang/language/issues/559 is closed
extension StringExtension on String {
String trimMultilineString() =>
split('\n').map((line) => line.trim()).join('\n').trim();
}